[
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\ndocs/\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\npublic/cdn/\n.artifacts/\nscripts/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarnclean\n\n# dotenv environment variables file\n.env\n.env.*\n!.env.example\n\n# parcel-bundler cache files\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Docusaurus cache and build output\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# IDE files\n.idea/\n.vscode/\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n# macOS\n.DS_Store\n\n# Vite build output\ndist\n\n# Temporary test files\n__test_strip.mjs\n__stripped_output.js\n_temp_builtIn/\n\n#शंकर "
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>. "
  },
  {
    "path": "README.md",
    "content": "# RA2WEB React\n\n免责声明：这是基于《时空分裂》中文版RA2WEB www.ra2web.com 的分析而开发，并意图基于最新的react和three版本进行重构。\n\n但项目所有权利（包括收益权）归《时空分裂》/RA2WEB负责人所有。未经《时空分裂》的所有者/RA2WEB负责人许可，严禁用于任何商业行为。\n\n需要注意的是，《时空分裂》的所有者从未以任何方式开源游戏客户端代码（即便存在诸如mod-sdk之类的周边开源内容）。本项目运行产生的BUG、功能不完善，不能等同视为对《时空分裂》的名誉贬损。任何基于本项目开展商业行为，包括但不限于植入广告、开发“弹幕红警”收受礼物获利、直接封装收费、以“作者”身份骗取赞助和充电收益等，均视为对《时空分裂》原作者Alexandru Ciucă和RA2WEB的侵权。\n\n红色警戒2网页版，一款经典的即时战略类游戏的完整TypeScript重构版本，使用React + TypeScript + Vite + Three.js构建。\n\n![动画](https://github.com/user-attachments/assets/d83f6001-d426-4d49-98a6-8282addc898d)\n\nDisclaimer\n\nThis project is developed based on the analysis of the Chinese version of Chronodivide — RA2WEB (www.ra2web.com), and is intended to be refactored using the latest versions of React and Three.js. All rights to this project, including profit rights, belong to the owner of Chronodivide. Without permission from the owner of Chronodivide, any commercial use of this project is strictly prohibited.\n\nIt should be noted that the owner of Chronodivide has never open-sourced the game client code in any form, even though some peripheral open‑source content such as a mod‑SDK exists. Bugs, incomplete functions or other issues arising from the operation of this project shall not be regarded as damage to the reputation of Chronodivide. Any commercial activities conducted based on this project, including but not limited to placing advertisements, developing a “bullet-screen Red Alert” mode to profit from gifts, directly packaging and selling the project, or fraudulently obtaining sponsorship and donation revenue by claiming to be the “author”, shall be deemed as infringement upon the original author of Chronodivide, Alexandru Ciucă, and RA2WEB.\n\n![image](https://github.com/user-attachments/assets/f146dc1c-ca15-456a-a8f0-4b43f2d431e8)\n\n![image](https://github.com/user-attachments/assets/a23760df-e679-4b32-a9a2-ca51c214c420)\n\n![image](https://github.com/user-attachments/assets/4781f451-7a51-45e2-919b-cbcb8bbd727a)\n\n## 项目简介\n\n本项目是使用Typescript编写，完全对标“红色警戒2”的游戏引擎，本地自行导入红色警戒2美术素材后，就可以获得类似红警2的游玩体验\n\n## 当前技术状态\n\n### 运行时和构建\n\n- 包管理与本地运行时：`Bun 1.3.10`\n- 开发服务器：`Vite 8.0.1`\n- UI：`React 19.2.4` + `react-dom 19.2.4`\n- 类型系统：`TypeScript 5.9.3`\n- 渲染：`three 0.183.2`\n- 自动化：`Playwright 1.58.2`\n- 默认开发和预览端口：`127.0.0.1:4000`\n\n## 快速开始\n\n### 环境要求\n\n- `Bun 1.3+`\n- 现代浏览器，推荐 Chrome / Edge\n- 浏览器需要支持：\n  - `WebGL`\n  - `Web Audio API`\n  - `File System Access API`\n\n### 安装与启动\n\n```bash\ncd redalert2\nbun install\nbun run dev\n```\n\n默认访问地址：\n\n```text\nhttp://127.0.0.1:4000\n```\n\n生产构建与预览：\n\n```bash\nbun run build\nbun run preview\n```\n\n类型检查：\n\n```bash\nbun run typecheck:entry\n```\n\n## 自动化回归\n\n仓库当前已经不再只依赖手点验证。`scripts/` 下维护了一组可直接执行的回归脚本，主要覆盖大厅、进图、机制和 tester 入口。\n\n常用命令包括：\n\n```bash\nbun run debug:game-res-init\nbun run debug:viewport\nbun run debug:options\nbun run debug:storage-explorer\nbun run debug:skirmish\nbun run debug:skirmish-lobby-data\nbun run debug:victory-exit\nbun run debug:superweapon\nbun run debug:nuke\nbun run debug:radiation\nbun run debug:minimap-shroud\nbun run debug:anti-air-hit\nbun run debug:terror-drone\nbun run debug:chrono-legionnaire\nbun run debug:test-entries\nbun run debug:tester-panels\n```\n\n这些脚本的产物默认会写入 `.artifacts/`，便于回看截图和 JSON 结果。\n\n## 测试入口\n\n主菜单中的测试入口目前分为三类：\n\n1. 素材测试\n   - `VXL测试`\n   - `SHP测试`\n   - `音频测试`\n2. 机制测试\n   - `建筑测试`\n   - `载具测试`\n   - `步兵测试`\n   - `飞行器测试`\n3. 场景测试\n   - `大厅测试`\n   - `世界测试`\n   - `移动测试`\n\n这些 tester 页面不是孤立 Demo，而是当前仓库里很重要的调试和回归入口。页面左侧面板状态会同步到调试状态对象，自动化脚本也会直接使用这些入口验证渲染和交互结果。\n\n## 技术架构\n\n### 核心技术栈\n\n- `React 19.2.4`\n- `TypeScript 5.9.3`\n- `Vite 8.0.1`\n- `three 0.183.2`\n- `Bun 1.3.10`\n- `Playwright 1.58.2`\n- `7z-wasm`\n- `file-system-access`\n- `@ffmpeg/ffmpeg`\n- `@ra2web/pcxfile`\n- `@ra2web/wavefile`\n\n### 目录说明\n\n```text\nredalert2/\n├── public/          静态资源、配置、locale、遗留样式\n├── scripts/         Playwright 自动化回归脚本\n├── src/\n│   ├── data/        原版资源格式、编码、地图、VFS\n│   ├── engine/      渲染、音频、资源加载、底层引擎能力\n│   ├── game/        游戏逻辑、对象系统、触发器、规则、超武\n│   ├── gui/         主菜单、HUD、选项、游戏内 UI\n│   ├── network/     网络和联机相关基础设施\n│   ├── tools/       独立 tester 页面\n│   └── util/        通用工具\n├── docs/            对齐记录与工程说明\n└── vite.config.ts   开发和构建配置\n```\n\n### 主要模块\n\n`src/engine/`\n\n- `gfx/`：three 渲染层、材质、批处理、viewport、lighting\n- `renderable/`：游戏对象到可视对象的桥接层\n- `sound/`：音频混音、音乐、音效播放\n- `gameRes/`：资源导入、CDN 加载、缓存与目录处理\n\n`src/game/`\n\n- `gameobject/`：单位、建筑、抛射体、trait、locomotor\n- `rules/`：INI 规则读取与对象规则构建\n- `trigger/`：地图触发器、条件、执行器\n- `superweapon/`：核弹、闪电风暴、超时空等超武逻辑\n\n`src/gui/`\n\n- `screen/mainMenu/`：主菜单、地图选择、大厅、选项\n- `screen/game/`：游戏内 HUD、世界交互、菜单\n- `component/`：React 组件\n- `jsx/`：自定义 UI 渲染桥接\n\n`src/tools/`\n\n- 提供素材、机制、场景三类 tester 页面\n- 当前是调试结果可视化和自动化断言的重要入口\n\n## 开发命令\n\n```bash\nbun run dev\nbun run build\nbun run preview\nbun run typecheck:entry\n```\n\n## 文档与调试约定\n\n- 开发端口固定为 `4000`\n- 主要技术对齐记录维护在 `docs/build-alignment-log.md`\n- 自动化产物默认输出到 `.artifacts/`\n- 构建通过并不等于所有行为已完全对齐，功能层面仍应优先参考专项脚本和实际流程验证\n\n## 贡献建议\n\n提交改动前，至少建议执行：\n\n```bash\nbun run typecheck:entry\nbun run build\n```\n\n如果改动涉及大厅、资源加载、进图、HUD、机制或 tester，请补跑相应的 `debug:*` 脚本。\n\n## 许可证\n\n本项目基于GNU General Public License v3.0（GPL-3.0）许可证开源。详见 [LICENSE](LICENSE) 文件。\n\n### 重要说明\n- 可以自由使用、修改和分发，除非取得RA2WEB负责人许可，否则严禁用于商业目的\n- 必须保留版权声明和许可证文本\n- 任何衍生作品必须使用相同的 GPL-3.0 许可证\n- 必须提供源代码，包括修改后的版本\n- 不能将 GPL 代码集成到专有软件中\n\n**注意：** 本项目仅用于学习和研究目的。红色警戒2是EA公司的知识产权，导入美术素材时请确保拥有合法的游戏副本。\n\n## 致谢\n\n- RA2WEB.COM\n- Three.js 社区\n- React 团队\n- TypeScript 团队\n- 相关开源依赖维护者\n- 红警 2 玩家社区\n\n---\n\n**免责声明**: 本项目仅供学习研究使用，不用于商业目的。红色警戒2及相关商标归EA公司所有。\n\n---\n"
  },
  {
    "path": "index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>网页红井-联机对战平台</title>\n    <meta name=\"keywords\"\n        content=\"红色警戒下载, 如何玩红警, webra2, 苹果如何玩红警, 平板上如何玩红警, 手机上如何玩红警, win7如何玩红警, win10如何玩红警, win11如何玩红警, 红警, 红警2, 红色警戒2, 网页红警, 云红警, 在线游戏, 游戏平台，对战平台，战网, 红色警戒3, 红警3, RA2, RA2WEB\" />\n    <meta name=\"description\" content=\"在网页上就能玩经典的红色井界游戏，无需下载安装，随时随地在手机、电脑、平板甚至手表上畅玩。提供多种游戏模式和地图，与全球玩家实时对战。\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content\" />\n    <link rel=\"stylesheet\" href=\"/css/main-legacy.css\" />\n    <link rel=\"stylesheet\" href=\"/res/fonts/fonts.css\" />\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <div id=\"ra2web-root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html> \n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"redalert2-web\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"packageManager\": \"bun@1.3.10\",\n  \"scripts\": {\n    \"dev\": \"node ./node_modules/vite/bin/vite.js\",\n    \"dev:bun\": \"bun --bun vite\",\n    \"build\": \"bun --bun vite build\",\n    \"test\": \"bun test\",\n    \"typecheck:entry\": \"bun --bun tsc -p tsconfig.build.json\",\n    \"preview\": \"bun --bun vite preview\",\n    \"live:runtime\": \"bun scripts/live-interaction-runtime.mjs\",\n    \"live:runtime:build\": \"bun --bun vite build && bun scripts/live-interaction-runtime.mjs\",\n    \"debug:skirmish\": \"bun scripts/skirmish-flow.mjs\",\n    \"debug:live-interaction\": \"bun scripts/live-interaction-flow.mjs\",\n    \"debug:live-interaction-loading\": \"bun scripts/live-interaction-loading-flow.mjs\",\n    \"debug:perf-smoke\": \"bun scripts/perf-smoke-flow.mjs\",\n    \"debug:perftest\": \"bun scripts/perftest-flow.mjs\",\n    \"debug:game-res-init\": \"bun scripts/game-res-init-flow.mjs\",\n    \"debug:refinery-free-unit\": \"bun scripts/refinery-free-unit-flow.mjs\",\n    \"debug:viewport\": \"bun scripts/viewport-flow.mjs\",\n    \"debug:terror-drone\": \"bun scripts/terror-drone-flow.mjs\",\n    \"debug:chrono-legionnaire\": \"bun scripts/chrono-legionnaire-flow.mjs\",\n    \"debug:radiation\": \"bun scripts/radiation-flow.mjs\",\n    \"debug:storage-explorer\": \"bun scripts/storage-explorer-flow.mjs\",\n    \"debug:options\": \"bun scripts/options-flow.mjs\",\n    \"debug:test-entries\": \"bun scripts/test-entry-flow.mjs\",\n    \"debug:tester-panels\": \"bun scripts/tester-panel-flow.mjs\",\n    \"debug:skirmish-lobby-data\": \"bun scripts/skirmish-lobby-data-flow.mjs\",\n    \"debug:victory-exit\": \"bun scripts/victory-exit-flow.mjs\",\n    \"debug:crate-sound\": \"bun scripts/crate-sound-flow.mjs\",\n    \"debug:superweapon\": \"bun scripts/superweapon-flow.mjs\",\n    \"debug:nuke\": \"bun scripts/nuke-flow.mjs\",\n    \"debug:minimap-shroud\": \"bun scripts/minimap-shroud-flow.mjs\",\n    \"debug:anti-air-hit\": \"bun scripts/anti-air-hit-flow.mjs\",\n    \"debug:lan-mesh\": \"bun scripts/lan-mesh-flow.mjs\",\n    \"debug:lan-app-message\": \"bun scripts/lan-app-message-flow.mjs\",\n    \"debug:lan-entry\": \"bun scripts/lan-entry-shot.mjs\",\n    \"debug:lan-lockstep\": \"bun scripts/lan-lockstep-flow.mjs\",\n    \"debug:lan-match-session\": \"bun scripts/lan-match-session-flow.ts\",\n    \"debug:lan-map-transfer\": \"bun scripts/lan-room-map-transfer-flow.ts\"\n  },\n  \"dependencies\": {\n    \"7z-wasm\": \"^1.1.0\",\n    \"@brakebein/threeoctree\": \"^2.0.1\",\n    \"@datastructures-js/priority-queue\": \"^6.3.5\",\n    \"@ffmpeg/ffmpeg\": \"^0.12.15\",\n    \"@puzzl/core\": \"^1.0.0-beta.1\",\n    \"@ra2web/pcxfile\": \"^1.0.1\",\n    \"@ra2web/wavefile\": \"^1.0.2\",\n    \"@timohausmann/quadtree-ts\": \"2.2.2\",\n    \"classnames\": \"^2.5.1\",\n    \"file-system-access\": \"^1.0.4\",\n    \"js-logger\": \"^1.6.1\",\n    \"jsqr\": \"^1.4.0\",\n    \"liang-barsky\": \"^1.0.12\",\n    \"mersenne-twister\": \"^1.1.0\",\n    \"qrcode\": \"^1.5.4\",\n    \"react\": \"19.2.4\",\n    \"react-dom\": \"19.2.4\",\n    \"shader-particle-engine\": \"^1.0.6\",\n    \"sprintf-js\": \"^1.1.3\",\n    \"stats.js\": \"^0.17.0\",\n    \"three\": \"0.183.2\",\n    \"three.meshline\": \"^1.4.0\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"1.58.2\",\n    \"@types/react\": \"19.2.14\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"@types/sprintf-js\": \"^1.1.4\",\n    \"@types/three\": \"0.183.1\",\n    \"@vitejs/plugin-basic-ssl\": \"^2.3.0\",\n    \"@vitejs/plugin-react\": \"6.0.1\",\n    \"typescript\": \"5.9.3\",\n    \"vite\": \"8.0.1\"\n  }\n}\n"
  },
  {
    "path": "public/config.ini",
    "content": "[General]\ndiscordUrl=https://discord.com\ncsfFile = general.csf\n\n# Where game resources are located\ngameresBaseUrl=/cdn/game-res/v2/\nmapsBaseUrl=/cdn/maps/\nmodsBaseUrl=/cdn/mods/\ngameResArchiveUrl=https://download.ra2web.com/full-pack.7z\npatchNotesUrl=//www.ra2web.com/patch-notes.html\nladderRulesUrl=//www.ra2web.com/ladder-rules.html\nmodSdkUrl=https://github.com/ra2web/mod-sdk\nbreakingNewsUrl=/breaking-news.html\noldClientsBaseUrl=/old/\nquickMatchEnabled=yes\nbotsEnabled=yes\ndebugLogging=wol\ndefaultLanguage=zh-CN\n\nviewport.width=1024\nviewport.height=768\n"
  },
  {
    "path": "public/css/main-legacy.css",
    "content": "html, body {\n    height: 100%;\n}\n\nbody {\n    margin: 0;\n    padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);\n    box-sizing: border-box;\n    overflow: hidden;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    background: rgba(0, 0, 0, .75);\n    overscroll-behavior: none;\n}\n\nhtml:-webkit-full-screen body {\n    background: black;\n}\n\nhtml:-moz-full-screen body {\n    background: black;\n}\n\n#ra2web-root .stats-layer canvas {\n    display: initial !important;\n    cursor: auto !important;\n}\n\n#ra2web-root {\n    position: relative;\n    touch-action: none;\n    user-select: none;\n    -webkit-user-select: none;\n}\n\n#ra2web-root, #ra2web-root input, #ra2web-root select, #ra2web-root .select, #ra2web-root button {\n    font-family: 'Fira Sans Condensed', Arial, sans-serif;\n    font-size: 13px;\n    color: yellow;\n    font-weight: 500;\n    border-radius: 0;\n}\n\n#ra2web-root select option {\n    background: black;\n}\n\n#ra2web-root a:link,\n#ra2web-root a:visited {\n    color: red;\n}\n\n#ra2web-root > * {\n    vertical-align: top;\n}\n\n#loader-wrapper {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    z-index: 1000;\n}\n\n#loader-logo {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    z-index: 10000;\n    background: url(res/img/cd-logo.png) no-repeat center center;\n}\n\n#loader {\n    display: block;\n    position: relative;\n    left: 50%;\n    top: 50%;\n    width: 240px;\n    height: 240px;\n    margin: -120px 0 0 -120px;\n    border-radius: 50%;\n    border: 3px solid transparent;\n    border-top-color: #3498db;\n\tanimation: spin 2s linear infinite;\n    z-index: 1001;\n}\n\n#loader:before {\n    content: \"\";\n    position: absolute;\n    top: 5px;\n    left: 5px;\n    right: 5px;\n    bottom: 5px;\n    border-radius: 50%;\n    border: 3px solid transparent;\n   \tborder-top-color: #e74c3c;\n    animation: spin 3s linear infinite;\n}\n\n#loader:after {\n    content: \"\";\n    position: absolute;\n    top: 15px;\n    left: 15px;\n    right: 15px;\n    bottom: 15px;\n    border-radius: 50%;\n    border: 3px solid transparent;\n    border-top-color: #f9c922;\n    animation: spin 1.5s linear infinite;\n}\n\n@keyframes spin {\n\t0%   {\n\t\ttransform: rotate(0deg);\n\t}\n\t100% {\n\t\ttransform: rotate(360deg);\n\t}\n}\n\n.no-js #loader-wrapper {\n\tdisplay: none;\n}\n\n#ra2web-root video::-webkit-media-controls {\n    display: none;\n}\n\n#ra2web-root .video-wrapper,\n#ra2web-root .video-wrapper video {\n    width: 632px;\n    height: 570px;\n}\n\n#ra2web-root .video-wrapper .logo {\n    position: absolute;\n    left: 325px;\n    top: 355px;\n    width: 400px;\n    height: 44px;\n    transform: translateX(-50%) translateY(-50%);\n    background-image: var(--res-menu-logo);\n}\n\n#ra2web-root .message-box {\n    width: 451px;\n    height: 326px;\n    background-image: var(--res-dlg-bgn);\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    -webkit-transform: translate(-50%, -50%);\n}\n\n#ra2web-root .message-box-content {\n    padding: 50px;\n}\n\n#ra2web-root .message-box-footer {\n    position: absolute;\n    bottom: 0;\n    right: 0;\n    padding: 20px;\n}\n\n#ra2web-root .toasts {\n    position: absolute;\n    top: 16px;\n    left: 16px;\n}\n\n#ra2web-root .toasts .toast {\n    max-width: 600px;\n    width: fit-content;\n    padding: 8px;\n    background: rgba(0, 0, 0, .75);\n    border: 1px red solid;\n    animation: slideInFromLeft .4s;\n}\n\n#ra2web-root .toasts .toast + .toast {\n    margin-top: 8px;\n}\n\n@keyframes slideInFromLeft {\n    0% {\n      transform: translateX(-100%);\n    }\n    100% {\n      transform: translateX(0);\n    }\n  }\n\n#ra2web-root .menu-button {\n    cursor: default;\n    text-align: center;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\n#ra2web-root .menu-button.disabled {\n    color: #6e6e6e;\n    opacity: 0.68;\n    text-shadow: 1px 1px rgba(0, 0, 0, 0.75);\n}\n\n#ra2web-root .menu-version-string {\n    text-align: center;\n}\n\n#ra2web-root .menu-mp-slot {\n    position: relative;\n}\n\n#ra2web-root pre.menu-mp-slot-text {\n    width: 100%;\n    text-align: center;\n    margin: 0;\n    padding: 10px 8px 0 8px;\n    box-sizing: border-box;\n    white-space: pre-line;\n    font-family: inherit;\n    font-size: 12px;\n    text-overflow: ellipsis;\n    overflow: hidden;\n    max-height: 71px;\n}\n\n#ra2web-root .menu-mp-slot-icon {\n    position: absolute;\n    top: 10px;\n    left: 7px;\n}\n\n#ra2web-root .sidebar-title {\n    position: absolute;\n    top: 50%;\n    left: 0;\n    transform: translateY(-50%);\n    width: inherit;\n    text-align: center;\n    font-size: 12px;\n}\n\n#ra2web-root .menu-tooltip {\n    padding: 10px;\n    box-sizing: border-box;\n    width: 0;\n    overflow: hidden;\n    white-space: nowrap;\n}\n\n#ra2web-root .menu-tooltip.anim {\n    transition: width .4s;\n    width: 100%;\n}\n\n#ra2web-root .login-wrapper {\n    width: 451px;\n    height: 500px;\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    -webkit-transform: translate(-50%, -50%);\n}\n\n#ra2web-root .login-wrapper .title {\n    text-align: center;\n    margin-bottom: 20px;\n}\n\n#ra2web-root .login-wrapper .login-form {\n    padding: 0 20px;\n    margin: 24px 0;\n}\n\n#ra2web-root .login-wrapper .server-list {\n    display: inline-block;\n    width: 250px;\n}\n\n#ra2web-root .login-wrapper .server-list,\n#ra2web-root .login-wrapper .refresh-button {\n    vertical-align: top;\n    margin-top: -4px;\n}\n\n#ra2web-root .login-wrapper .refresh-button {\n    margin-left: 8px;\n}\n\n#ra2web-root .login-wrapper .server-list .list-item {\n    display: flex;\n}\n\n#ra2web-root .login-wrapper .server-list .list-item .label {\n    flex-grow: 1;\n}\n\n#ra2web-root .server-ping {\n    width: 70px;\n    justify-content: right;\n    display: flex;\n}\n\n#ra2web-root .server-ping .ping-text {\n    margin-right: 3px;\n}\n\n#ra2web-root .server-ping .ping-text.ping-good {\n    color: lawngreen;\n}\n\n#ra2web-root .server-ping .ping-text.ping-avg {\n    color: yellow;\n}\n\n#ra2web-root .server-ping .ping-text.ping-bad {\n    color: red;\n}\n\n#ra2web-root .list-item.selected .server-ping .ping-text.ping-bad {\n    color: orange;\n    text-shadow: 1px 1px black;\n}\n\n#ra2web-root .login-wrapper .server-list .list-item .offline-text {\n    color: red;\n    text-transform: uppercase;\n}\n\n#ra2web-root .login-wrapper .server-list .list-item .online-text {\n    color: lawngreen;\n    text-transform: uppercase;\n}\n\n#ra2web-root .login-wrapper .news {\n    box-sizing: border-box;\n    height: 200px;\n    overflow-y: auto;\n    overflow-x: hidden;\n}\n\n#ra2web-root .login-wrapper .news > div {\n    font-size: 14px;\n    color: white;\n    font-weight: normal;\n    font-family: Arial, sans-serif;\n}\n\n#ra2web-root .login-box .field {\n    margin-bottom: 10px;\n}\n#ra2web-root .login-box .field label {\n    margin-right: 10px;\n    text-align: right;\n    display: inline-block;\n    min-width: 100px;\n}\n\n#ra2web-root .login-box.new-account-box {\n    width: 500px;\n}\n\n#ra2web-root .new-account-box .login-box .field label {\n    min-width: 130px;\n}\n\n#ra2web-root .login-box.create-game-box .field input[name=\"enablepass\"],\n#ra2web-root .login-box.create-game-box .field input[name=\"lobbypass\"] {\n    margin-right: 10px;\n    vertical-align: middle;\n}\n\n#ra2web-root .prompt-box .field label {\n    display: block;\n}\n\n#ra2web-root .prompt-box .field input[type=\"text\"] {\n    margin-top: 5px;\n}\n\n#ra2web-root .dialog-button {\n    width: 126px;\n    height: 25px;\n    border: none;\n    outline: none;\n    background-image: var(--res-mnbttn);\n    background-position: 0 0;\n    display: block;\n    margin-top: 10px;\n}\n\n#ra2web-root .dialog-button:hover:not(:disabled) {\n    background-position: -126px 0;\n}\n\n#ra2web-root .dialog-button:active:not(:disabled) {\n    background-position: -252px 0;\n}\n\n#ra2web-root .dialog-button:disabled {\n    color: black;\n}\n\n#ra2web-root .lobby-form,\n#ra2web-root .gamebrowser-wrapper,\n#ra2web-root .map-sel-form,\n#ra2web-root .replay-sel-form,\n#ra2web-root .mod-sel-form,\n#ra2web-root .qm-form,\n#ra2web-root .ladder {\n    width: 632px;\n}\n\n#ra2web-root .lobby-form,\n#ra2web-root .qm-form,\n#ra2web-root .ladder {\n    padding: 40px 40px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .lobby-form {\n    padding: 30px 30px;\n}\n\n#ra2web-root .lobby-form.lobby-form-server-sel {\n    padding: 15px 30px;\n}\n\n#ra2web-root .lobby-form .game-server {\n    margin-bottom: 8px;\n    text-align: right;\n    padding-right: 2px;\n}\n\n#ra2web-root .lobby-form .game-server span.label {\n    margin-right: 10px;\n    vertical-align: middle;\n}\n\n#ra2web-root .lobby-form.lobby-form-sp {\n    padding: 40px 59px;\n}\n\n#ra2web-root .lobby-form .player-slots,\n#ra2web-root .lobby-form .game-options,\n#ra2web-root .qm-form .qm-top .opts {\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\n#ra2web-root input[type=\"checkbox\"] {\n    background-image: var(--res-cue-i);\n    width: 18px;\n    height: 18px;\n    -webkit-appearance: none;\n    appearance: none;\n    margin: 0;\n    border: 0;\n}\n\n#ra2web-root input[type=\"checkbox\"]:checked {\n    background-image: var(--res-cce-i);\n}\n\n#ra2web-root input[type=\"checkbox\"].semi-checked-left {\n    background-image: var(--res-cce-il);\n}\n\n#ra2web-root input[type=\"checkbox\"].semi-checked-right {\n    background-image: var(--res-cce-ir);\n}\n\n#ra2web-root input[type=\"range\"] {\n    -webkit-appearance: none;\n    appearance: none;\n    background: transparent;\n    margin: 0;\n    height: 24px;\n}\n\ninput[type=\"range\"]::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    appearance: none;\n    border-radius: 0;\n    border: none;\n    height: 22px;\n    width: 12px;\n    background-color: red;\n    background-image: var(--res-icons-24);\n    background-position: -264px 0;\n}\n\ninput[type=\"range\"]::-moz-range-thumb {\n    -webkit-appearance: none;\n    border-radius: 0;\n    border: none;\n    height: 22px;\n    width: 12px;\n    background-color: red;\n    background-image: var(--res-icons-24);\n    background-position: -264px 0;\n}\n\ninput[type=\"range\"]::-webkit-slider-runnable-track {\n    width: 100%;\n    height: 24px;\n    background: transparent;\n    border: 1px red solid;\n}\n\ninput[type=\"range\"]::-moz-range-track {\n    width: 100%;\n    height: 24px;\n    background: transparent;\n    border: 1px red solid;\n}\n\ninput[type=\"range\"]:focus {\n    outline: none;\n}\n\n#ra2web-root input[type=\"text\"],\n#ra2web-root input[type=\"url\"],\n#ra2web-root input[type=\"password\"],\n#ra2web-root select,\n#ra2web-root .select {\n    background: transparent;\n    border: 1px red solid;\n}\n\n#ra2web-root input[type=\"text\"],\n#ra2web-root input[type=\"url\"],\n#ra2web-root input[type=\"password\"],\n#ra2web-root select,\n#ra2web-root .select {\n    height: 26px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .dialog-button {\n    height: 25px;\n    box-sizing: border-box;\n}\n\n#ra2web-root input[type=\"text\"],\n#ra2web-root input[type=\"url\"],\n#ra2web-root input[type=\"password\"] {\n    padding-left: 4px;\n}\n\n#ra2web-root input:focus,\n#ra2web-root select:focus {\n    outline: none;\n}\n\n#ra2web-root select:disabled,\n#ra2web-root .select.disabled,\n#ra2web-root input:disabled,\n#ra2web-root label input:disabled + span,\n#ra2web-root .lobby-form .all-disabled span,\n#ra2web-root .lobby-form .all-disabled input[type=\"text\"]:disabled {\n    color: #9c0000;\n    border-color: #9c0000;\n}\n\n#ra2web-root input[type=\"checkbox\"]:disabled,\n#ra2web-root input[type=\"range\"]:disabled {\n    opacity: 0.7;\n}\n\n#ra2web-root input[type=\"range\"] + input[type=\"text\"]:disabled {\n    opacity: 1;\n}\n\n#ra2web-root .select {\n    position: relative;\n    user-select: none;\n    -webkit-user-select: none;\n}\n\n#ra2web-root .select .select-value {\n    padding: 4px 21px 4px 3px;\n    height: 100%;\n    box-sizing: border-box;\n}\n\n#ra2web-root .select::before {\n    pointer-events: none;\n    content: \"\";\n    position: absolute;\n    right: 1px;\n    top: 1px;\n    width: 18px;\n    height: 22px;\n    background-image: var(--res-icons-24);\n    background-position: -96px 0;\n}\n#ra2web-root .select::before:hover {\n    background-position: -120px 0;\n}\n\n#ra2web-root .select .select-layer {\n    z-index: 1;\n    position: absolute;\n    top: 26px;\n    left: 0;\n    width: 100%;\n    outline: 1px red solid;\n    background: rgba(0, 0, 0, .75);\n}\n\n#ra2web-root .select .select-layer .option {\n    padding: 4px 3px;\n    height: 24px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .select .select-value > div,\n#ra2web-root .select .select-layer .option > div {\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n#ra2web-root .select .select-layer .option.selected {\n    background: red;\n}\n\n#ra2web-root .select .select-layer .option.disabled {\n    color: gray;\n}\n\n#ra2web-root .player-color-select.bg-color .select .select-value {\n    padding: 2px 21px 2px 2px;\n}\n\n#ra2web-root .player-color-select .select .select-layer .option.bg-color {\n    padding: 2px;\n}\n\n#ra2web-root .player-color-select.bg-color .select .select-value > div,\n#ra2web-root .player-color-select .select .select-layer .option.bg-color > div {\n    width: 100%;\n    height: 100%;\n}\n\n#ra2web-root .player-color-select.bg-color .select.disabled .select-value > div {\n    opacity: .75;\n}\n\n#ra2web-root .lobby-form .player-slots {\n    height: 263px; /* 27px * 9 slots + 20px header */\n}\n\n#ra2web-root .lobby-form .player-slot-header .player-header-players {\n    margin-left: 57px; /* 16px rank, 3px spacing, 16px ping, 3px spacing, 16px ready, 3px spacing */\n    width: 150px;\n}\n\n#ra2web-root .lobby-form.lobby-form-sp .player-slot-header .player-header-players {\n    margin-left: 0;\n}\n\n#ra2web-root .lobby-form .player-slot-header .player-header-side {\n    margin-left: 57px; /* 3px spacing, 47px icon, 7 px spacing */\n    width: 120px;\n}\n\n#ra2web-root .lobby-form .player-slot-header .player-header-color,\n#ra2web-root .lobby-form .player-slot-header .player-header-position,\n#ra2web-root .lobby-form .player-slot-header .player-header-team {\n    margin-left: 7px;\n    width: 55px;\n}\n\n#ra2web-root .lobby-form .player-slot {\n    height: 26px;\n}\n\n#ra2web-root .lobby-form .player-slot.player-slot-header {\n    height: 20px;\n}\n\n#ra2web-root .lobby-form .player-slot + .player-slot {\n    margin-top: 1px;\n}\n\n#ra2web-root .country-select {\n    display: inline-block;\n}\n\n#ra2web-root .lobby-form .player-slot .rank-indicator,\n#ra2web-root .lobby-form .player-slot .ping-indicator,\n#ra2web-root .lobby-form .player-slot .player-status,\n#ra2web-root .player-country-icon,\n#ra2web-root .lobby-form .player-slot .player-country-select,\n#ra2web-root .lobby-form .player-slot .player-color-select,\n#ra2web-root .lobby-form .player-slot .player-start-pos-select,\n#ra2web-root .lobby-form .player-slot .player-team-select,\n#ra2web-root .lobby-form .player-slot-header > div {\n    display: inline-block;\n    vertical-align: middle;\n    margin-left: 3px;\n}\n\n#ra2web-root .lobby-form .player-slot .rank-indicator {\n    margin-left: 0;\n}\n\n#ra2web-root .rank-indicator,\n#ra2web-root .ping-indicator,\n#ra2web-root .lobby-form .player-slot .player-status {\n    width: 16px;\n    height: 16px;\n}\n\n#ra2web-root .lobby-form.lobby-form-sp .player-slot .rank-indicator,\n#ra2web-root .lobby-form.lobby-form-sp .player-slot .ping-indicator,\n#ra2web-root .lobby-form.lobby-form-sp .player-slot .player-status {\n    display: none;\n}\n\n#ra2web-root .player-country-icon {\n    width: 47px;\n    height: 23px;\n}\n\n#ra2web-root .lobby-form .player-slot .player-country-select,\n#ra2web-root .lobby-form .player-slot .player-color-select,\n#ra2web-root .lobby-form .player-slot .player-start-pos-select,\n#ra2web-root .lobby-form .player-slot .player-team-select {\n    margin-left: 7px;\n}\n\n#ra2web-root .lobby-form .player-slot .player-country-select .select,\n#ra2web-root .qm-form .player-country-select .select {\n    width: 120px;\n}\n\n#ra2web-root .lobby-form .player-slot .player-color-select .select,\n#ra2web-root .lobby-form .player-slot .player-start-pos-select .select,\n#ra2web-root .lobby-form .player-slot .player-team-select .select,\n#ra2web-root .qm-form .player-color-select {\n    width: 55px;\n}\n\n#ra2web-root .lobby-form .player-slot .player-name {\n    margin-left: 3px;\n    vertical-align: middle;\n    width: 150px;\n}\n\n#ra2web-root .lobby-form.lobby-form-sp .player-slot .player-name {\n    margin-left: 0;\n}\n\n#ra2web-root .lobby-form .game-options {\n    margin-top: 10px;\n}\n\n#ra2web-root .lobby-form .game-options::after {\n    content: \"\";\n    display: block;\n    clear: both;\n}\n\n#ra2web-root .lobby-form .game-options-left {\n    margin-left: 0;\n    float: left;\n    height: 114px;\n    column-count: 2;\n    column-gap: 10px;\n    column-fill: auto;\n}\n\n#ra2web-root .lobby-form.lobby-form-sp .game-options-left {\n    column-gap: 0;\n    width: 250px;\n    padding-top: 4px;\n}\n\n#ra2web-root .lobby-form .game-options-right {\n    float: right;\n}\n\n#ra2web-root .lobby-form .game-options-left > div {\n    margin-bottom: 5px;\n}\n\n#ra2web-root .lobby-form.lobby-form-sp .game-options-left > div {\n    margin-bottom: 12px;\n}\n\n#ra2web-root .lobby-form .game-options label {\n    display: inline-block;\n    line-height: 18px;\n}\n\n#ra2web-root .lobby-form .game-options-left label {\n    white-space: nowrap;\n}\n\n#ra2web-root input[type=\"checkbox\"] {\n    vertical-align: top;\n    margin-right: 3px;\n}\n\n#ra2web-root .lobby-form .game-options-right > div {\n    margin-bottom: 6px;\n    margin-left: 10px;\n}\n\n#ra2web-root .slider-item input[type=\"range\"] {\n    width: 79px;\n    vertical-align: top;\n    margin-left: 0;\n    margin-right: 0;\n}\n\n#ra2web-root .slider-item input[type=\"text\"] {\n    width: 50px;\n    box-sizing: border-box;\n    text-align: center;\n    padding-left: 0;\n    border-color: red;\n    color: yellow;\n}\n\n#ra2web-root .slider-item span.label {\n    width: 100px;\n    display: inline-block;\n    vertical-align: middle;\n}\n\n#ra2web-root .slider-item input[type=\"text\"] {\n    height: 24px;\n    background: #5a0000;\n    box-shadow: inset 0 0 10px #000;\n}\n\n#ra2web-root .lobby-form .game-options-right .checkbox-item {\n    margin-top: 12px;\n}\n\n#ra2web-root .list {\n    border: 1px red solid;\n    overflow-y: auto;\n    overflow-x: hidden;\n}\n\n#ra2web-root .list-title {\n    text-align: center;\n    margin-bottom: 8px;\n}\n\n#ra2web-root .list-header,\n#ra2web-root .list .list-item {\n    user-select: none;\n    -webkit-user-select: none;\n    cursor: default;\n    padding: 3px;\n}\n\n#ra2web-root .list .list-item.selected {\n    background: red;\n}\n\n#ra2web-root .list .list-item.disabled {\n    background: transparent;\n    color: gray;\n}\n\n#ra2web-root .gamebrowser-wrapper {\n    padding: 16px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .gamebrowser-wrapper .gamebrowser-top {\n    overflow: hidden;\n}\n\n#ra2web-root .gamebrowser-wrapper .gamebrowser-bottom {\n    display: flex;\n    align-items: stretch;\n    margin-top: 10px;\n}\n\n#ra2web-root .gamebrowser-wrapper .chat-wrapper {\n    width: 410px;\n}\n\n#ra2web-root .gamebrowser-wrapper .players-list {\n    height: 242px;\n    width: 180px;\n    margin-left: 10px;\n}\n\n#ra2web-root .gamebrowser-wrapper .games-header {\n    height: 24px;\n}\n\n#ra2web-root .icon-button {\n    width: 24px;\n    height: 24px;\n    background-image: var(--res-icons-24);\n    border: none;\n    outline: none;\n}\n\n#ra2web-root .gamebrowser-wrapper .refresh-button {\n    background-position: 0 0;\n    vertical-align: middle;\n}\n\n#ra2web-root .gamebrowser-wrapper .refresh-button:hover {\n    background-position: -24px 0;\n}\n\n#ra2web-root .gamebrowser-wrapper .chat-wrapper .messages {\n    height: 200px;\n}\n\n#ra2web-root .chat-wrapper .send-message-button {\n    background-position: -48px 0;\n}\n\n#ra2web-root .chat-wrapper .send-message-button:hover {\n    background-position: -72px 0;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-flags span {\n    width: 16px;\n    height: 16px;\n    display: inline-block;\n    margin-right: 3px;\n    vertical-align: middle;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-map,\n#ra2web-root .gamebrowser-wrapper .games .game .game-name {\n    margin-right: 5px;\n    width: 150px;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-players {\n    width: 30px;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-map,\n#ra2web-root .gamebrowser-wrapper .games .game .game-name,\n#ra2web-root .gamebrowser-wrapper .games .game .game-players,\n#ra2web-root .gamebrowser-wrapper .games .game .game-host,\n#ra2web-root .gamebrowser-wrapper .games .game .game-ping {\n    display: inline-block;\n    vertical-align: middle;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-host .rank-indicator {\n    display: inline-block;\n    vertical-align: top;\n    margin-left: 3px;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-flags img {\n    vertical-align: middle;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .games-label {\n    margin-left: 3px;\n    vertical-align: middle;\n}\n\n#ra2web-root .gamebrowser-wrapper .games-list {\n    height: 235px;\n}\n\n#ra2web-root .players-list .player {\n    padding: 3px;\n    text-overflow: ellipsis;\n    overflow: hidden;\n    white-space: nowrap;\n    cursor: pointer;\n}\n\n#ra2web-root .players-list .player.operator {\n    color: cyan;\n}\n\n#ra2web-root .players-list .player .channel-op-indicator {\n    display: inline-block;\n    vertical-align: middle;\n    width: 14px;\n    height: 14px;\n}\n\n#ra2web-root .players-list .player .rank-indicator {\n    display: inline-block;\n    vertical-align: top;\n    margin-right: 3px;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-host {\n    width: 129px;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-ping {\n    width: 30px;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-ping meter {\n    width: 100%;\n    height: 11px;\n    position: relative;\n    top: -1px;\n\n    /* Needed for Firefox */\n    background: transparent;\n    border-width: 0;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-ping meter::-webkit-meter-bar {\n    background: transparent;\n    border-radius: 0;\n    border-width: 0;\n    height: 11px;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-ping meter::-moz-meter-bar {\n    background: transparent;\n    border-radius: 0;\n    border-width: 0;\n    height: 11px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-ping meter::-webkit-meter-optimum-value {\n    background-blend-mode: multiply, screen, multiply;\n    background:\n        linear-gradient(90deg, #b0b0b0, #fff),\n        linear-gradient(180deg, #606060 20%, #000 21%),\n        linear-gradient(180deg, #fff 79%, #ccc 80%),\n        linear-gradient(limegreen, limegreen);\n}\n#ra2web-root .gamebrowser-wrapper .games .game .game-ping meter:-moz-meter-optimum::-moz-meter-bar {\n    background-blend-mode: multiply, screen, multiply;\n    background:\n        linear-gradient(90deg, #b0b0b0, #fff),\n        linear-gradient(180deg, #606060 20%, #000 21%),\n        linear-gradient(180deg, #fff 79%, #ccc 80%),\n        linear-gradient(limegreen, limegreen);\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-ping meter::-webkit-meter-suboptimum-value {\n    background-blend-mode: multiply, screen, multiply;\n    background:\n        linear-gradient(90deg, #b0b0b0, #fff),\n        linear-gradient(180deg, #606060 20%, #000 21%),\n        linear-gradient(180deg, #fff 79%, #ccc 80%),\n        linear-gradient(orange, orange);\n}\n#ra2web-root .gamebrowser-wrapper .games .game .game-ping meter:-moz-meter-sub-optimum::-moz-meter-bar {\n    background-blend-mode: multiply, screen, multiply;\n    background:\n        linear-gradient(90deg, #b0b0b0, #fff),\n        linear-gradient(180deg, #606060 20%, #000 21%),\n        linear-gradient(180deg, #fff 79%, #ccc 80%),\n        linear-gradient(orange, orange);\n}\n\n#ra2web-root .gamebrowser-wrapper .games .game .game-ping meter::-webkit-meter-even-less-good-value {\n    background-blend-mode: multiply, screen, multiply;\n    background:\n        linear-gradient(90deg, #808080, #fff),\n        linear-gradient(180deg, #606060 20%, #000 21%),\n        linear-gradient(180deg, #fff 79%, #ccc 80%),\n        linear-gradient(red, red);\n}\n#ra2web-root .gamebrowser-wrapper .games .game .game-ping meter:-moz-meter-sub-sub-optimum::-moz-meter-bar {\n    background-blend-mode: multiply, screen, multiply;\n    background:\n        linear-gradient(90deg, #808080, #fff),\n        linear-gradient(180deg, #606060 20%, #000 21%),\n        linear-gradient(180deg, #fff 79%, #ccc 80%),\n        linear-gradient(red, red);\n}\n\n#ra2web-root .lobby-form .chat-wrapper {\n    margin-top: 10px;\n    clear: both;\n}\n\n#ra2web-root .chat-wrapper .messages {\n    margin-bottom: 10px;\n    padding: 3px;\n    border: 1px red solid;\n    height: 125px;\n    overflow-y: auto;\n}\n\n#ra2web-root .lobby-form .messages {\n    height: 70px;\n}\n\n#ra2web-root .chat-wrapper .messages .message {\n    white-space: pre-wrap;\n    word-wrap: break-word;\n}\n\n#ra2web-root .chat-wrapper .messages .message.operator-message {\n    color: cyan !important;\n}\n\n#ra2web-root .chat-wrapper .messages .message.type-page {\n    color: white;\n}\n\n#ra2web-root .chat-wrapper .messages .message.type-whisper {\n    color: mediumpurple;\n}\n\n#ra2web-root .chat-wrapper .messages .message .user-link {\n    cursor: pointer;\n}\n\n#ra2web-root .chat-wrapper .new-message-wrapper {\n    padding-right: 27px; /** 3px margin + 24px icon */\n    position: relative;\n}\n\n#ra2web-root .chat-wrapper .new-message {\n    width: 100%;\n    display: flex;\n    align-items: center;\n}\n\n#ra2web-root .chat-wrapper .new-message input {\n    flex-grow: 1;\n}\n\n#ra2web-root .chat-wrapper .new-message * + input {\n    margin-left: 8px;\n}\n\n#ra2web-root .chat-wrapper .new-message input::placeholder {\n    color: gray;\n}\n\n#ra2web-root .chat-wrapper .send-message-button {\n    position: absolute;\n    right: 0;\n    top: 0;\n}\n\n#ra2web-root .map-sel-form {\n    padding: 86px 86px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .map-sel-form .map-sel-title {\n    text-align: center;\n    margin-bottom: 40px;\n}\n\n#ra2web-root .map-sel-form .map-sel-body {\n    display: flex;\n}\n#ra2web-root .map-sel-form .map-sel-body > * + * {\n    margin-left: 20px;\n}\n\n#ra2web-root .map-sel-form .map-sel-game-mode {\n    width: 150px;\n}\n\n#ra2web-root .map-sel-form .map-sel-game-mode .list-title {\n    line-height: 26px;\n}\n\n#ra2web-root .map-sel-form .map-sel-map {\n    width: 250px;\n}\n\n#ra2web-root .map-sel-form .game-mode-list,\n#ra2web-root .map-sel-form .map-list {\n    height: 300px;\n}\n\n#ra2web-root .map-sel-form .list-header,\n#ra2web-root .map-sel-form .map-list .list-item {\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n#ra2web-root .map-sel-form .map-list-title {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    text-align: left;\n}\n\n#ra2web-root .map-sel-form .map-list-sort label {\n    font-size: 16px;\n    vertical-align: middle;\n}\n\n#ra2web-root .map-sel-form .map-list-sort .map-list-sort-select {\n    margin-left: 5px;\n    min-width: 100px;\n}\n\n#ra2web-root .map-sel-form .map-sel-search {\n    margin-top: 10px;\n}\n\n#ra2web-root .map-sel-form .map-sel-search label {\n    display: flex;\n    align-items: center;\n}\n\n#ra2web-root .map-sel-form .map-sel-search label span {\n    margin-right: 5px;\n}\n\n#ra2web-root .map-sel-form .map-sel-search label input {\n    flex: 1 0;\n}\n\n#ra2web-root .lan-setup-form {\n    padding: 20px 34px 26px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .lan-setup-form[data-lan-view=\"entry\"] {\n    height: 100%;\n    min-height: 100%;\n    display: flex;\n    flex-direction: column;\n}\n\n#ra2web-root .lan-setup-form[data-lan-view=\"waiting\"] {\n    padding: 18px 30px 22px;\n    height: 100%;\n    min-height: 100%;\n    display: flex;\n    flex-direction: column;\n}\n\n#ra2web-root .lan-setup-form .lan-setup-notice {\n    margin-bottom: 18px;\n    padding: 8px 10px;\n    border: 1px red solid;\n    background: rgba(32, 0, 0, 0.45);\n    color: #ffcc7a;\n    line-height: 1.35;\n}\n\n#ra2web-root .lan-setup-form .lan-setup-content {\n    display: flex;\n    align-items: flex-start;\n}\n\n#ra2web-root .lan-setup-form .lan-setup-content > * + * {\n    margin-left: 16px;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-layout {\n    flex: 1 1 auto;\n    min-height: 510px;\n    display: flex;\n    flex-direction: column;\n    gap: 14px;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-profile-panel {\n    flex: 0 0 auto;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-profile-grid {\n    display: grid;\n    grid-template-columns: minmax(260px, 1.2fr) minmax(220px, 0.9fr);\n    gap: 16px;\n    align-items: stretch;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-profile-editor {\n    display: flex;\n    flex-direction: column;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-field-hint {\n    margin-top: 8px;\n    color: silver;\n    line-height: 1.35;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-profile-stats {\n    display: grid;\n    grid-template-columns: repeat(2, minmax(0, 1fr));\n    gap: 10px;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-stat {\n    border: 1px red solid;\n    background: rgba(0, 0, 0, 0.3);\n    min-height: 58px;\n    padding: 7px 10px 6px;\n    display: flex;\n    flex-direction: column;\n    justify-content: space-between;\n    box-sizing: border-box;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-stat span {\n    color: silver;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-stat strong {\n    color: white;\n    font-weight: inherit;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-panel,\n#ra2web-root .lan-setup-form .lan-room-summary-panel,\n#ra2web-root .lan-setup-form .lan-room-loading-panel {\n    min-height: 170px;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-recent-panel {\n    flex: 1 1 auto;\n    min-height: 292px;\n    display: flex;\n    flex-direction: column;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-recent-list {\n    flex: 1 1 auto;\n    min-height: 240px;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-recent-item {\n    padding: 9px 10px 8px;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-recent-item + .lan-entry-recent-item {\n    border-top: 1px solid rgba(255, 0, 0, 0.35);\n}\n\n#ra2web-root .lan-setup-form .lan-entry-recent-item-top,\n#ra2web-root .lan-setup-form .lan-entry-recent-item-meta {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 10px;\n    flex-wrap: wrap;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-recent-item-top strong {\n    color: white;\n    font-weight: inherit;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-recent-item-top span,\n#ra2web-root .lan-setup-form .lan-entry-recent-item-members {\n    color: silver;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-recent-item-meta {\n    margin-top: 4px;\n    font-size: 15px;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-recent-item-members {\n    margin-top: 5px;\n    line-height: 1.35;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-recent-chip {\n    display: inline-flex;\n    align-items: center;\n    min-height: 20px;\n    padding: 2px 7px 1px;\n    border: 1px red solid;\n    background: rgba(34, 0, 0, 0.45);\n    color: #ffe165;\n    box-sizing: border-box;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-empty-state {\n    flex: 1 1 auto;\n    min-height: 180px;\n    border: 1px red solid;\n    background: rgba(0, 0, 0, 0.2);\n    padding: 14px;\n    color: silver;\n    line-height: 1.45;\n    display: flex;\n    align-items: center;\n}\n\n#ra2web-root .lan-setup-form .lan-setup-main {\n    flex: 1 1 auto;\n    min-width: 0;\n}\n\n#ra2web-root .lan-setup-form .lan-waiting-main {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    margin-top: auto;\n}\n\n#ra2web-root .lan-setup-form .lan-room-status-strip {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 8px;\n    margin-bottom: 10px;\n}\n\n#ra2web-root .lan-setup-form .lan-status-chip {\n    display: inline-flex;\n    align-items: center;\n    gap: 4px;\n    min-height: 24px;\n    padding: 3px 8px 2px;\n    border: 1px red solid;\n    background: rgba(0, 0, 0, 0.25);\n    line-height: 1.35;\n    box-sizing: border-box;\n}\n\n#ra2web-root .lan-setup-form .lan-status-chip strong {\n    color: white;\n    font-weight: inherit;\n}\n\n#ra2web-root .lan-setup-form .lan-status-divider {\n    color: silver;\n}\n\n#ra2web-root .lan-setup-form .lan-room-form-shell {\n    margin-top: 14px;\n    flex: 1 1 auto;\n    display: flex;\n    flex-direction: column;\n    min-height: 0;\n}\n\n#ra2web-root .lan-setup-form .lan-room-form-shell-compact {\n    margin-top: 0;\n}\n\n#ra2web-root .lan-setup-form .lan-room-form-shell > .lobby-form {\n    width: 100%;\n    flex: 1 1 auto;\n    min-height: 0;\n    padding: 0;\n    box-sizing: border-box;\n}\n\n#ra2web-root .lan-setup-form[data-lan-view=\"waiting\"] .lan-room-form-shell > .lobby-form {\n    display: flex;\n    flex-direction: column;\n}\n\n#ra2web-root .lan-setup-form .lan-room-form-shell > .lobby-form .player-slots {\n    height: auto;\n    min-height: 0;\n    flex: 0 0 auto;\n}\n\n#ra2web-root .lan-setup-form[data-lan-view=\"waiting\"] .lan-room-form-shell > .lobby-form .player-slots {\n    min-height: 263px;\n}\n\n#ra2web-root .lan-setup-form .lan-room-form-shell > .lobby-form .player-slot:empty {\n    display: none;\n}\n\n#ra2web-root .lan-setup-form[data-lan-view=\"waiting\"] .lan-room-form-shell > .lobby-form .game-options {\n    margin-top: auto;\n    padding-top: 12px;\n}\n\n#ra2web-root .lan-setup-form .lan-room-form-shell > .lobby-form .lobby-form-before-chat {\n    margin-top: 4px;\n    clear: both;\n}\n\n#ra2web-root .lan-setup-form .lan-room-form-shell > .lobby-form .lobby-form-before-chat .lan-room-status-strip {\n    margin-bottom: 0;\n}\n\n#ra2web-root .lan-setup-form .lan-room-form-shell > .lobby-form .chat-wrapper {\n    margin-top: 4px;\n}\n\n#ra2web-root .lan-setup-form .lan-room-form-shell > .lobby-form .messages {\n    height: 54px;\n    margin-bottom: 6px;\n}\n\n#ra2web-root .lan-setup-form .lan-room-loading-panel-compact {\n    min-height: 0;\n}\n\n#ra2web-root .lan-setup-form .lan-panel {\n    border: 1px red solid;\n    background: rgba(0, 0, 0, 0.25);\n    padding: 10px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .lan-setup-form .lan-panel + .lan-panel {\n    margin-top: 14px;\n}\n\n#ra2web-root .lan-setup-form .lan-entry-layout > .lan-panel + .lan-panel,\n#ra2web-root .lan-setup-form .lan-dialog-grid > .lan-panel + .lan-panel {\n    margin-top: 0;\n}\n\n#ra2web-root .lan-setup-form .lan-panel-header {\n    display: flex;\n    align-items: baseline;\n    justify-content: space-between;\n    gap: 12px;\n    margin-bottom: 8px;\n}\n\n#ra2web-root .lan-setup-form .lan-panel-header h3 {\n    margin: 0;\n    font-size: 20px;\n}\n\n#ra2web-root .lan-setup-form .lan-panel-header span {\n    color: silver;\n    text-align: right;\n}\n\n#ra2web-root .lan-setup-form .lan-input-label {\n    display: block;\n    margin-bottom: 5px;\n}\n\n#ra2web-root .lan-setup-form .lan-text-input {\n    width: 100%;\n    box-sizing: border-box;\n}\n\n#ra2web-root .lan-setup-form .lan-join-hint {\n    line-height: 1.45;\n}\n\n#ra2web-root .lan-setup-form .lan-sdp-textarea {\n    width: 100%;\n    min-height: 118px;\n    resize: vertical;\n    box-sizing: border-box;\n    border: 1px red solid;\n    background: black;\n    color: white;\n    padding: 7px 8px;\n    font: inherit;\n}\n\n#ra2web-root .lan-setup-form .lan-sdp-textarea::placeholder {\n    color: gray;\n}\n\n#ra2web-root .lan-setup-form .lan-actions {\n    margin-top: 10px;\n    display: flex;\n    align-items: center;\n    flex-wrap: wrap;\n    gap: 8px;\n}\n\n#ra2web-root .lan-setup-form .lan-hint {\n    color: silver;\n}\n\n#ra2web-root .lan-setup-form .lan-qr-card {\n    margin-bottom: 10px;\n}\n\n#ra2web-root .lan-setup-form .lan-qr-artwork,\n#ra2web-root .lan-setup-form .lan-scanner-preview {\n    border: 1px red solid;\n    background: black;\n    min-height: 286px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n#ra2web-root .lan-setup-form .lan-qr-artwork img,\n#ra2web-root .lan-setup-form .lan-scanner-preview video {\n    display: block;\n    width: 100%;\n    max-width: 286px;\n    image-rendering: pixelated;\n}\n\n#ra2web-root .lan-setup-form .lan-qr-placeholder {\n    padding: 20px;\n    text-align: center;\n    color: silver;\n    line-height: 1.4;\n}\n\n#ra2web-root .lan-setup-form .lan-hidden-input {\n    display: none;\n}\n\n#ra2web-root .lan-setup-form .lan-error-text {\n    margin-top: 10px;\n    color: #ff8888;\n}\n\n#ra2web-root .lan-setup-form .lan-chat-panel .messages {\n    height: 110px;\n}\n\n#ra2web-root .lan-dialog-overlay {\n    position: fixed;\n    inset: 0;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background: rgba(0, 0, 0, 0.65);\n    z-index: 60;\n    padding: 20px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .lan-dialog {\n    width: min(760px, 100%);\n    max-height: calc(100vh - 40px);\n    overflow: auto;\n    border: 1px red solid;\n    background: rgba(10, 0, 0, 0.96);\n    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05);\n}\n\n#ra2web-root .lan-dialog.lan-dialog-wide {\n    width: min(1080px, 100%);\n}\n\n#ra2web-root .lan-dialog-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 12px;\n    padding: 14px 16px 12px;\n    border-bottom: 1px solid rgba(255, 0, 0, 0.6);\n}\n\n#ra2web-root .lan-dialog-header h3 {\n    margin: 0;\n    font-size: 22px;\n}\n\n#ra2web-root .lan-dialog-close {\n    min-width: 34px;\n    min-height: 34px;\n    border: 1px red solid;\n    background: black;\n    color: white;\n    font: inherit;\n    cursor: pointer;\n}\n\n#ra2web-root .lan-dialog-body {\n    padding: 16px;\n}\n\n#ra2web-root .lan-dialog-body > * + * {\n    margin-top: 16px;\n}\n\n#ra2web-root .lan-dialog-grid {\n    display: grid;\n    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);\n    gap: 16px;\n    align-items: start;\n}\n\n#ra2web-root .lan-setup-form .tone-good {\n    color: #55ff55;\n}\n\n#ra2web-root .lan-setup-form .tone-warn {\n    color: #ffcc66;\n}\n\n#ra2web-root .lan-setup-form .tone-bad {\n    color: #ff6666;\n}\n\n@media (max-width: 700px) {\n    #ra2web-root .lan-setup-form {\n        padding: 20px 18px;\n    }\n\n    #ra2web-root .lan-setup-form .lan-entry-layout {\n        gap: 12px;\n    }\n\n    #ra2web-root .lan-setup-form .lan-setup-content {\n        display: block;\n    }\n\n    #ra2web-root .lan-setup-form .lan-setup-content > * + * {\n        margin-left: 0;\n        margin-top: 16px;\n    }\n\n    #ra2web-root .lan-setup-form .lan-panel-header {\n        display: block;\n    }\n\n    #ra2web-root .lan-setup-form .lan-panel-header span {\n        display: block;\n        margin-top: 4px;\n        text-align: left;\n    }\n\n    #ra2web-root .lan-setup-form .lan-entry-profile-grid,\n    #ra2web-root .lan-setup-form .lan-entry-profile-stats {\n        grid-template-columns: 1fr;\n    }\n\n    #ra2web-root .lan-dialog-overlay {\n        padding: 10px;\n    }\n\n    #ra2web-root .lan-dialog {\n        max-height: calc(100vh - 20px);\n    }\n\n    #ra2web-root .lan-dialog-grid {\n        grid-template-columns: 1fr;\n    }\n}\n\n#ra2web-root ::-webkit-scrollbar {\n    width: 19px;\n    height: 18px;\n}\n#ra2web-root ::-webkit-scrollbar,\n#ra2web-root ::-webkit-scrollbar-button:vertical,\n#ra2web-root ::-webkit-scrollbar-thumb:vertical {\n    border-left: 1px red solid;\n    border-right: 1px red solid;\n}\n#ra2web-root ::-webkit-scrollbar-button:vertical,\n#ra2web-root ::-webkit-scrollbar-thumb:vertical {\n    background-image: var(--res-icons-24);\n}\n#ra2web-root ::-webkit-scrollbar-thumb:vertical {\n    border-top: 1px solid #a20000;\n    border-bottom: 1px solid #a20000;\n}\n#ra2web-root ::-webkit-scrollbar-button:vertical:increment {\n    background-position: -96px 0;\n    width: 18px;\n    height: 22px;\n}\n#ra2web-root ::-webkit-scrollbar-button:vertical:increment:hover {\n    background-position: -120px 0;\n}\n#ra2web-root ::-webkit-scrollbar-button:vertical:decrement {\n    background-position: -144px 0;\n    width: 18px;\n    height: 22px;\n}\n#ra2web-root ::-webkit-scrollbar-button:vertical:decrement:hover {\n    background-position: -168px 0;\n}\n#ra2web-root ::-webkit-scrollbar-thumb:vertical {\n    background-position: -216px 0;\n    background-repeat: repeat-y;\n}\n\n#ra2web-root .loading-screen .special-unit-name {\n    position: absolute;\n    top: 94px;\n    left: 20px;\n    color: black;\n    text-transform: uppercase;\n}\n\n#ra2web-root .loading-screen .briefing-text {\n    position: absolute;\n    top: 170px;\n    left: 20px;\n    background: rgba(0, 0, 0, .5);\n    padding: 3px;\n    width: 400px;\n    font-weight: bold;\n}\n\n\n#ra2web-root .loading-screen .loading-text {\n    position: absolute;\n    top: 280px;\n    left: 20px;\n    background: rgba(0, 0, 0, .5);\n    padding: 3px;\n    font-weight: bold;\n}\n\n#ra2web-root .loading-screen .player-status-container {\n    position: absolute;\n    top: 300px;\n    left: 20px;\n    background: rgba(0, 0, 0, .5);\n    padding: 3px;\n    width: 400px;\n}\n\n#ra2web-root .loading-screen .player-status {\n    margin-top: 3px;\n    display: flex;\n    align-items: center;\n}\n\n#ra2web-root progress {\n    background: transparent;\n    border-width: 1px;\n    border-style: solid;\n    border-color: currentColor;\n    padding: 2px;\n    height: 11px;\n    box-sizing: border-box;\n}\n\n#ra2web-root progress::-webkit-progress-bar {\n    background: transparent;\n}\n\n#ra2web-root progress::-webkit-progress-value {\n    background-blend-mode: multiply, screen, multiply;\n    background:\n        linear-gradient(90deg, #808080, #fff),\n        linear-gradient(180deg, #606060 20%, #000 21%),\n        linear-gradient(180deg, #fff 79%, #ccc 80%),\n        linear-gradient(currentColor, currentColor);\n}\n\n#ra2web-root progress::-moz-progress-bar {\n    background-blend-mode: multiply, screen, multiply;\n    background:\n        linear-gradient(90deg, #808080, #fff),\n        linear-gradient(180deg, #606060 20%, #000 21%),\n        linear-gradient(180deg, #fff 79%, #ccc 80%),\n        linear-gradient(currentColor, currentColor);\n}\n\n#ra2web-root .loading-screen .country-name {\n    position: absolute;\n    bottom: 30px;\n    right: 80px;\n    font-weight: bold;\n}\n\n#ra2web-root .loading-screen .map-name {\n    position: absolute;\n    bottom: 30px;\n    left: 20px;\n    padding: 3px;\n    background: rgba(0, 0, 0, .5);\n    font-weight: bold;\n}\n\n#ra2web-root .loading-screen .player-team {\n    margin-right: 10px;\n    min-width: 40px;\n    color: white;\n}\n\n#ra2web-root .loading-screen .player-name {\n    margin-left: 10px;\n    font-weight: bold;\n}\n\n#ra2web-root .loading-screen .player-country-icon {\n    margin-left: 10px;\n}\n\n#ra2web-root .opts {\n    padding: 86px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .opts .slider-item,\n#ra2web-root .opts .item {\n    margin: 15px 0;\n}\n#ra2web-root .opts .item span.label {\n    width: 150px;\n    display: inline-block;\n    vertical-align: middle;\n}\n#ra2web-root .opts .slider-item {\n    text-align: center;\n}\n#ra2web-root .opts .item span.info {\n    margin-left: 5px;\n    vertical-align: middle;\n}\n\n#ra2web-root .opts .slider-item span.label,\n#ra2web-root .opts .item span.label {\n    width: 150px;\n    text-align: right;\n    margin-right: 10px;\n}\n\n#ra2web-root .opts.sound-opts input[type=\"range\"] {\n    width: 200px;\n}\n\n#ra2web-root .opts.general-opts {\n    padding: 34px 86px;\n}\n\n#ra2web-root .opts.general-opts .slider-item {\n    text-align: left;\n}\n\n#ra2web-root .opts.general-opts .slider-item span {\n    width: 150px;\n}\n\n#ra2web-root .opts.general-opts input[type=\"range\"] {\n    width: 150px;\n}\n\n#ra2web-root .opts.general-opts fieldset + fieldset {\n    margin-top: 20px;\n}\n\n#ra2web-root .opts.general-opts .select {\n    min-width: 100px;\n}\n\n#ra2web-root .opts.general-opts .resolution-select .select {\n    min-width: 150px;\n}\n\n#ra2web-root .opts.general-opts .fullscreen-toggle-button {\n    min-width: 100px;\n}\n\n#ra2web-root[data-mobile-layout=\"true\"] .opts.general-opts {\n    width: 100%;\n    max-width: 440px;\n    padding: 24px 48px;\n}\n\n#ra2web-root[data-compact-layout=\"true\"] .opts.general-opts {\n    padding: 16px 72px;\n}\n\n#ra2web-root[data-compact-layout=\"true\"] .opts.general-opts fieldset + fieldset {\n    margin-top: 10px;\n}\n\n#ra2web-root[data-compact-layout=\"true\"] .opts.general-opts .slider-item,\n#ra2web-root[data-compact-layout=\"true\"] .opts.general-opts .item {\n    margin: 8px 0;\n}\n\n#ra2web-root[data-compact-layout=\"true\"] .opts.general-opts .slider-item span.label,\n#ra2web-root[data-compact-layout=\"true\"] .opts.general-opts .item span.label {\n    width: 140px;\n}\n\n#ra2web-root[data-compact-layout=\"true\"] .opts.general-opts .select {\n    min-width: 96px;\n}\n\n#ra2web-root[data-compact-layout=\"true\"] .opts.general-opts .resolution-select .select,\n#ra2web-root[data-compact-layout=\"true\"] .opts.general-opts .fullscreen-toggle-button {\n    min-width: 128px;\n}\n\n#ra2web-root[data-mobile-layout=\"true\"] .opts .item span.label,\n#ra2web-root[data-mobile-layout=\"true\"] .opts .slider-item span.label {\n    width: 135px;\n}\n\n#ra2web-root .opts.sound-opts {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    height: 100%;\n    padding: 32px 0;\n    margin: 0 auto;\n    width: 410px;\n}\n\n#ra2web-root .opts.sound-opts .music-jukebox {\n    margin-top: 16px;\n    display: flex;\n    flex-direction: column;\n    min-height: 0;\n}\n\n#ra2web-root .opts.sound-opts .music-jukebox .jukebox-content {\n    display: flex;\n    justify-content: center;\n    align-items: end;\n    min-height: 0;\n}\n\n#ra2web-root .opts.sound-opts .music-jukebox .jukebox-content .controls {\n    width: 135px;\n    margin-left: 50px;\n}\n\n#ra2web-root .opts.sound-opts .music-jukebox .jukebox-content .controls > div {\n    margin-top: 16px;\n}\n\n#ra2web-root .opts.sound-opts .music-jukebox .jukebox-content .playlist {\n    width: 220px;\n    height: 100%;\n    max-height: 200px;\n    min-height: 0;\n}\n\n#ra2web-root .opts.sound-opts .music-jukebox .jukebox-footer {\n    margin-top: 16px;\n    display: flex;\n    justify-content: space-between;\n    margin-left: 50px;\n}\n\n#ra2web-root .key-opts {\n    margin: 0 auto;\n    position: relative;\n    top: 50%;\n    transform: translateY(-50%);\n}\n\n#ra2web-root .key-opts .key-opts-list,\n#ra2web-root .key-opts .key-opts-cur-assign,\n#ra2web-root .key-opts .key-opts-ch-assign {\n    display: flex;\n}\n\n#ra2web-root .key-opts .key-opts-list {\n    height: 200px;\n    margin-bottom: 15px;\n}\n\n#ra2web-root .key-opts .key-opts-left,\n#ra2web-root .key-opts .key-opts-right {\n    margin: 0 20px;\n    width: 50%;\n}\n\n#ra2web-root .key-opts .key-opts-list .key-opts-left {\n    display: flex;\n    flex-direction: column;\n}\n\n#ra2web-root .key-opts .key-opts-list .key-opts-right {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n}\n\n#ra2web-root .key-opts .key-opts-list .key-opts-right .key-opts-desc-container {\n    height: 100px;\n    margin-bottom: 50px;\n}\n\n#ra2web-root .key-opts .key-opts-list .key-opts-right .key-opts-cur-assign {\n    height: 30px;\n    margin-top: 15px;\n}\n\n#ra2web-root .key-opts .key-opts-cur-assign {\n    margin-bottom: 15px;\n}\n\n#ra2web-root .key-opts .key-opts-cur-assign .key-opts-cur-assign-value {\n    min-height: 15px;\n}\n\n#ra2web-root .key-opts .key-opts-ch-assign input {\n    margin: 5px 0;\n    width: 100%;\n}\n\n#ra2web-root .key-opts .key-opts-ch-assign .key-opts-right {\n    margin-top: 10px;\n}\n\n#ra2web-root .key-opts .key-opts-ch-assign-warn {\n    margin: 15px 20px 0 20px;\n}\n\n#ra2web-root fieldset {\n    border: 1px red solid;\n}\n\n#ra2web-root .replay-sel-form {\n    padding: 46px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .replay-sel-form .replay-list {\n    height: 300px;\n}\n\n#ra2web-root .replay-sel-form .replay-list .replay-name {\n    width: 70%;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n    margin-right: 5px;\n}\n\n#ra2web-root .replay-sel-form .replay-details {\n    border: 1px red solid;\n    padding: 3px;\n    margin-top: 16px;\n}\n\n#ra2web-root .replay-sel-form .storage-warning {\n    margin-top: 16px;\n    color: orange;\n}\n\n#ra2web-root .keep-replay-box input[type=\"text\"] {\n    width: 100%;\n    margin-top: 8px;\n}\n\n#ra2web-root .mod-sel-form {\n    padding: 46px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .mod-sel-form .mod-list {\n    height: 200px;\n}\n\n#ra2web-root .mod-sel-form .mod-list .mod-name {\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n}\n\n#ra2web-root .mod-sel-form .mod-details {\n    border: 1px red solid;\n    padding: 3px;\n    margin-top: 16px;\n}\n\n#ra2web-root .mod-sel-form .mod-details .mod-desc {\n    overflow: auto;\n    display: block;\n    max-height: 70px;\n}\n\n#ra2web-root .diplo-form {\n    padding: 34px 86px;\n}\n\n#ra2web-root .con-info-form {\n    padding: 86px 86px;\n}\n\n#ra2web-root .diplo-form,\n#ra2web-root .con-info-form {\n    box-sizing: border-box;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n}\n\n#ra2web-root .diplo-form .players {\n    overflow: auto;\n    min-height: 240px;\n}\n\n#ra2web-root .diplo-form table {\n    margin: 0 auto;\n}\n\n#ra2web-root .diplo-form thead {\n    color: yellow;\n    text-align: left;\n}\n\n#ra2web-root .diplo-form th {\n    font-weight: 500;\n}\n\n#ra2web-root .diplo-form th,\n#ra2web-root .diplo-form td {\n    width: 70px;\n    padding: 5px 0;\n}\n\n#ra2web-root .diplo-form th.player-country,\n#ra2web-root .diplo-form td.player-country {\n    width: 55px;\n}\n\n#ra2web-root .diplo-form th.player-ping,\n#ra2web-root .diplo-form td.player-ping {\n    width: 20px;\n    min-width: 20px;\n}\n\n#ra2web-root .diplo-form th.player-name,\n#ra2web-root .diplo-form td.player-name {\n    width: 200px;\n}\n\n#ra2web-root .diplo-form input[type=\"checkbox\"]:disabled {\n    opacity: 0.5;\n}\n\n#ra2web-root .diplo-form-footer,\n#ra2web-root .con-info-form-footer {\n    margin: 0 auto;\n    width: 100%;\n    max-width: 500px;\n}\n\n#ra2web-root .diplo-form-footer > *,\n#ra2web-root .con-info-form-footer > * {\n    margin-top: 10px;\n}\n\n#ra2web-root .diplo-form-footer > * + *,\n#ra2web-root .con-info-form-footer > * + * {\n    margin-top: 20px;\n}\n\n#ra2web-root .con-info-form .con-info-form-content {\n    flex-grow: 1;\n}\n\n#ra2web-root .con-info-form table {\n    margin: 0 auto;\n}\n\n#ra2web-root .con-info-form th.player-name {\n    text-align: left;\n}\n\n#ra2web-root .con-info-form td {\n    width: 70px;\n    padding: 3px 0;\n}\n\n#ra2web-root .con-info-form th.player-name,\n#ra2web-root .con-info-form td.player-name {\n    width: 150px;\n}\n\n#ra2web-root .con-info-form th.player-ping,\n#ra2web-root .con-info-form td.player-ping {\n    width: 250px;\n}\n\n#ra2web-root .con-info-form th.player-time,\n#ra2web-root .con-info-form td.player-time {\n    width: 50px;\n    text-align: right;\n}\n\n#ra2web-root .con-info-form td.player-ping meter {\n    width: 100%;\n    height: 24px;\n\n    /* Needed for Firefox */\n    background: transparent;\n    border: 1px red solid;\n}\n\n#ra2web-root .player-ping meter::-webkit-meter-bar {\n    background: transparent;\n    border-radius: 0;\n    border: 1px red solid;\n    padding: 1px;\n    height: 24px;\n}\n#ra2web-root .player-ping meter::-moz-meter-bar {\n    background: transparent;\n    border-radius: 0;\n    margin: 1px;\n    height: 22px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .player-ping meter::-webkit-meter-optimum-value {\n    background: limegreen;\n}\n#ra2web-root .player-ping meter:-moz-meter-optimum::-moz-meter-bar {\n    background: limegreen;\n}\n\n#ra2web-root .player-ping meter::-webkit-meter-suboptimum-value {\n    background: orange;\n}\n#ra2web-root .player-ping meter:-moz-meter-sub-optimum::-moz-meter-bar {\n    background: orange;\n}\n\n#ra2web-root .player-ping meter::-webkit-meter-even-less-good-value {\n    background: red;\n}\n#ra2web-root .player-ping meter:-moz-meter-sub-sub-optimum::-moz-meter-bar {\n    background: red;\n}\n\n#ra2web-root .prefetch-progress {\n    display: flex;\n    justify-content: center;\n    pointer-events: none;\n    color: red;\n}\n\n#ra2web-root .prefetch-progress > div {\n    padding: 0 5px;\n    background: rgba(0, 0, 0, .5);\n}\n\n#ra2web-root .prefetch-progress label {\n    margin-right: 10px;\n}\n\n#ra2web-root .prefetch-progress label,\n#ra2web-root .prefetch-progress progress {\n    vertical-align: middle;\n}\n\n#ra2web-root .game-res-box.message-box,\n#ra2web-root .patch-notes-box.message-box,\n#ra2web-root .basic-error-box.message-box {\n    background: rgba(255, 255, 255, .1);\n    border: 2px #8d8d8d outset;\n}\n\n#ra2web-root .game-res-box.message-box,\n#ra2web-root .patch-notes-box.message-box {\n    width: 640px;\n    height: 520px;\n    text-align: center;\n    /* Leave space for the disclaimer on low res */\n    margin-top: -30px;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content {\n    padding: 30px;\n    padding-top: 5px;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .title {\n    font-size: 15px;\n    text-align: center;\n    margin: 8px 0 15px 0;\n}\n\n#ra2web-root .game-res-box.message-box .close-button {\n    background-image: url(\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjIgKDY3MTQ1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5iYXNlbGluZS1jbG9zZS0yNHB4PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGcgaWQ9InJlQ3JlYXRlIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8ZyBpZD0idHhWaWV3X2dyYXBocyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTExMzIuMDAwMDAwLCAtMTk4LjAwMDAwMCkiIGZpbGw9InllbGxvdyIgZmlsbC1ydWxlPSJub256ZXJvIj4KICAgICAgICAgICAgPGcgaWQ9Ikdyb3VwLTE0IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzMzAuMDAwMDAwLCAxNzYuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICA8ZyBpZD0iR3JvdXAtMTAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyLjAwMDAwMCwgMTYuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgPGcgaWQ9ImJhc2VsaW5lLWNsb3NlLTI0cHgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDc4MC4wMDAwMDAsIDYuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxwb2x5Z29uIGlkPSJTaGFwZSIgcG9pbnRzPSIxOSA2LjQxIDE3LjU5IDUgMTIgMTAuNTkgNi40MSA1IDUgNi40MSAxMC41OSAxMiA1IDE3LjU5IDYuNDEgMTkgMTIgMTMuNDEgMTcuNTkgMTkgMTkgMTcuNTkgMTMuNDEgMTIiPjwvcG9seWdvbj4KICAgICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==\");\n    width: 24px;\n    height: 24px;\n    position: absolute;\n    top: 8px;\n    right: 8px;\n    cursor: pointer;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .drop-container {\n    border: 2px dashed #353535;\n    background-repeat: no-repeat;\n    background-image: url(res/img/download-arrow.png);\n    background-position: 50% 170px;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .drop-container.dropzone-active {\n    background: rgba(255, 255, 255, .2);\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .drop-container .drop-figures {\n    color: white;\n    font-size: 16px;\n    height: 153px;\n    margin-bottom: 40px;\n    pointer-events: none;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .drop-container .drop-figures * {\n    vertical-align: middle;\n    margin: 0 10px 0 15px;\n    display: inline-block;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .drop-container .desc {\n    color: #777;\n    font-size: 14px;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .browse-buttons {\n    text-align: center;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .browse-buttons .dialog-button:first-child {\n    margin-right: 10px;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .dialog-button,\n#ra2web-root .basic-error-box.message-box .message-box-footer .dialog-button {\n    display: inline-block;\n    margin: 0;\n    background: darkred;\n    border: 1px red outset;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .dialog-button:hover:not(:disabled),\n#ra2web-root .basic-error-box.message-box .message-box-footer .dialog-button:hover:not(:disabled) {\n    background: orangered;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .link-container .link-field {\n    display: flex;\n    align-items: baseline;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .link-container .link-field input {\n    margin-left: 8px;\n    flex-grow: 1;\n    display: block;\n    background: rgba(255, 255, 255, .1);\n    border-color: #ccc;\n    border-style: inset;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .link-container .download-button {\n    text-align: center;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content .browse-container .archive-formats {\n    text-align: center;\n}\n\n#ra2web-root .game-res-box.message-box .message-box-content em {\n    font-size: 11px;\n}\n\n#ra2web-root .patch-notes-box.message-box .message-box-content {\n    height: calc(100% - 65px);\n    padding: 0;\n    border-bottom: 1px darkgray solid;\n}\n\n#ra2web-root .patch-notes-box.message-box .message-box-content iframe {\n    padding: 0;\n}\n\n#ra2web-root .score-wrapper {\n    margin: 86px;\n    padding: 8px;\n    box-sizing: border-box;\n    background-color: rgba(0, 0, 0, .65);\n}\n\n#ra2web-root .score-wrapper .score-title {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    height: 19px;\n    padding-bottom: 8px;\n    margin-bottom: 8px;\n    border-bottom: 1px red solid;\n}\n\n#ra2web-root .score-wrapper .score-title .game-result {\n    font-size: 16px;\n    text-transform: uppercase;\n}\n\n#ra2web-root .score-wrapper .score-title .points-gain-value {\n    color: orangered;\n}\n\n#ra2web-root .score-wrapper .score-title .points-gain-value.positive {\n    color: lime;\n}\n\n#ra2web-root .score-wrapper .score-header {\n    display: flex;\n    justify-content: space-between;\n    margin-bottom: 16px;\n}\n\n#ra2web-root .score-wrapper table {\n    margin: 0 auto;\n}\n\n#ra2web-root .score-wrapper thead {\n    color: yellow;\n    text-align: left;\n}\n\n#ra2web-root .score-wrapper th {\n    font-weight: 500;\n}\n\n#ra2web-root .score-wrapper td {\n    text-shadow: 1px 1px black;\n}\n\n#ra2web-root .score-wrapper th,\n#ra2web-root .score-wrapper td {\n    width: 70px;\n    padding: 5px 0;\n}\n\n#ra2web-root .score-wrapper .number {\n    text-align: right;\n}\n\n#ra2web-root .score-wrapper th.player-rank,\n#ra2web-root .score-wrapper td.player-rank {\n    width: 20px;\n    min-width: 20px;\n}\n\n#ra2web-root .score-wrapper th.player-name,\n#ra2web-root .score-wrapper td.player-name {\n    width: 200px;\n}\n\n#ra2web-root .score-wrapper th.player-mmr,\n#ra2web-root .score-wrapper td.player-mmr {\n    width: 100px;\n}\n\n#ra2web-root .score-wrapper td.player-mmr .mmr-gain.positive {\n    color: lime;\n}\n\n#ra2web-root .score-wrapper td.player-mmr .mmr-gain {\n    color: orangered;\n}\n\n#ra2web-root .patch-notes,\n#ra2web-root .ladder-rules {\n    padding: 5px;\n    box-sizing: border-box;\n    border: 0;\n    height: 100%;\n    width: 100%;\n}\n\n#ra2web-root .credits-container {\n    overflow-y: auto;\n    overflow-x: hidden;\n    margin: 7px;\n    height: calc(100% - 11px);\n    width: calc(100% - 12px);\n}\n\n#ra2web-root .credits-container .credits {\n    padding: 32px;\n    text-align: center;\n}\n\n#ra2web-root .credits-container .credits .def {\n    display: flex;\n    margin: 0 auto;\n    width: 75%;\n}\n\n#ra2web-root .credits-container .credits .def .filler {\n    flex-grow: 1;\n}\n\n#ra2web-root .storage-explorer {\n    width: 100%;\n    height: 100%;\n    box-sizing: border-box;\n    padding: 8px;\n}\n\n#ra2web-root .storage-explorer .fe_fileexplorer_wrap {\n\tfont-size: 16px !important;\n\tcolor: #000 !important;\n}\n\n#ra2web-root .storage-explorer .fe_fileexplorer_wrap .fe_fileexplorer_path_segments_scroll_wrap::-webkit-scrollbar,\n#ra2web-root .storage-explorer .fe_fileexplorer_wrap .fe_fileexplorer_folder_tools_scroll_wrap::-webkit-scrollbar,\n#ra2web-root .storage-explorer .fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay::-webkit-scrollbar,\n#ra2web-root .storage-explorer .fe_fileexplorer_wrap .fe_fileexplorer_textarea::-webkit-scrollbar {\n\tborder: 0 !important;\n\twidth: 0 !important;\n\theight: 0 !important;\n}\n\n#ra2web-root .storage-explorer .fe_fileexplorer_wrap button,\n#ra2web-root .storage-explorer .fe_fileexplorer_wrap input,\n#ra2web-root .storage-explorer .fe_fileexplorer_wrap select {\n\tcolor: inherit !important;\n}\n\n#ra2web-root .storage-explorer .fe_fileexplorer_wrap .fe_fileexplorer_popup_wrap {\n\tcolor: inherit !important;\n}\n\n#ra2web-root .storage-explorer .fe_fileexplorer_wrap .fe_fileexplorer_items_wrap {\n\tpadding-right: 2px;\n}\n\n#ra2web-root .qm-form {\n    padding: 16px;\n    width: 100%;\n    height: 100%;\n    box-sizing: border-box;\n    display: flex;\n    flex-direction: column;\n}\n\n#ra2web-root .qm-form .qm-top {\n    min-height: 275px;\n    display: flex;\n    justify-content: space-between;\n}\n\n#ra2web-root .qm-form .opts {\n    flex-grow: 1;\n    margin-top: 9px;\n    margin-right: 10px;\n    border: 1px red solid;\n    padding: 20px 0 0 0;\n}\n\n#ra2web-root .qm-form .opts .item {\n    margin: 0;\n}\n\n#ra2web-root .qm-form .opts .item + .item {\n    margin-top: 15px;\n}\n\n#ra2web-root .qm-form .opts .item span.label {\n    width: 140px;\n}\n\n#ra2web-root .qm-form .item.qm-game-type-item {\n    margin-top: 5px;\n}\n\n#ra2web-root .qm-form .qm-game-type {\n    display: inline-block;\n    vertical-align: top;\n    margin-top: -5px;\n}\n\n#ra2web-root .qm-form .qm-game-type .button-select {\n    display: block;\n}\n\n#ra2web-root .qm-form .qm-game-type > .button-select + .button-select {\n    margin-top: 8px;\n}\n\n#ra2web-root .button-select {\n    display: inline-block;\n    vertical-align: middle;\n}\n\n#ra2web-root .button-select div {\n    display: inline-block;\n}\n\n#ra2web-root .button-select .option {\n    border: 1px red solid;\n    font-size: 14px;\n    padding: 4px 8px 3px 8px;\n    background: rgba(0, 0, 0, .75);\n}\n\n#ra2web-root .button-select .option:hover,\n#ra2web-root .button-select .option.selected {\n    border-color: orange;\n}\n\n#ra2web-root .button-select .option.disabled {\n    color: gray;\n    border-color: gray;\n}\n\n#ra2web-root .button-select > div + div {\n    margin-left: 8px;\n}\n\n#ra2web-root .qm-form .country-select {\n    display: inline-flex;\n    flex-direction: row-reverse;\n}\n\n#ra2web-root .qm-form fieldset.qm-profile {\n    width: 170px;\n    margin-inline: 0;\n}\n\n#ra2web-root .qm-form .qm-profile legend {\n    font-size: 16px;\n}\n\n#ra2web-root .qm-form .qm-profile .placement {\n    position: relative;\n    top: 50%;\n    transform: translateY(-50%);\n    margin: 0;\n    margin-top: 0 !important;\n    text-align: center;\n}\n\n#ra2web-root .qm-form .qm-profile .player-rank {\n    margin-bottom: 16px;\n}\n\n#ra2web-root .qm-form .qm-profile .player-rank .rank-indicator {\n    vertical-align: middle;\n    margin-left: 0;\n}\n\n#ra2web-root .qm-form .qm-profile .player-rank .rank-name {\n    font-size: 16px;\n}\n\n#ra2web-root .qm-form .qm-profile .player-rank .rank-number {\n    margin-left: 20px;\n    margin-top: 3px;\n    color: cyan;\n}\n\n#ra2web-root .qm-form .qm-profile * + .item {\n    margin-top: 15px;\n}\n\n/* Mobile touch controls - hidden on desktop */\n.mobile-touch-controls {\n    display: none;\n}\n\n#ra2web-root[data-mobile-layout=\"true\"] .mobile-touch-controls {\n    display: flex;\n    position: absolute;\n    bottom: 20px;\n    left: 50%;\n    transform: translateX(-50%);\n    gap: 24px;\n    z-index: 1000;\n    pointer-events: auto;\n}\n\n.mobile-touch-btn {\n    width: 56px;\n    height: 56px;\n    border-radius: 50%;\n    border: 2px solid rgba(255, 255, 255, 0.4);\n    background: rgba(0, 0, 0, 0.2);\n    color: rgba(255, 255, 255, 0.5);\n    font-size: 20px;\n    font-weight: bold;\n    font-family: inherit;\n    cursor: pointer;\n    touch-action: manipulation;\n    user-select: none;\n    -webkit-user-select: none;\n    transition: background 0.15s, border-color 0.15s, color 0.15s;\n}\n\n.mobile-touch-btn.active {\n    background: rgba(255, 255, 255, 0.25);\n    border-color: rgba(255, 255, 255, 0.8);\n    color: rgba(255, 255, 255, 0.9);\n}\n\n#ra2web-root .qm-form .qm-profile .value {\n    float: right;\n}\n\n#ra2web-root .qm-form .qm-profile .rank-indicator {\n    display: inline-block;\n    vertical-align: top;\n    margin-left: 3px;\n}\n\n#ra2web-root .qm-form .qm-profile .promo-progress span.label {\n    display: block;\n}\n\n#ra2web-root .qm-form .qm-profile .promo-progress .value {\n    display: block;\n    width: 100%;\n    text-align: left;\n    margin-top: 8px;\n    float: none;\n}\n\n#ra2web-root .qm-form .qm-profile .promo-progress.demotion .value {\n    color: orangered;\n}\n\n#ra2web-root .qm-form .qm-profile .promo-progress .next-rank .promotion-indicator {\n    color: lime;\n\n}\n\n#ra2web-root .qm-form .qm-profile .promo-progress .next-rank .demotion-indicator {\n    text-shadow: 0px 0px 5px orange;\n}\n\n#ra2web-root .qm-form .qm-profile .promo-progress progress {\n    width: 100%;\n}\n\n#ra2web-root .qm-form .qm-profile hr {\n    border-top: 1px solid;\n    border-bottom: 0;\n    border-color: dimgray;\n    margin: 15px 0 8px 0;\n}\n\n#ra2web-root .qm-form .qm-profile .info {\n    vertical-align: top;\n    margin-left: 3px;\n}\n\n#ra2web-root .qm-form .qm-bottom {\n    margin-top: 15px;\n    display: flex;\n    flex-direction: row;\n    overflow-y: auto;\n    flex: 1;\n}\n\n#ra2web-root .qm-form .qm-bottom .chat-wrapper {\n    display: flex;\n    flex-direction: column;\n    flex-grow: 1;\n    flex-basis: min-content;\n}\n\n#ra2web-root .qm-form .qm-bottom .chat-wrapper .messages {\n    height: auto;\n    flex-grow: 1;\n}\n\n#ra2web-root .qm-form .qm-bottom .players-list {\n    height: auto;\n    width: 190px;\n    box-sizing: border-box;\n    margin-left: 10px;\n}\n\n#ra2web-root .ladder {\n    padding: 20px;\n}\n\n#ra2web-root .ladder .toolbar {\n    margin-bottom: 10px;\n    display: flex;\n    /* justify-content: center; */\n}\n\n#ra2web-root .ladder .ladder-content {\n    display: flex;\n    align-items: flex-start;\n    margin: 0 auto;\n}\n\n#ra2web-root .ladder .ladder-content .ladder-types {\n    margin-right: 10px;\n    width: 130px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .ladder .ladder-content .list.ladder-types {\n    border: 0px;\n}\n\n#ra2web-root .ladder .ladder-content .list.ladder-types .list-item {\n    border: 1px red solid;\n    padding: 4px 8px 3px 8px;\n}\n\n#ra2web-root .ladder .ladder-content .list.ladder-types .list-item:not(.selected) {\n    background-color: #480000;\n}\n\n#ra2web-root .ladder .ladder-content .list.ladder-types .list-item:hover {\n    background-color: red;\n}\n\n#ra2web-root .ladder .ladder-content .list.ladder-types .list-item + .list-item {\n    margin-top: 8px;\n}\n\n#ra2web-root .ladder .ladder-content .season-info {\n    padding: 24px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .ladder .ladder-content .season-info .item {\n    margin-top: 16px;\n}\n\n#ra2web-root .ladder .ladder-content .season-info header + .item {\n    margin-top: 32px;\n}\n\n#ra2web-root .ladder .ladder-content .season-info h2 {\n    margin-top: 0;\n}\n\n#ra2web-root .ladder .season-info,\n#ra2web-root .ladder table {\n    border: 1px red solid;\n    width: 450px;\n}\n\n#ra2web-root .ladder table {\n    border-collapse: collapse;\n}\n\n#ra2web-root .ladder table th,\n#ra2web-root .ladder table td {\n    font-size: 13px;\n    padding: 3px 8px;\n}\n\n#ra2web-root .ladder table thead th {\n    border-bottom: 1px red solid;\n    text-align: left;\n}\n\n#ra2web-root .ladder table thead th.player-rank-icon {\n    width: 20px;\n}\n\n#ra2web-root .ladder table thead th.player-name {\n    width: 200px;\n}\n\n#ra2web-root .ladder .player-rank-icon .rank-indicator {\n    display: inline-block;\n    vertical-align: top;\n}\n\n#ra2web-root .ladder table tr:nth-child(2n) {\n    background-color: rgba(21, 21, 21, .75);\n}\n\n#ra2web-root .ladder table tr:nth-child(2n+1) {\n    background-color: rgba(55, 55, 55, .75);\n}\n\n#ra2web-root .ladder table tr.selected {\n    background-color: red;\n}\n\n#ra2web-root .ladder table tr.disabled {\n    color: gray;\n}\n\n#ra2web-root .ladder .pagination {\n    margin-top: 10px;\n    display: flex;\n    justify-content: center;\n}\n\n#ra2web-root .ladder button {\n    appearance: none;\n    border: 1px red solid;\n    background-color: #480000;\n    padding: 4px 8px 3px 8px;\n}\n\n#ra2web-root .ladder * + button {\n    margin-left: 5px;\n}\n\n#ra2web-root .ladder button:hover:not(:disabled) {\n    background-color: orangered;\n}\n\n#ra2web-root .ladder button:disabled {\n    color: gray;\n}\n\n#ra2web-root .ladder .toolbar.no-season-select {\n    padding-left: 140px;\n    min-height: 26px;\n}\n\n#ra2web-root .ladder .season-select,\n#ra2web-root .ladder .ladder-select {\n    width: 130px;\n    margin-right: 10px;\n}\n\n#ra2web-root .ladder .ladder-select {\n    width: auto;\n    min-width: 200px;\n}\n\n#ra2web-root .ladder .ladder-select .select-layer {\n    max-height: 320px;\n    overflow-y: auto;\n}\n\n#ra2web-root .ladder .player-search {\n    display: flex;\n}\n\n#ra2web-root .ladder .player-search .player {\n    margin-right: 5px;\n    width: 100px;\n}\n\n#ra2web-root .game-chat-input {\n    width: 400px;\n    display: flex;\n    align-items: center;\n    font-size: 13px;\n    padding: 0 4px;\n    background: rgba(0, 0, 0, .75)\n}\n\n#ra2web-root .game-chat-input input {\n    flex-grow: 1;\n    border: 0;\n    height: 20px;\n    font-size: 13px;\n}\n\n/* Bot Upload Dialog */\n#ra2web-root .bot-upload-dialog-overlay {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0, 0, 0, 0.7);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    z-index: 9999;\n}\n\n#ra2web-root .bot-upload-dialog {\n    background: #5a0000;\n    border: 2px solid #8B0000;\n    border-radius: 4px;\n    width: 460px;\n    max-height: 80vh;\n    display: flex;\n    flex-direction: column;\n    color: #FFD700;\n    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.7), 0 0 15px rgba(139, 0, 0, 0.5);\n}\n\n#ra2web-root .bot-upload-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 12px 16px;\n    border-bottom: 1px solid #8B0000;\n    background: #700000;\n}\n\n#ra2web-root .bot-upload-header h3 {\n    margin: 0;\n    font-size: 15px;\n    color: #ffd700;\n}\n\n#ra2web-root .bot-upload-close {\n    background: none;\n    border: none;\n    color: #FFD700;\n    font-size: 20px;\n    cursor: pointer;\n    padding: 0 4px;\n}\n\n#ra2web-root .bot-upload-close:hover {\n    color: #FFF8DC;\n}\n\n#ra2web-root .bot-upload-body {\n    padding: 16px;\n    overflow-y: auto;\n    flex: 1;\n}\n\n#ra2web-root .bot-upload-section {\n    margin-bottom: 16px;\n}\n\n#ra2web-root .bot-upload-section h4 {\n    margin: 0 0 8px;\n    font-size: 13px;\n    color: #FFD700;\n}\n\n#ra2web-root .bot-upload-label {\n    display: block;\n    margin-bottom: 6px;\n    font-size: 13px;\n    color: #FFD700;\n}\n\n#ra2web-root .bot-upload-input {\n    width: 100%;\n    font-size: 12px;\n    padding: 6px;\n    box-sizing: border-box;\n}\n\n#ra2web-root .bot-upload-hint {\n    margin-top: 4px;\n    font-size: 11px;\n    color: #D4A017;\n}\n\n#ra2web-root .bot-upload-status {\n    padding: 8px;\n    text-align: center;\n    color: #FFD700;\n    font-size: 13px;\n}\n\n#ra2web-root .bot-upload-message {\n    padding: 8px 12px;\n    border-radius: 3px;\n    margin-bottom: 12px;\n    font-size: 12px;\n    white-space: pre-wrap;\n}\n\n#ra2web-root .bot-upload-message-success {\n    background: rgba(255, 215, 0, 0.15);\n    border: 1px solid rgba(255, 215, 0, 0.3);\n    color: #FFD700;\n}\n\n#ra2web-root .bot-upload-message-error {\n    background: rgba(255, 0, 0, 0.2);\n    border: 1px solid rgba(255, 80, 80, 0.5);\n    color: #FF6B6B;\n}\n\n#ra2web-root .bot-upload-empty {\n    color: #D4A017;\n    font-size: 12px;\n    text-align: center;\n    padding: 12px;\n}\n\n#ra2web-root .bot-upload-list {\n    list-style: none;\n    margin: 0;\n    padding: 0;\n}\n\n#ra2web-root .bot-upload-item {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 6px 8px;\n    border-bottom: 1px solid #8B0000;\n    font-size: 12px;\n}\n\n#ra2web-root .bot-upload-item:last-child {\n    border-bottom: none;\n}\n\n#ra2web-root .bot-upload-item-info {\n    display: flex;\n    gap: 8px;\n    align-items: center;\n}\n\n#ra2web-root .bot-upload-item-name {\n    color: #FFD700;\n    font-weight: bold;\n}\n\n#ra2web-root .bot-upload-item-version {\n    color: #D4A017;\n}\n\n#ra2web-root .bot-upload-item-author {\n    color: #B8860B;\n}\n\n#ra2web-root .bot-upload-item-remove {\n    background: rgba(139, 0, 0, 0.5);\n    border: 1px solid #B22222;\n    color: #FFD700;\n    padding: 2px 8px;\n    font-size: 11px;\n    cursor: pointer;\n    border-radius: 2px;\n}\n\n#ra2web-root .bot-upload-item-remove:hover {\n    background: rgba(178, 34, 34, 0.7);\n}\n\n#ra2web-root .bot-upload-footer {\n    padding: 12px 16px;\n    border-top: 1px solid #8B0000;\n    background: #700000;\n    text-align: right;\n}\n\n#ra2web-root .bot-upload-btn {\n    background: rgba(139, 0, 0, 0.5);\n    border: 1px solid #B22222;\n    color: #FFD700;\n    padding: 4px 12px;\n    font-size: 12px;\n    cursor: pointer;\n    border-radius: 2px;\n    margin-left: 8px;\n}\n\n#ra2web-root .bot-upload-btn:hover {\n    background: rgba(178, 34, 34, 0.7);\n}\n"
  },
  {
    "path": "public/mods.ini",
    "content": "[General]\n1=athse\n2=gonghui\n3=meisuzhenba\n\n[gonghui]\nID=gonghui\nName=共和国之辉\nDescription=风靡全球的低质量MOD移植到网页平台，更多平衡性提高!\nAuthor=共和国之辉网\nVersion=Rev.2025.05.01.1\nWebsite=https://www.gongheguozhihui.com\nDownload=https://ra2webmod.k0s.cn/mod/gonghui/gonghui-05012025-1.zip\nDownloadSize=2028803\n\n[athse]\nID=athse\nName=Scorched Earth\nDescription=A mature overhaul of RA2, with an emphasis on the best combat experience.\nAuthor=G-E\nVersion=Rev.2023.06.11\nWebsite=https://www.moddb.com/mods/scorched-earth-ra2-mod-with-smart-ai\nDownload=athse/athse-snapshot-06112023.rar\nDownloadSize=112948223\n\n[meisuzhenba]\nID=meisuzhenba\nName=红色警戒2原版阵营补丁\nDescription=将所有的子阵营合并成两个，但是美国可以建造巨炮、黑鹰、狙击手、坦克杀手，苏联可以建造辐射工兵、自爆卡车、恐怖分子\nAuthor=QQ2174328393\nVersion=Rev.1\nWebsite=https://www.bilibili.com\nDownload=https://download.ra2web.com/meisuzhenba-v1.zip\nDownloadSize=97419"
  },
  {
    "path": "public/other/file-explorer.css",
    "content": ".fe_fileexplorer_hidden { display: none !important; }\n.fe_fileexplorer_invisible { visibility: hidden; }\n.fe_fileexplorer_disabled { filter: grayscale(95%); opacity: 0.6; }\n\n.fe_fileexplorer_open_icon { background-image: url('fileexplorer_sprites.png'); width: 24px; height: 24px; background-position: -48px -96px; image-rendering: pixelated; }\n\n.fe_fileexplorer_wrap { position: relative; font-size: 1.0em; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; cursor: default; height: 100%; min-height: 9em; }\n.fe_fileexplorer_wrap .fe_fileexplorer_operation_in_progress { cursor: progress; }\n.fe_fileexplorer_wrap .fe_fileexplorer_operation_in_progress button { cursor: progress; }\n.fe_fileexplorer_wrap .fe_fileexplorer_dropzone_wrap { height: 100%; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap { border: 1px solid #AAAAAA; color: #000000; background-color: #FFFFFF; display: flex; flex-direction: column; height: 100%; box-sizing: border-box; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused { border: 1px solid #0063B1; }\n\n.fe_fileexplorer_wrap button::-moz-focus-inner { border: 0; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_toolbar { display: flex; margin-top: 0.4em; align-items: center; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtools { display: flex; margin-left: 5px; margin-right: 0.1em; }\n.fe_fileexplorer_wrap .fe_fileexplorer_navtools button { padding: 0; border: 0 none; box-sizing: border-box; height: 24px; background-color: transparent; outline: none; background-repeat: no-repeat; image-rendering: pixelated; }\n.fe_fileexplorer_wrap .fe_fileexplorer_navtools button.fe_fileexplorer_disabled { opacity: 0.4; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_back { background-image: url('fileexplorer_sprites.png'); width: 32px; background-position: -0px -0px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_forward { background-image: url('fileexplorer_sprites.png'); width: 32px; background-position: -64px -0px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_history { background-image: url('fileexplorer_sprites.png'); width: 18px; background-position: -84px -24px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_up { background-image: url('fileexplorer_sprites.png'); width: 24px; background-position: -0px -24px; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_back:not(.fe_fileexplorer_disabled):hover, .fe_fileexplorer_wrap .fe_fileexplorer_navtool_back:not(.fe_fileexplorer_disabled):focus { background-image: url('fileexplorer_sprites.png'); width: 32px; background-position: -32px -0px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_forward:not(.fe_fileexplorer_disabled):hover, .fe_fileexplorer_wrap .fe_fileexplorer_navtool_forward:not(.fe_fileexplorer_disabled):focus { background-image: url('fileexplorer_sprites.png'); width: 32px; background-position: -96px -0px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_history:not(.fe_fileexplorer_disabled):hover, .fe_fileexplorer_wrap .fe_fileexplorer_navtool_history:not(.fe_fileexplorer_disabled):focus { background-image: url('fileexplorer_sprites.png'); width: 18px; background-position: -102px -24px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_up:not(.fe_fileexplorer_disabled):hover, .fe_fileexplorer_wrap .fe_fileexplorer_navtool_up:not(.fe_fileexplorer_disabled):focus { background-image: url('fileexplorer_sprites.png'); width: 24px; background-position: -24px -24px; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_wrap { display: flex; flex: 1; align-items: center; overflow: hidden; border: 1px solid #D9D9D9; margin-right: 12px; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_icon { height: 24px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_icon_inner { background-image: url('fileexplorer_sprites.png'); width: 24px; height: 24px; margin-left: 2px; margin-right: 4px; background-position: -72px -96px; image-rendering: pixelated; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segments_scroll_wrap { flex: 1; position: relative; overflow-x: scroll; box-sizing: border-box; scrollbar-width: none; -ms-overflow-style: none; }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segments_scroll_wrap::-webkit-scrollbar { height: 0px; background: transparent; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segments_wrap { display: flex; flex: 1; }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segments_wrap button { padding: 0.5em; border: 1px solid transparent; box-sizing: border-box; line-height: 1; background-color: transparent; outline: none; font-size: 0.75em; white-space: nowrap; }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segments_wrap::after { content: ''; padding-left: 10%; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap { display: flex; border: 1px solid transparent; outline: none; }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap::-moz-focus-inner { border: 0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap .fe_fileexplorer_path_opts { padding: 0; background-repeat: no-repeat; background-image: url('fileexplorer_sprites.png'); width: 18px; background-position: -48px -24px; image-rendering: pixelated; }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap:hover { border: 1px solid #CCE8FF; background-color: #E5F3FF; }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap:hover .fe_fileexplorer_path_opts { padding: 0; border-left: 1px solid #CCE8FF; }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap:focus, .fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap.fe_fileexplorer_path_segment_wrap_focus { border: 1px solid #99D1FF; background-color: #CCE8FF; }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap:focus .fe_fileexplorer_path_opts, .fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap.fe_fileexplorer_path_segment_wrap_focus .fe_fileexplorer_path_opts { border-left: 1px solid #99D1FF; }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap.fe_fileexplorer_path_segment_wrap_down .fe_fileexplorer_path_name { padding: calc(0.5em + 1px) calc(0.5em - 1px) calc(0.5em - 1px) calc(0.5em + 1px); }\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap.fe_fileexplorer_path_segment_wrap_down .fe_fileexplorer_path_opts { background-image: url('fileexplorer_sprites.png'); background-position: -84px -24px; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap.fe_fileexplorer_drag_hover { border: 1px solid #99D1FF; background-color: #CCE8FF; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_body_wrap_outer { flex: 1; display: flex; margin-top: 0.3em; overflow: hidden; position: relative; }\n.fe_fileexplorer_wrap .fe_fileexplorer_body_wrap { display: flex; align-items: stretch; overflow: hidden; min-height: 5em; width: 100%; height: 100%; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools_scroll_wrap { padding: 0.4em 10px; border-right: 1px solid #CCE8FF; overflow-y: scroll; box-sizing: border-box; scrollbar-width: none; -ms-overflow-style: none; position: relative; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools_scroll_wrap::-webkit-scrollbar { width: 0px; background: transparent; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools { display: flex; flex-direction: column; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools button { margin-bottom: 0.3em; border: 1px solid transparent; box-sizing: border-box; padding: 4px; width: 34px; height: 34px; background-color: transparent; outline: none; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools button::before { display: block; width: 24px; height: 24px; content: ''; background-repeat: no-repeat; image-rendering: pixelated; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools button:not(.fe_fileexplorer_disabled):hover, .fe_fileexplorer_wrap .fe_fileexplorer_folder_tools button:not(.fe_fileexplorer_disabled):focus { border: 1px solid #99D1FF; background-color: #E5F3FF; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_separator { margin: 0 -0.1em 0.3em; border-top: 1px solid #DFE7F0; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_new_folder::before { background-image: url('fileexplorer_sprites.png'); background-position: -24px -144px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_new_file::before { background-image: url('fileexplorer_sprites.png'); background-position: -0px -144px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_upload::before { background-image: url('fileexplorer_sprites.png'); background-position: -96px -144px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_download::before { background-image: url('fileexplorer_sprites.png'); background-position: -96px -120px; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_copy::before { background-image: url('fileexplorer_sprites.png'); background-position: -24px -120px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_paste::before { background-image: url('fileexplorer_sprites.png'); background-position: -48px -144px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_cut::before { background-image: url('fileexplorer_sprites.png'); background-position: -48px -120px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_delete::before { background-image: url('fileexplorer_sprites.png'); background-position: -72px -120px; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_item_checkboxes::before { background-image: url('fileexplorer_sprites.png'); background-position: -72px -144px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_show_item_checkboxes .fe_fileexplorer_folder_tool_item_checkboxes::before { background-image: url('fileexplorer_sprites.png'); background-position: -0px -120px; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_scroll_wrap { flex: 1; overflow-y: auto; box-sizing: border-box; outline: none; position: relative; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_scroll_wrap::-moz-focus-inner { border: 0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_scroll_wrap_inner { position: relative; min-height: 100%; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_message_wrap { padding: 1.5em 1em 1em 1em; color: #6D6D6D; font-size: 0.75em; text-align: center; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_wrap { display: flex; flex-wrap: wrap; padding: 0.3em 12px 0.2em 4px; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_wrap { margin-left: 0.56em; margin-bottom: 1px; width: 4.7em; box-sizing: border-box; text-align: center; overflow: hidden; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_wrap_inner { position: relative; border: 1px solid transparent; padding: 0.1em 0.3em; outline: none; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_wrap_inner::-moz-focus-inner { border: 0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_wrap_inner:hover { background-color: #E5F3FF; border-color: #E5F3FF; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap:not(.fe_fileexplorer_inner_wrap_focused) .fe_fileexplorer_item_selected .fe_fileexplorer_item_wrap_inner { background-color: #D9D9D9; border-color: #D9D9D9; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap:not(.fe_fileexplorer_inner_wrap_focused) .fe_fileexplorer_item_selected .fe_fileexplorer_item_wrap_inner:hover { background-color: #E5F3FF; border-color: #99D1FF; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap:not(.fe_fileexplorer_items_focus) .fe_fileexplorer_item_selected .fe_fileexplorer_item_wrap_inner { background-color: #D9D9D9; border-color: #D9D9D9; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap:not(.fe_fileexplorer_items_focus) .fe_fileexplorer_item_wrap_inner:hover { background-color: #E5F3FF; border-color: #99D1FF; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap.fe_fileexplorer_items_selecting .fe_fileexplorer_item_wrap_inner { background-color: transparent; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap.fe_fileexplorer_items_focus .fe_fileexplorer_item_selected .fe_fileexplorer_item_wrap_inner { background-color: #CDE8FF; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap.fe_fileexplorer_items_focus .fe_fileexplorer_item_selected .fe_fileexplorer_item_wrap_inner:hover { border-color: #99D1FF; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap.fe_fileexplorer_items_focus .fe_fileexplorer_item_selected.fe_fileexplorer_item_focused .fe_fileexplorer_item_wrap_inner { background-color: #CCE8FF; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap.fe_fileexplorer_items_focus .fe_fileexplorer_item_focused .fe_fileexplorer_item_wrap_inner { border-color: #99D1FF; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap .fe_fileexplorer_items_wrap .fe_fileexplorer_item_wrap.fe_fileexplorer_drag_hover .fe_fileexplorer_item_wrap_inner { background-color: #CDE8FF; }\n\n/*\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap.fe_fileexplorer_items_copy { cursor: copy; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap.fe_fileexplorer_items_move_copy .fe_fileexplorer_item_folder:not(.fe_fileexplorer_item_selected) .fe_fileexplorer_item_wrap_inner:hover { background-color: #CDE8FF; border-color: transparent; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap.fe_fileexplorer_items_move_copy .fe_fileexplorer_item_wrap:not(.fe_fileexplorer_item_folder):not(.fe_fileexplorer_item_selected) .fe_fileexplorer_item_wrap_inner { opacity: 0.7; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap.fe_fileexplorer_items_move_copy .fe_fileexplorer_item_wrap:not(.fe_fileexplorer_item_folder):not(.fe_fileexplorer_item_selected) .fe_fileexplorer_item_wrap_inner:hover { background-color: transparent; border-color: transparent; }\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap.fe_fileexplorer_items_move_copy .fe_fileexplorer_item_wrap.fe_fileexplorer_item_selected:not(.fe_fileexplorer_item_focused) .fe_fileexplorer_item_wrap_inner:hover { border-color: transparent; }\n*/\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_checkbox { position: absolute; left: 0; top: 0; margin: 2px; z-index: 1; display: none; padding: initial; border: initial; transform: none; }\n.fe_fileexplorer_wrap .fe_fileexplorer_show_item_checkboxes .fe_fileexplorer_item_wrap_inner:hover .fe_fileexplorer_item_checkbox { display: block; }\n.fe_fileexplorer_wrap .fe_fileexplorer_show_item_checkboxes .fe_fileexplorer_item_selected .fe_fileexplorer_item_checkbox { display: block; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon { width: 48px; height: 48px; margin-left: auto; margin-right: auto; background-repeat: no-repeat; position: relative; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_img { width: auto; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon img { position: absolute; bottom: 2px; left: 50%; transform: translateX(-50%); max-width: 100%; max-height: calc(100% - 2px); -webkit-box-shadow: 1px 1px 1px 0px rgba(0, 0, 0, 0.15); -moz-box-shadow: 1px 1px 1px 0px rgba(0, 0, 0, 0.15); box-shadow: 1px 1px 1px 0px rgba(0, 0, 0, 0.15); }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_text { margin-top: 0.1em; font-size: 0.75em; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; word-wrap: break-word; overflow: hidden; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_text.fe_fileexplorer_invisible { color: transparent; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_folder { background-image: url('fileexplorer_sprites.png'); background-position: -48px -48px; image-rendering: pixelated; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file { background-image: url('fileexplorer_sprites.png'); background-position: -0px -48px; image-rendering: pixelated; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext)::after { position: absolute; bottom: 10px; left: 0px; box-sizing: border-box; content: attr(data-ext); color: #FFFFFF; font-size: 11px; padding: 1px 3px; width: 36px; overflow: hidden; white-space: nowrap; background-color: #888888; text-transform: uppercase; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_a::after { background-color: #F03C3C; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_b::after { background-color: #F05A3C; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_c::after { background-color: #F0783C; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_d::after { background-color: #F0963C; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_e::after { background-color: #E0862B; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_f::after { background-color: #DCA12B; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_g::after { background-color: #C7AB1E; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_h::after { background-color: #C7C71E; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_i::after { background-color: #ABC71E; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_j::after { background-color: #8FC71E; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_k::after { background-color: #72C71E; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_l::after { background-color: #56C71E; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_m::after { background-color: #3AC71E; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_n::after { background-color: #1EC71E; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_o::after { background-color: #1EC73A; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_p::after { background-color: #1EC756; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_q::after { background-color: #1EC78F; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_r::after { background-color: #1EC7AB; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_s::after { background-color: #1EC7C7; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_t::after { background-color: #1EABC7; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_u::after { background-color: #1E8FC7; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_v::after { background-color: #1E72C7; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_w::after { background-color: #3C78F0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_x::after { background-color: #3C5AF0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_y::after { background-color: #3C3CF0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_z::after { background-color: #5A3CF0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_0::after { background-color: #783CF0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_1::after { background-color: #963CF0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_2::after { background-color: #B43CF0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_3::after { background-color: #D23CF0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_4::after { background-color: #F03CF0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_5::after { background-color: #F03CD2; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_6::after { background-color: #F03CB4; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_7::after { background-color: #F03C96; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_8::after { background-color: #F03C78; }\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext).fe_fileexplorer_item_icon_ext_9::after { background-color: #F03C5A; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_wrap { position: absolute; left: 53px; top: 0; width: calc(100% - 53px); height: 100%; pointer-events: none; z-index: 2; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_wrap.fe_fileexplorer_items_show_clipboard_overlay_paste { height: 200px; max-height: 75%; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_inner_wrap { position: relative; height: 100%; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_text_wrap { position: absolute; left: 0; top: 0; width: 100%; height: 100%; box-sizing: border-box; background-color: rgba(255, 255, 255, 0.95); border: 2px dashed #AAAAAA; -webkit-box-shadow: 2px 3px 5px 0px rgba(0, 0, 0, 0.15); -moz-box-shadow: 2px 3px 5px 0px rgba(0, 0, 0, 0.15); box-shadow: 2px 3px 5px 0px rgba(0, 0, 0, 0.15); }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_wrap:hover .fe_fileexplorer_items_clipboard_overlay_paste_text_wrap, .fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_wrap_focus .fe_fileexplorer_items_clipboard_overlay_paste_text_wrap { border-color: #3298FE; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_wrap:not(.fe_fileexplorer_items_show_clipboard_overlay_paste) .fe_fileexplorer_items_clipboard_overlay_paste_text_wrap { display: none; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_text { position: absolute; left: 50%; top: 50%; color: #888888; transform: translate(-50%, -50%); text-align: center; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_text_big { font-size: 2em; margin-bottom: 0.3em; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_text_small { font-size: 0.75em; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay { position: absolute; left: 0; top: -10px; width: 100%; height: calc(100% + 10px); margin: 0; border: 0 none; padding: 0; color: transparent; background-color: transparent; text-shadow: 0px 0px 0px transparent; caret-color: transparent; cursor: default; resize: none; box-sizing: border-box; scrollbar-width: none; -ms-overflow-style: none; text-align: center; outline: none; font-size: 1px; line-height: 1; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay::-webkit-scrollbar { width: 0px; height: 0px; background: transparent; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_wrap.fe_fileexplorer_items_clipboard_contextmenu .fe_fileexplorer_items_clipboard_overlay { pointer-events: auto; }\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_wrap.fe_fileexplorer_items_show_clipboard_overlay_paste .fe_fileexplorer_items_clipboard_overlay { pointer-events: auto; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_select_box { position: absolute; box-sizing: border-box; border: 1px solid #0078D7; background-color: rgba(0, 120, 215, 0.33); }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_wrap { display: flex; white-space: nowrap; font-size: 0.75em; color: #14273E; }\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_wrap.fe_fileexplorer_statusbar_wrap_multiline { display: block; }\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_text_wrap { display: flex; margin-left: 15px; margin-right: 12px; padding-top: 0.3em; padding-bottom: 0.3em; overflow: hidden; flex: 1; line-height: 1.1; }\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_text_segment_wrap { padding-right: 1em; border-right: 1px solid #F0F0F0; margin-right: 1em; }\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_text_segment_wrap_last { padding-right: 0; border-right: 0 none; margin-right: 0; overflow: hidden; text-overflow: ellipsis; }\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_measure_em_size { display: inline-block; position: fixed; left: -9999px; width: 1em; height: 1em; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_action_wrap { display: flex; }\n.fe_fileexplorer_wrap .fe_fileexplorer_action_progress_wrap { display: flex; padding-top: 0.3em; padding-bottom: 0.3em; overflow: hidden; }\n.fe_fileexplorer_wrap .fe_fileexplorer_action_progress_msg_wrap { margin-left: 1em; border-left: 1px solid #F0F0F0; padding-left: 1em; overflow: hidden; text-overflow: ellipsis; line-height: 1.1; }\n.fe_fileexplorer_wrap .fe_fileexplorer_action_progress_msg_wrap_last { margin-right: 0.4em; }\n.fe_fileexplorer_wrap .fe_fileexplorer_action_progress_cancel_wrap { padding-left: 0.6em; padding-right: 0.6em; line-height: 1.1; }\n.fe_fileexplorer_wrap .fe_fileexplorer_action_progress_cancel_wrap::after { content: '\\00D7'; font-weight: bold; outline: none; }\n.fe_fileexplorer_wrap .fe_fileexplorer_action_progress_cancel_wrap:hover::after, .fe_fileexplorer_wrap .fe_fileexplorer_action_progress_cancel_wrap:focus::after { color: #E81123; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_wrap.fe_fileexplorer_statusbar_wrap_multiline .fe_fileexplorer_action_progress_wrap { width: 100%; }\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_wrap.fe_fileexplorer_statusbar_wrap_multiline .fe_fileexplorer_action_progress_msg_wrap:first-child { margin-left: 15px; border-left: 0 none; padding-left: 0; }\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_wrap.fe_fileexplorer_statusbar_wrap_multiline .fe_fileexplorer_action_progress_msg_wrap_last { flex-grow: 1; }\n\n@font-face { font-family: 'fe_fileexplorer_actions'; src: url('fileexplorer_actions.woff?20200530-01') format('woff'); font-weight: normal; font-style: normal; font-display: block; }\n\n.fe_fileexplorer_wrap .fe_fileexplorer_action_progress_msg_icon { font-family: 'fe_fileexplorer_actions' !important; speak: none; font-weight: normal; font-variant: normal; text-transform: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; line-height: 1; }\n.fe_fileexplorer_wrap .fe_fileexplorer_action_progress_msg_icon.fe_fileexplorer_action_progress_msg_icon_uploads_in_progress::before { content: '\\E900'; }\n.fe_fileexplorer_wrap .fe_fileexplorer_action_progress_msg_icon.fe_fileexplorer_action_progress_msg_icon_queued::before { content: '\\E901'; }\n.fe_fileexplorer_wrap .fe_fileexplorer_action_progress_msg_icon.fe_fileexplorer_action_progress_msg_icon_done::before { content: '\\E902'; }\n.fe_fileexplorer_wrap .fe_fileexplorer_action_progress_msg_icon.fe_fileexplorer_action_progress_msg_icon_errors::before { content: '\\E903'; }\n\n.fe_fileexplorer_popup_wrap { position: absolute; left: -9999px; max-height: 33vh; overflow: hidden; overflow-y: auto; border: 1px solid #A0A0A0; background-color: #F2F2F2; min-width: 11em; max-width: 17em; z-index: 100; -webkit-box-shadow: 5px 5px 4px -3px rgba(0, 0, 0, 0.57); -moz-box-shadow: 5px 5px 4px -3px rgba(0, 0, 0, 0.57); box-shadow: 5px 5px 4px -3px rgba(0, 0, 0, 0.57); font-size: 1.0em; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; cursor: default; outline: none; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_inner_wrap { position: relative; padding: 2px; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_split { margin-left: 34px; margin-top: 0.1em; border-top: 1px solid #D7D7D7; padding-top: 0.1em; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_wrap { display: flex; align-items: center; box-sizing: border-box; outline: none; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_wrap:focus { background-color: #C3DEF5; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon { height: 24px; image-rendering: pixelated; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_inner { width: 24px; height: 24px; margin-left: 5px; margin-right: 5px; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_text { overflow: hidden; text-overflow: ellipsis; font-size: 0.75em; line-height: 1; white-space: nowrap; padding: 0.5em 0.3em; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_text.fe_fileexplorer_popup_item_active { font-weight: bold; }\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_wrap.fe_fileexplorer_popup_item_disabled:focus { background-color: #E5E5E5; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_wrap.fe_fileexplorer_popup_item_disabled .fe_fileexplorer_popup_item_text { color: #6D6D6D; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_wrap.fe_fileexplorer_popup_item_disabled .fe_fileexplorer_popup_item_icon_inner { filter: grayscale(95%); opacity: 0.9; }\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_wrap:focus .fe_fileexplorer_popup_item_icon_back { background-image: url('fileexplorer_sprites.png'); background-position: -96px -48px; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_wrap:focus .fe_fileexplorer_popup_item_icon_forward { background-image: url('fileexplorer_sprites.png'); background-position: -24px -96px; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_check { background-image: url('fileexplorer_sprites.png'); background-position: -0px -96px; }\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_folder { background-image: url('fileexplorer_sprites.png'); background-position: -96px -96px; }\n\n.fe_fileexplorer_textarea { position: absolute; resize: none; border: 1px solid #000000; padding: 1px; font-family: inherit; font-size: 0.75em; overflow-y: auto; box-sizing: border-box; scrollbar-width: none; -ms-overflow-style: none; text-align: center; outline: none; z-index: 2; }\n.fe_fileexplorer_textarea::-webkit-scrollbar { width: 0px; height: 0px; background: transparent; }\n.fe_fileexplorer_textarea[readonly] { color: #666666; }\n\n.fe_fileexplorer_floating_drag_icon_wrap { position: fixed; left: -9999px; padding: 1.5em; pointer-events: none; border: 1px solid rgba(151, 220, 252, 0.4); background-image: linear-gradient(rgba(227, 245, 252, 0.4), rgba(189, 231, 252, 0.4)); z-index: 100; -webkit-box-shadow: 5px 5px 4px -3px rgba(0, 0, 0, 0.3); -moz-box-shadow: 5px 5px 4px -3px rgba(0, 0, 0, 0.3); box-shadow: 5px 5px 4px -3px rgba(0, 0, 0, 0.3); user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; }\n.fe_fileexplorer_floating_drag_icon_wrap .fe_fileexplorer_floating_drag_icon_wrap_inner { position: relative; width: 48px; height: 48px; overflow: hidden; }\n.fe_fileexplorer_floating_drag_icon_wrap .fe_fileexplorer_item_icon { width: 48px; height: 48px; background-repeat: no-repeat; position: relative; opacity: 0.82; image-rendering: pixelated; }\n.fe_fileexplorer_floating_drag_icon_wrap .fe_fileexplorer_item_icon_folder { background-image: url('fileexplorer_sprites.png'); background-position: -48px -48px; image-rendering: pixelated; }\n.fe_fileexplorer_floating_drag_icon_wrap .fe_fileexplorer_item_icon_file { background-image: url('fileexplorer_sprites.png'); background-position: -0px -48px; image-rendering: pixelated; }\n.fe_fileexplorer_floating_drag_icon_wrap .fe_fileexplorer_floating_drag_icon_wrap_inner[data-numitems]::after { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -30%); padding: 0.1em 0.3em; font-size: 0.75em; background-color: #0074CC; border: 1px solid #FFFFFF; color: #FFFFFF; content: attr(data-numitems); }\n\n.fe_fileexplorer_download_iframe_wrap { position: fixed; left: -9999px; border: 0 none; width: 1px; height: 1px; }\n\n@media (pointer: coarse) {\n\t.fe_fileexplorer_wrap .fe_fileexplorer_item_wrap { margin-top: 0.1em; margin-bottom: 0.1em; }\n\t.fe_fileexplorer_wrap .fe_fileexplorer_show_item_checkboxes .fe_fileexplorer_item_checkbox { display: block; }\n\n\t.fe_fileexplorer_wrap .fe_fileexplorer_item_wrap_inner:hover { background-color: transparent; border-color: transparent; }\n\t.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap:not(.fe_fileexplorer_inner_wrap_focused) .fe_fileexplorer_item_selected .fe_fileexplorer_item_wrap_inner:hover { background-color: transparent; border-color: transparent; }\n\t.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused .fe_fileexplorer_items_wrap:not(.fe_fileexplorer_items_focus) .fe_fileexplorer_item_wrap_inner:hover { background-color: transparent; border-color: transparent; }\n}"
  },
  {
    "path": "public/res/fonts/fonts.css",
    "content": "/* Generated from https://fonts.googleapis.com/css2?family=Fira+Sans+Condensed:wght@500;600;700&display=swap */\r\n\r\n/* cyrillic-ext */\r\n@font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 500;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWQXOuMl0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\r\n  }\r\n  /* cyrillic */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 500;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWQXOuMB0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\r\n  }\r\n  /* greek-ext */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 500;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWQXOuMh0ciZb.woff2) format('woff2');\r\n    unicode-range: U+1F00-1FFF;\r\n  }\r\n  /* greek */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 500;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWQXOuMd0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0370-03FF;\r\n  }\r\n  /* vietnamese */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 500;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWQXOuMt0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\r\n  }\r\n  /* latin-ext */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 500;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWQXOuMp0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\r\n  }\r\n  /* latin */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 500;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWQXOuMR0cg.woff2) format('woff2');\r\n    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\r\n  }\r\n  /* cyrillic-ext */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 600;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWSnJuMl0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\r\n  }\r\n  /* cyrillic */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 600;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWSnJuMB0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\r\n  }\r\n  /* greek-ext */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 600;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWSnJuMh0ciZb.woff2) format('woff2');\r\n    unicode-range: U+1F00-1FFF;\r\n  }\r\n  /* greek */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 600;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWSnJuMd0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0370-03FF;\r\n  }\r\n  /* vietnamese */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 600;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWSnJuMt0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\r\n  }\r\n  /* latin-ext */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 600;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWSnJuMp0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\r\n  }\r\n  /* latin */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 600;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWSnJuMR0cg.woff2) format('woff2');\r\n    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\r\n  }\r\n  /* cyrillic-ext */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 700;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWU3IuMl0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\r\n  }\r\n  /* cyrillic */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 700;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWU3IuMB0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\r\n  }\r\n  /* greek-ext */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 700;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWU3IuMh0ciZb.woff2) format('woff2');\r\n    unicode-range: U+1F00-1FFF;\r\n  }\r\n  /* greek */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 700;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWU3IuMd0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0370-03FF;\r\n  }\r\n  /* vietnamese */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 700;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWU3IuMt0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\r\n  }\r\n  /* latin-ext */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 700;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWU3IuMp0ciZb.woff2) format('woff2');\r\n    unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\r\n  }\r\n  /* latin */\r\n  @font-face {\r\n    font-family: 'Fira Sans Condensed';\r\n    font-style: normal;\r\n    font-weight: 700;\r\n    font-display: swap;\r\n    src: url(wEOsEADFm8hSaQTFG18FErVhsC9x-tarWU3IuMR0cg.woff2) format('woff2');\r\n    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\r\n  }\r\n"
  },
  {
    "path": "public/res/locale/en-US.json",
    "content": "{\r\n    \"gui:ok\": \"OK\",\r\n    \"txt_copyright\": \"© 2000 ELECTRONIC ARTS INC. ALL RIGHTS RESERVED\",\r\n    \"gui:wwbrand\": \"WESTWOOD STUDIOS™ IS AN ELECTRONIC ARTS™ BRAND\",\r\n    \"gui:loadingex\": \"Loading...\",\r\n    \"ts:disclaimer\": \"DISCLAIMER:\\n\\\"Chrono Divide\\\" is a non-profit fan project and is in no way affiliated with Electronic Arts Inc.\\nNo copyright infringement is intended. All rights are held by their respective owners.\",\r\n\r\n    \"ts:downloading\": \"Downloading...\",\r\n    \"ts:downloadingpg\": \"Downloading... (%.1f%%)\",\r\n    \"ts:downloadingpgsize\": \"Downloading %.1f MiB / %.1f MiB... (%d%%)\",\r\n    \"ts:downloadingpgunkn\": \"Downloading %.1f MiB...\",\r\n    \"ts:downloadfailed\": \"A remote resource could not be downloaded.\",\r\n\r\n    \"ts:gameres_locate_title\": \"Locate original game assets\",\r\n    \"ts:gameres_import_desc\": \"If you have a copy of RA2 already installed, you can import it below. You can also choose to import an archive containing *.mix files from the local filesystem or a web URL.\",\r\n    \"ts:gameres_drop_desc\": \"Drop the required game files here\",\r\n    \"ts:gameres_or\": \"OR\",\r\n    \"ts:gameres_browse_folder\": \"Select folder...\",\r\n    \"ts:gameres_browse_archive\": \"Select archive...\",\r\n    \"ts:gameres_supported_archive_formats\": \"Supported archive formats: rar, tar, tar.gz, tar.bz2, tar.xz, zip, 7z, exe (sfx)\",\r\n    \"ts:gameres_download_url\": \"URL:\",\r\n    \"ts:gameres_invalid_url\": \"Please enter a valid URL.\",\r\n    \"ts:gameres_insecure_url\": \"You must enter a secure URL (HTTPS).\",\r\n    \"ts:gameres_download_button\": \"Download\",\r\n    \"ts:gameres_download_size\": \"Download size: ~%d MiB\",\r\n\r\n    \"ts:import_preparing_for_import\": \"Preparing for import...\",\r\n    \"ts:import_loading_archive\": \"Loading archive...\",\r\n    \"ts:import_extracting_archive\": \"Extracting archive...\\n\\nThis may take a minute.\",\r\n    \"ts:import_extracting\": \"Extracting \\\"%s\\\"...\",\r\n    \"ts:import_importing\": \"Importing \\\"%s\\\"...\",\r\n    \"ts:import_importing_pg\": \"Importing \\\"%s\\\"... (%d%%)\",\r\n    \"ts:import_importing_long\": \"Importing \\\"%s\\\"...\\n\\nThis may take a minute.\",\r\n    \"ts:import_failed\": \"Failed to import game assets.\",\r\n    \"ts:import_file_not_found\": \"File \\\"%s\\\" not found.\",\r\n    \"ts:import_archive_download_failed\": \"The download failed.\\n\\nPlease ensure that the specified URL is accessible and that the server accepts cross-origin (CORS) requests. Alternatively, you can manually download the file by right-clicking on the link below and selecting \\\"Save link as...\\\":\\n\\n%s\\n\\nOnce the download is complete, close this dialog and import the file directly, without opening it.\",\r\n    \"ts:import_archive_extract_failed\": \"Archive extraction failed.\",\r\n    \"ts:import_invalid_archive\": \"Archive extraction failed. Please provide a valid archive.\",\r\n    \"ts:import_out_of_memory\": \"Ran out of memory while extracting archive. Please make sure you are using a 64-bit desktop browser with a memory limit of at least 1GiB per tab.\",\r\n    \"ts:import_load_files_failed\": \"Failed to load game files.\",\r\n    \"ts:import_checksum_mismatch\": \"File \\\"%s\\\" appears to be corrupt. Please make sure you are using a compatible game client as source (Origin or XWIS), updated to the latest version (v1.006).\",\r\n    \"ts:import_no_web_assembly\": \"WebAssembly is required for archive extraction.\",\r\n    \"ts:import_no_storage\": \"No browser storage is available. Please make sure you are not browsing in Private mode.\",\r\n    \"ts:storage_quota_exceeded\": \"Browser storage quota exceeded. Please make sure you have sufficient disk space and not browsing in Private mode.\",\r\n    \"ts:storage_io_error\": \"A file could not be read from browser storage. Please close this tab and re-open the game in a new tab. If the problem persists, clear all website data and retry.\",\r\n    \"ts:storage_migrating_file\": \"Moving \\\"%s\\\" to new storage system...\",\r\n    \"ts:replay_storage_migrating\": \"Migrating replay storage... (%d%%)\",\r\n\r\n    \"ts:warning\": \"Warning\",\r\n    \"ts:preloading\": \"Prefetching assets...\",\r\n    \"ts:reportbug\": \"Report a Bug\",\r\n    \"ts:reportbugtt\": \"You can report any bugs you find on our Discord server\",\r\n    \"ts:reportbugdesc\": \"You can submit a bug on our dedicated Discord server channel by following the link below:\",\r\n    \"ts:patchnotes\": \"Patch Notes\",\r\n    \"stt:patchnotes\": \"Displays the change log for each version of the game client\",\r\n    \"ts:infoandcredits\": \"Info & Credits\",\r\n    \"stt:infoandcredits\": \"View additional information and credits\",\r\n    \"ts:outdatedclient\": \"You are using an outdated client. Please refresh the page. If the problem persists, you may also need to clear your browser's cache.\",\r\n    \"txt_mismatch\": \"Game versions incompatible. The room you are trying to join is using a different game version.\",\r\n    \"gui:joinernomap\": \"You do not have %s.\",\r\n    \"ts:gamecrashed\": \"Grats. You broke it. :|\\n\\nThe game client has crashed unexpectedly. This is a fatal error.\",\r\n    \"ts:custommapcrash\": \"The current map is most likely unsupported or contains errors. Please contact the map author.\",\r\n    \"ts:desyncdetected\": \"Game desynchronization detected. This a fatal error.\",\r\n    \"ts:badnickname\": \"Nickname may contain only alphanumerical characters (A-Z, a-z, 0-9), underscore (_) or dash (-).\",\r\n    \"ts:toall\": \"To all:\",\r\n    \"ts:toallies\": \"To allies:\",\r\n    \"ts:to\": \"To %s:\",\r\n    \"ts:replaychatfrom\": \"(%s):\",\r\n    \"ts:chatfrom\": \"%s:\",\r\n    \"ts:chatfromallies\": \"%s (allies):\",\r\n    \"ts:pagefrom\": \"(%s):\",\r\n    \"ts:chatuserlink\": \"[%s]\",\r\n    \"ts:chattimestamp\": \"[%s]\",\r\n    \"ts:chatcyclehint\": \"Press (%s) to cycle chat recipients\",\r\n    \"ts:chatrestricted\": \"You are currently not allowed to speak in public games.\",\r\n    \"ts:stalematewarning\": \"Stalemate detected! The game will end in %d minutes unless any player trains a unit, constructs a building (except walls), destroys or captures an enemy building, gains resources (if owning at least a production building or Construction Yard).\",\r\n    \"ts:stalematetimer\": \"The game will end in:\",\r\n    \"ts:playerassetssplit\": \"Units and resources owned by %s have been split among remaining allies.\",\r\n\r\n    \"gui:mastervolume\": \"Master Volume\",\r\n    \"gui:sfxvolume\": \"SFX Volume\",\r\n    \"gui:ambientvolume\": \"Ambient Volume\",\r\n    \"gui:uivolume\": \"UI Volume\",\r\n    \"gui:creditsvolume\": \"Credits Volume\",\r\n    \"gui:requestaudiopermission\": \"The game requires your permission to play audio.\\n\\nTo prevent this message from being shown in the future, please allow audio to always play on this website from your browser settings.\",\r\n    \"gui:fullscreen\": \"Fullscreen (%s)\",\r\n    \"stt:fullscreen\": \"Toggle full screen mode\",\r\n    \"ts:hotkeyfswarning\": \"Some hotkeys may only be captured in full-screen\",\r\n    \"cmnd:togglefps\": \"Toggle FPS\",\r\n    \"cmnd:togglefpsdesc\": \"Toggles display of performance statistics, such as frame rate, memory usage and latency\",\r\n    \"ts:reconnectprompt\": \"Would you like to reconnect to your previous game?\",\r\n    \"ts:reconnect\": \"Reconnect\",\r\n    \"ts:gameplayopts\": \"Gameplay\",\r\n    \"ts:flyerlabel\": \"Show Flyer Helper\",\r\n    \"stt:flyerlabel\": \"Marks the position of flying units on the ground\",\r\n    \"ts:flyeralways\": \"Always\",\r\n    \"ts:flyerselected\": \"Selected\",\r\n    \"ts:flyernever\": \"Never\",\r\n    \"ts:attackmovebutton\": \"Attack/Move Button\",\r\n    \"stt:attackmovebutton\": \"Allows using a different mouse button for issuing orders versus selecting units\",\r\n    \"ts:attackmovebuttonleft\": \"Left Mouse\",\r\n    \"ts:attackmovebuttonright\": \"Right Mouse\",\r\n    \"ts:rightclickscroll\": \"Right Click Scrolling\",\r\n    \"stt:rightclickscroll\": \"When right mouse button is held, player can scroll around the map\",\r\n    \"ts:mouseaccel\": \"Mouse Acceleration\",\r\n    \"stt:mouseaccel\": \"Controls precision of mouse movements when the mouse pointer is captured\",\r\n    \"ts:mouseaccelhint\": \"Overrides the native OS setting on supported platforms. Disable this if you are experiencing unpredictable pointer movement.\",\r\n    \"ts:gfxopts\": \"Graphics\",\r\n    \"ts:resolution\": \"Resolution\",\r\n    \"ts:resolutionhint\": \"Maximum resolution is determined by your operating system settings and the size of the browser tab/window. You can use the browser zoom function ([Ctrl] + [+]/[-]) to adjust it.\",\r\n    \"ts:resolutionfit\": \"Fit window (%s)\",\r\n    \"ts:resolutionfullscreen\": \"Fullscreen (%s)\",\r\n    \"ts:gfxmodels\": \"Models\",\r\n    \"stt:gfxmodels\": \"Adjusts the quality of 3D voxel models\",\r\n    \"ts:gfxshadows\": \"Dynamic Shadows\",\r\n    \"stt:gfxshadows\": \"Adjust the quality of dynamically rendered shadows\",\r\n    \"ts:gfxqualityhigh\": \"High\",\r\n    \"ts:gfxqualitymed\": \"Medium\",\r\n    \"ts:gfxqualitylow\": \"Low\",\r\n    \"ts:gfxqualityoff\": \"Off\",\r\n    \"ts:pingvalue\": \"%d ms\",\r\n    \"ts:serveroffline\": \"Offline\",\r\n    \"ts:serveronline\": \"Online\",\r\n\r\n    \"gui:demo\": \"Demo Mode\",\r\n    \"stt:demo\": \"Play a singleplayer match against a training dummy\",\r\n    \"gui:aidummy\": \"Training Dummy\",\r\n    \"gui:ainormal\": \"AI - Normal\",\r\n    \"gui:ainormal:tooltip\": \"Normal difficulty AI. Builds base, trains units and attacks.\",\r\n    \"gui:aicustom\": \"AI - Custom\",\r\n    \"gui:aicustom:tooltip\": \"Uses uploaded custom AI bot script.\",\r\n    \"gui:botupload\": \"Upload AI Bot\",\r\n    \"gui:botupload:title\": \"Upload AI Bot Script\",\r\n    \"gui:botupload:select\": \"Select Bot Zip File\",\r\n    \"gui:botupload:success\": \"Bot uploaded successfully!\",\r\n    \"gui:botupload:fail\": \"Bot upload failed\",\r\n    \"gui:botupload:manage\": \"Manage Bots\",\r\n    \"gui:botupload:remove\": \"Remove\",\r\n    \"gui:botupload:nobot\": \"No custom bots uploaded\",\r\n    \"gui:botupload:hint\": \"Upload a .zip file containing bot.ts or index.ts\",\r\n    \"stt:skirmishbuttonuploadbot\": \"Upload a custom AI bot script package\",\r\n\r\n    \"gui:gameresultwaiting\": \"Waiting for server results...\",\r\n    \"gui:gameresultvictory\": \"Victory!\",\r\n    \"gui:gameresultdefeat\": \"Defeat!\",\r\n    \"gui:gameresultdraw\": \"Draw!\",\r\n\r\n    \"gui:quickmatchgamemode\": \"Select Mode\",\r\n    \"gui:ranked\": \"Ranked\",\r\n    \"gui:unranked\": \"Unranked\",\r\n    \"gui:quickmatchplay\": \"Play\",\r\n    \"wol:matchmodeunavail\": \"This game mode is currently unavailable.\",\r\n    \"wol:matchavgwaittime\": \"Average Wait Time: \",\r\n    \"wol:matchavgwaittimeminutes\": \"%s minute(s)\",\r\n    \"wol:matchavgwaittimeunavail\": \"Unavailable\",\r\n    \"wol:matchplayersinqueue\": \"Players in queue: %d\",\r\n    \"wol:matchstartseconds\": \"Game starting in %d...\",\r\n    \"gui:viewladder\": \"View Ladder\",\r\n    \"gui:breakingnews\": \"Breaking News\",\r\n    \"gui:viewrules\": \"View Rules\",\r\n    \"gui:rules\": \"Rules\",\r\n    \"gui:laddercurrent\": \"Current Season\",\r\n    \"gui:ladderprev\": \"Previous Season\",\r\n    \"gui:ladderseason\": \"Season %s\",\r\n    \"gui:draws\": \"Draws :\",\r\n    \"gui:profileprovmmr\": \"Provisional MMR :\",\r\n    \"gui:profilemmr\": \"MMR :\",\r\n    \"gui:mmr\": \"MMR\",\r\n    \"gui:profilebonuspool\": \"Bonus Pool :\",\r\n    \"gui:ladderplacement\": \"Complete %d ranked matches to determine your initial rank placement.\",\r\n    \"gui:ladderpromoprogress\": \"Progress :\",\r\n    \"gui:ladderdivision\": \"Division: %s\",\r\n    \"gui:ladderseasoninfo\": \"Season Info\",\r\n    \"gui:laddertoptierstart\": \"Generals Start: \",\r\n    \"gui:laddertoptierpromotions\": \"Generals Promotions: \",\r\n    \"gui:laddertoptierdemotions\": \"Generals Demotions: \",\r\n    \"gui:ladderseasonlock\": \"Season Lock: \",\r\n    \"gui:ladderrankedplayers\": \"Ranked Players: %d\",\r\n    \"gui:laddertype1v1\": \"1v1\",\r\n    \"gui:laddertype2v2\": \"2v2\",\r\n    \"gui:laddertype2v2random\": \"2v2 Random\",\r\n    \"gui:laddertype2v2arranged\": \"2v2 Arranged\",\r\n\r\n    \"GUI:RankPrivate\": \"Private\",\r\n    \"GUI:RankCorporal\": \"Corporal\",\r\n    \"GUI:RankSergeant\": \"Sergeant\",\r\n    \"GUI:RankLieutenant\": \"Lieutenant\",\r\n    \"GUI:RankMajor\": \"Major\",\r\n    \"GUI:RankColonel\": \"Colonel\",\r\n    \"GUI:RankBrigGeneral\": \"Brigadier General\",\r\n    \"GUI:RankGeneral\": \"General\",\r\n    \"GUI:RankFiveStar\": \"5-Star General\",\r\n    \"GUI:RankCmdInChief\": \"Commander-in-chief\",\r\n\r\n    \"gui:team\": \"Team\",\r\n    \"gui:teamno\": \"Team %s\",\r\n    \"gui:teamgame\": \"Team Alliance\",\r\n    \"stt:modeteamgame\": \"In 'Team Alliance' players should choose a start position near their allies.\",\r\n    \"gui:startposition\": \"Start\",\r\n    \"gui:noneassymbols\": \"--\",\r\n    \"stt:hostcombostart\": \"Player's start position.\",\r\n    \"stt:hostcomboteam\": \"Player's team.\",\r\n    \"txt_cannot_ally\": \"Must have more than one team to start a game!\",\r\n\r\n    \"gui:hostteams\": \"Host Teams\",\r\n    \"stt:hostcboxhostteams\": \"The host chooses the team and starting location for each player\",\r\n    \"gui:destroyablebridges\": \"Destroyable Bridges\",\r\n    \"stt:destroyablebridges\": \"Bridges can be destroyed by force-firing on them.\",\r\n    \"gui:nodogengikills\": \"No Dog Engineer Kills\",\r\n    \"stt:nodogengikills\": \"Dogs will be unable to kill engineers.\",\r\n    \"stt:multiengineer\": \"Capturing an enemy structure requires %d engineers instead of one.\",\r\n\r\n    \"gui:replays\": \"Replays\",\r\n    \"stt:replays\": \"Play back a recording of a previously played game\",\r\n    \"gui:selectreplay\": \"Select replay:\",\r\n    \"gui:loadreplay\": \"Load\",\r\n    \"gui:deletereplay\": \"Delete\",\r\n    \"gui:replaylisterror\": \"Failed to load replays\",\r\n    \"gui:replayerror\": \"Failed to load replay\",\r\n    \"gui:replayversionmismatch\": \"The replay was recorded with a different game version (%s) and could not be loaded.\",\r\n    \"gui:replaymodmismatch\": \"The replay was recorded with different game client modifications and could not be loaded.\",\r\n    \"gui:replayopenoldclient\": \"The replay will be loaded with game version %s in a new browser tab or window.\",\r\n    \"gui:replaywindowclose\": \"You may now close this browser tab/window\",\r\n    \"gui:confirmdeletereplay\": \"Are you sure you want to delete the replay \\\"%s\\\"?\",\r\n    \"gui:keepreplay\": \"Keep\",\r\n    \"stt:keepreplay\": \"Permanently stores the replay, preventing it from being automatically deleted when new replays are created\",\r\n    \"gui:renamereplay\": \"Rename\",\r\n    \"gui:replaynameprompt\": \"Enter a replay name:\",\r\n    \"gui:exportreplay\": \"Export...\",\r\n    \"stt:exportreplay\": \"Exports a raw replay file that can be imported later in a compatible client.\",\r\n    \"gui:importreplay\": \"Import...\",\r\n    \"stt:importreplay\": \"Imports a replay previously exported from a compatible client\",\r\n    \"gui:importreplayerror\": \"Failed to import replay. The file is corrupt or incompatible with this game client.\",\r\n    \"gui:savereplayerror\": \"Failed to save replay. Please check your browser storage quota.\",\r\n    \"gui:replayexistserror\": \"A replay with that name already exists.\",\r\n    \"gui:deletereplayerror\": \"Failed to delete replay\",\r\n    \"gui:replaytime\": \"Recorded at\",\r\n    \"gui:gameversion\": \"Game version\",\r\n    \"gui:gameid\": \"Game ID\",\r\n    \"gui:duration\": \"Duration\",\r\n    \"tip:replayrewind\": \"Restart\",\r\n    \"tip:play\": \"Play\",\r\n    \"tip:pause\": \"Pause\",\r\n    \"tip:replayspeed\": \"Replay speed\",\r\n    \"ts:replayspeedconfirm\": \"Replay speed changed to %s\",\r\n\r\n    \"gui:mods\": \"Mods\",\r\n    \"stt:mods\": \"Manage and play modified versions of the base game\",\r\n    \"gui:selectmod\": \"Select Mod:\",\r\n    \"gui:modname\": \"Name\",\r\n    \"gui:modstatus\": \"Status\",\r\n    \"gui:modstatusinstalled\": \"Installed\",\r\n    \"gui:modstatusupdateavail\": \"Update Available\",\r\n    \"gui:modstatusnotinstalled\": \"Not Installed\",\r\n    \"gui:modloaded\": \"Loaded\",\r\n    \"gui:modactioninstall\": \"Install\",\r\n    \"gui:modactionupdate\": \"Update\",\r\n    \"gui:modactionloadanyway\": \"Load Anyway\",\r\n    \"gui:moddescription\": \"Description\",\r\n    \"gui:modauthor\": \"Author(s)\",\r\n    \"gui:modwebsite\": \"Website\",\r\n    \"gui:modversion\": \"Version\",\r\n    \"gui:modunsupported\": \"Unsupported\",\r\n    \"gui:modlisterror\": \"Failed to load mods\",\r\n    \"gui:loadmod\": \"Load\",\r\n    \"gui:unloadmod\": \"Unload\",\r\n    \"gui:uninstallmod\": \"Uninstall\",\r\n    \"stt:uninstallmod\": \"Deletes the selected mod and all files belonging to it\",\r\n    \"gui:confirmuninstallmod\": \"Are you sure you want to uninstall the mod \\\"%s\\\"?\\n\\nIf you continue, all mod files will be deleted. This operation cannot be undone!\",\r\n    \"gui:uninstallmoderror\": \"Failed to delete mod files\",\r\n    \"gui:importmod\": \"Import...\",\r\n    \"stt:importmod\": \"Installs a mod from a local archive file\",\r\n    \"gui:browsemod\": \"View Files\",\r\n    \"stt:browsemod\": \"Reveals the mod files in Storage Explorer\",\r\n    \"gui:modsdk\": \"Mod SDK\",\r\n    \"stt:modsdk\": \"Opens the documentation and resources for mod development in a new tab\",\r\n    \"gui:importmoderror\": \"The mod archive could not be imported.\",\r\n    \"gui:importmodfolderprompt\": \"Choose a name for the mod:\\n\\nThe name may only contain alphanumeric characters, dash (-) and underscore (_).\",\r\n    \"gui:importmodfolderbadname\": \"The name may only contain alphanumeric characters, dash (-) and underscore (_).\",\r\n    \"gui:importmodfolderexists\": \"A mod with that name already exists. Please choose a different name.\",\r\n    \"gui:importmodbadarchive\": \"The provided archive doesn't appear to contain a valid mod.\",\r\n    \"gui:importmodunsupportedwarn\": \"The mod you are trying to install was not updated for the Chrono Divide game engine. You may still install any vanilla RA2 mod, but it may not work correctly. Proceed at your own risk!\",\r\n    \"gui:importduplicatemoderror\": \"This mod is already installed.\",\r\n    \"gui:installmoddownloadprompt\": \"The mod will now be downloaded from the server. The estimated download size is %d MiB. Do you wish to continue?\",\r\n    \"gui:modupdateavail\": \"An update is available.\",\r\n    \"gui:updatemodprompt\": \"Version: %s\\nDownload Size: %.1d MiB.\\n\\nDo you want to download it now?\",\r\n    \"gui:manualdownloadmodprompt\": \"Please download the mod archive from the following URL, then click the \\\"Import...\\\" button from the sidebar to install it.\",\r\n    \"gui:installmodstoragefull\": \"There is insufficient space available in browser storage (%d MiB available, %d MiB required). Try freeing some disk space and retry.\",\r\n\r\n    \"gui:roomdesc\": \"Room Description\",\r\n    \"gui:ping\": \"Ping\",\r\n    \"gui:hostname\": \"Host\",\r\n    \"gui:gamemod\": \"Mod: %s\",\r\n    \"gui:custommap\": \"Custom Map\",\r\n    \"stt:verifiedmap\": \"Verified: This map has been reviewed and contains no custom game rules that create unfair advantages.\",\r\n    \"stt:unverifiedmap\": \"Unverified: This map may contain custom game rules that could impact fair gameplay.\",\r\n    \"gui:hostnomapupload\": \"The map cannot be transferred because new accounts are not allowed to upload custom maps.\",\r\n    \"ts:region\": \"Region\",\r\n    \"ts:serverlabel\": \"Server\",\r\n    \"ts:connectfailed\": \"Couldn't connect to server\",\r\n    \"gui:changeserver\": \"Change Server\",\r\n    \"stt:changeserver\": \"Play on another game server or region\",\r\n    \"ts:serverfull\": \"Server is Full.\",\r\n    \"ts:loginavgwaittime\": \"Average wait time: \",\r\n    \"ts:loginavgwaittimeminutes\": \"%s minute(s)\",\r\n    \"ts:loginavgwaittimeunavail\": \"Unavailable\",\r\n    \"ts:loginpositioninqueue\": \"Position in queue: %d\",\r\n    \"wol:toomanyloginattempts\": \"Too many failed login attempts\",\r\n    \"wol:alreadyloggedin\": \"This nickname is already logged in\",\r\n    \"wol:instancenotfound\": \"Game instance not found\",\r\n    \"wol:createdtoomanyinstances\": \"You have created too many game instances recently and you must wait before trying again.\",\r\n    \"wol:instancenotallowed\": \"Not allowed to connect to this game instance\",\r\n    \"wol:gamealreadystarted\": \"Cannot join a game that has already started\",\r\n    \"ts:assetloaderror\": \"Failed to load game assets\",\r\n    \"ts:gameiniterror\": \"Failed to initialize game\",\r\n    \"ts:mapnotfound\": \"Map %s not found\",\r\n    \"ts:mapdownloadfailed\": \"Failed to download map %s\",\r\n    \"ts:mapmismatch\": \"The replay was recorded with a different version of the map %s and cannot be loaded.\",\r\n    \"ts:mapunsupportedgamemode\": \"The selected map doesn't support any of the available game modes.\",\r\n    \"ts:mapunsupportedgame\": \"The selected map doesn't appear to be a Red Alert 2 map.\",\r\n    \"ts:mapunsupportedtileset\": \"The selected map uses an unsupported tile set.\",\r\n    \"ts:mapunsupportedoverlay\": \"The selected map uses an unsupported overlay (%d).\",\r\n    \"ts:mapunsupportedterrain\": \"The selected map uses an unsupported terrain (%s).\",\r\n    \"ts:mapunsupportedweapon\": \"The selected map uses an unsupported weapon type (%s).\",\r\n    \"ts:mapunsupportedprojectile\": \"The selected map uses an unsupported projectile type (%s).\",\r\n    \"ts:mapunsupportedwarhead\": \"The selected map uses an unsupported warhead type (%s).\",\r\n    \"ts:mapunsupportedtechno\": \"The selected map uses an unsupported techno type (%s).\",\r\n    \"ts:mapunsupportedtheater\": \"The selected map uses an unsupported theater (%s).\",\r\n    \"ts:mapunsupportedtriggers\": \"The selected map contains unsupported trigger events and/or actions and may not work correctly.\\n\\nYou may proceed at your own risk!\",\r\n    \"ts:gameinitoom\": \"Ran out of memory while loading game. Please make sure you are using a 64-bit desktop browser with a memory limit of at least 1GiB per tab.\",\r\n    \"ts:gamecrashoom\": \"The game crashed because it ran out of memory. Please make sure you are using a 64-bit desktop browser with a memory limit of at least 1GiB per tab.\",\r\n    \"ts:rendererwarning\": \"Unsupported graphics card detected. Would you like to try low quality settings?\\n\\nFor optimal performance, please make sure your browser is using a dedicated graphics card (e.g. NVIDIA, AMD) and hardware acceleration is enabled. For common setups, you can follow {link}these instructions{/link}.\",\r\n    \"ts:rendereruselow\": \"Use low settings\",\r\n    \"ts:rendererignore\": \"Ignore\",\r\n    \"ts:rendererchangedesc\": \"Graphics card change detected. Would you like to switch to high quality settings?\",\r\n    \"ts:rendereriniterror\": \"Failed to initialize WebGL renderer\",\r\n    \"ts:guiinitfserror\": \"Failed to read data from browser storage. Please refresh the page. If the problem persists, empty your browser cache and site data.\",\r\n    \"ts:guiinitunknownerror\": \"Unknown error occurred while initializing user interface\",\r\n    \"ts:modloaderror\": \"This game mod could not be loaded. Please contact the mod author.\",\r\n    \"gui:notready\": \"Not ready\",\r\n    \"stt:notready\": \"Notifies the host that you are not ready to start the game\",\r\n    \"ts:importmap\": \"Custom Map...\",\r\n    \"stt:importmap\": \"Adds a custom map file from the local file system\",\r\n    \"ts:importmaperror\": \"The map could not be imported.\",\r\n    \"ts:filenameerror\": \"The file name contains restricted characters and cannot be saved\",\r\n    \"ts:importmapunsupportedtype\": \"Unsupported file type.\\n\\nSupported map types: %s\",\r\n    \"ts:importmapduplicateerror\": \"Map \\\"%s\\\" already exists.\",\r\n    \"ts:sortnone\": \"-\",\r\n    \"ts:sortname\": \"Map Name\",\r\n    \"ts:sortmaxslots\": \"Max Slots\",\r\n    \"stt:sortby\": \"Sorts available maps by the selected criteria\",\r\n\r\n    \"ts:storage_quota_warning\": \"WARNING: New replays may not be saved because browser storage is full (%d of %d MiB used). Please check your browser storage quota, make sure you have sufficient disk space and not browsing in Private mode.\",\r\n\r\n    \"gui:storage\": \"Storage\",\r\n    \"gui:storageused\": \"%s of %s used\",\r\n    \"gui:uploading\": \"Uploading \\\"%s\\\"...\",\r\n    \"gui:uploadfinished\": \"Upload finished.\",\r\n    \"gui:uploadfailed\": \"Upload failed.\",\r\n    \"gui:uploadfailedquota\": \"Upload failed. Storage quota exceeded.\",\r\n    \"gui:confirmdeletefiles\": \"Are you sure you want to permanently delete these %d items?\",\r\n    \"gui:confirmdeletefile\": \"Are you sure you want to permanently delete this item?\",\r\n    \"gui:confirmdeletesystemfile\": \"The item \\\"%s\\\" is a system file or folder. If you remove it, the game may no longer work correctly and you will have to locate the original game files again.\\n\\nAre you sure you want to continue?\",\r\n    \"gui:exitandreload\": \"Exit & Reload\",\r\n    \"gui:confirmoverwritefile\": \"The destination folder \\\"%s\\\" already contains a file named \\\"%s\\\".\\n\\nAre you sure you want to overwrite it?\",\r\n    \"gui:yestoall\": \"Yes to all\",\r\n    \"gui:creatingarchive\": \"Creating ZIP archive...\\n\\nThis may take a minute.\",\r\n    \"gui:downloadfinished\": \"Download finished.\",\r\n    \"gui:downloadfailed\": \"Download failed.\",\r\n    \"gui:downloadaborted\": \"Download aborted.\",\r\n    \"gui:newfolderprompt\": \"Choose a folder name:\",\r\n    \"gui:newfoldernotallowed\": \"Can't create a new folder here.\",\r\n    \"gui:newfolderexists\": \"An entry with that name already exists.\",\r\n    \"gui:newfolderinvalidname\": \"Invalid folder name.\",\r\n    \"GUI:LoadingFileExplorer\": \"Loading file explorer...\"\r\n}\r\n"
  },
  {
    "path": "public/res/locale/zh-CN.json",
    "content": "{\n    \"gui:ok\": \"确定\",\n    \"txt_copyright\": \"© 2025 网页红井制作组保留渲染引擎和联机服务的权利\",\n    \"gui:wwbrand\": \"美术素材版权为EA所有，目前正在逐步替换中\",\n    \"gui:loadingex\": \"加载中...\",\n    \"ts:disclaimer\": \"免责声明：\\n\\\"网页红井\\\" 是一个非盈利的粉丝项目，与 Electronic Arts Inc.(EA) 没有任何关联。\\n没有侵犯版权的意图。所有权利均归其各自所有者所有。\",\n    \"ts:downloadingpgsize\": \"下载中 %.1f MiB / %.1f MiB... (%d%%)\",\n    \"ts:downloadingpgunkn\": \"下载中 %.1f MiB...\",\n    \"ts:downloading\": \"正在下载...\",\n    \"ts:downloadingpg\": \"正在下载...（%d%%）\",\n    \"ts:downloadfailed\": \"无法下载远程资源。请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:gameres_locate_title\": \"❗❗--请关注公众号 思牛逼 获取游玩指南--❗❗\",\n    \"ts:gameres_import_desc\": \"如果你需要游玩MOD，那么你必须要导入网页红井的完全体副本。否则请点击右上角 X\",\n    \"ts:gameres_drop_desc\": \"将所需的游戏文件拖放到此处\",\n    \"ts:gameres_or\": \"或者\",\n    \"ts:gameres_browse_folder\": \"选择文件夹...\",\n    \"ts:gameres_browse_archive\": \"选择归档文件...\",\n    \"ts:gameres_supported_archive_formats\": \"支持的归档文件格式：rar、tar、tar.gz、tar.bz2、tar.xz、zip、7z、exe（sfx）\",\n    \"ts:gameres_download_desc\": \"我们在交流群中提供，您可以微信关注公众号 思牛逼 获取群号，其他问题也可以入群讨论\",\n    \"ts:gameres_download_hint\": \"提示：加入QQ群后，群文件内下载 完全体副本，存储在你的本地\",\n    \"ts:gameres_download_size\": \"然后点击下方的 选择归档文件 按钮，选择你下载的完全体副本后导入\",\n    \"ts:import_preparing_for_import\": \"准备导入...\",\n    \"ts:import_loading_archive\": \"正在加载归档...\",\n    \"ts:import_extracting_archive\": \"正在解压归档...\\n\\n这可能需要一分钟的时间。\",\n    \"ts:import_importing\": \"正在导入\\\"%s\\\"...\",\n    \"ts:import_importing_pg\": \"正在导入\\\"%s\\\"...（%d%%）\",\n    \"ts:import_importing_long\": \"正在导入\\\"%s\\\"...\\n\\n这可能需要一分钟的时间。\",\n    \"ts:import_failed\": \"无法导入游戏资源。请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:import_file_not_found\": \"找不到文件\\\"%s\\\"。请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:import_extracting\": \"正在解压 \\\"%s\\\"...\",\n    \"ts:import_archive_download_failed\": \"下载失败，请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:import_archive_extract_failed\": \"解压归档文件失败。请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:import_invalid_archive\": \"解压归档文件失败。请提供有效的归档文件。请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:import_out_of_memory\": \"解压归档文件时内存不足。请确保您使用的是64位桌面浏览器，并且每个选项卡的内存限制至少为1 GiB。请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:import_load_files_failed\": \"无法加载游戏文件。请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:import_checksum_mismatch\": \"文件\\\"%s\\\"似乎损坏。请确保您使用的是兼容的游戏客户端（Origin 或 XWIS），已更新到最新版本（v1.006）。\",\n    \"ts:import_no_web_assembly\": \"WebAssembly 必须用于解压归档文件。\",\n    \"ts:import_no_storage\": \"没有可用的浏览器存储。请确保您未在隐私模式下浏览。\",\n    \"ts:storage_quota_exceeded\": \"浏览器存储配额超过限制。请确保您有足够的磁盘空间，并且未在隐私模式下浏览。\",\n    \"ts:storage_io_error\": \"无法从浏览器存储中读取文件。请关闭此选项卡，然后在新选项卡中重新打开游戏。如果问题仍然存在，请清除所有网站数据并重试。\",\n    \"ts:storage_migrating_file\": \"正在将\\\"%s\\\"移动到新的存储系统中...\",\n    \"ts:replay_storage_migrating\": \"正在迁移回放存储...（%d%%）\",\n    \"ts:warning\": \"警告\",\n    \"ts:preloading\": \"预加载资源中...\",\n    \"ts:reportbug\": \"报告错误\",\n    \"ts:reportbugtt\": \"请微信关注公众号 思牛逼 报告BUG或者获取解决方案！\",\n    \"ts:reportbugdesc\": \"请微信关注公众号 思牛逼 报告BUG或者获取解决方案！：\",\n    \"ts:patchnotes\": \"补丁说明\",\n    \"stt:patchnotes\": \"显示游戏客户端每个版本的更改日志\",\n    \"ts:infoandcredits\": \"信息与制作人员\",\n    \"stt:infoandcredits\": \"查看附加信息和制作人员名单\",\n    \"ts:outdatedclient\": \"您正在使用过时的客户端。请刷新页面。如果问题仍然存在，请微信关注公众号 思牛逼 阅读里面文章获取解决方案！。\",\n    \"txt_mismatch\": \"游戏版本不兼容。请关注微信公众号 思牛逼 获取解决方案！\",\n    \"gui:joinernomap\": \"你没有 %s。\",\n    \"ts:gamecrashed\": \"恭喜。游戏崩溃。:|\\n\\n游戏客户端意外崩溃。这是一个致命错误。请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:custommapcrash\": \"当前地图可能是不受支持的，或包含错误。请与地图作者联系。或者微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:desyncdetected\": \"检测到游戏失去同步。这是一个致命错误。请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:badnickname\": \"昵称只能包含字母、数字字符（A-Z, a-z, 0-9），下划线 (_) 或破折号 (-)。\",\n    \"ts:toall\": \"对所有人说：\",\n    \"ts:toallies\": \"对盟友说：\",\n    \"ts:to\": \"对 %s 说：\",\n    \"ts:replaychatfrom\": \"（%s）：\",\n    \"ts:chatfrom\": \"%s：\",\n    \"ts:chatfromallies\": \"%s（盟友）：\",\n    \"ts:pagefrom\": \"（%s）：\",\n    \"ts:chatuserlink\": \"［%s］\",\n    \"ts:chattimestamp\": \"［%s］\",\n    \"ts:chatcyclehint\": \"按下（%s）以循环切换聊天接收者\",\n    \"ts:stalematewarning\": \"检测到僵局！游戏将在 %d 分钟后结束。除非任何玩家训练单位、建造建筑（除了墙壁）、摧毁或夺取敌方建筑、获得资源（如果拥有至少一个生产建筑或建筑工厂）\",\n    \"ts:stalematetimer\": \"游戏将在：\",\n    \"gui:mastervolume\": \"主音量\",\n    \"gui:sfxvolume\": \"音效音量\",\n    \"gui:ambientvolume\": \"环境音量\",\n    \"gui:uivolume\": \"界面音量\",\n    \"gui:creditsvolume\": \"制作人员音量\",\n    \"gui:requestaudiopermission\": \"游戏需要您的许可来播放音频。\\n\\n为了防止将来显示此消息，请从浏览器设置中允许音频始终在此网站上播放。\",\n    \"gui:fullscreen\": \"全屏（%s）\",\n    \"stt:fullscreen\": \"切换全屏模式\",\n    \"ts:hotkeyfswarning\": \"某些快捷键只能在全屏模式中捕捉\",\n    \"cmnd:togglefps\": \"切换 FPS\",\n    \"cmnd:togglefpsdesc\": \"切换显示性能统计信息，如帧率、内存使用情况和延迟\",\n    \"ts:reconnectprompt\": \"您是否要重新连接到上一局游戏？\",\n    \"ts:reconnect\": \"重新连接\",\n    \"ts:gameplayopts\": \"游戏设置\",\n    \"ts:flyerlabel\": \"显示飞行单位辅助\",\n    \"stt:flyerlabel\": \"在地面上标记飞行单位的位置\",\n    \"ts:flyeralways\": \"始终\",\n    \"ts:flyerselected\": \"选定\",\n    \"ts:flyernever\": \"从不\",\n    \"ts:attackmovebutton\": \"攻击/移动按钮\",\n    \"stt:attackmovebutton\": \"允许使用不同的鼠标按钮来下达指令和选择单位\",\n    \"ts:attackmovebuttonleft\": \"鼠标左键\",\n    \"ts:attackmovebuttonright\": \"鼠标右键\",\n    \"ts:rightclickscroll\": \"右键滚动\",\n    \"stt:rightclickscroll\": \"当按住右鼠标键时，玩家可以在地图上滚动\",\n    \"ts:mouseaccel\": \"鼠标加速\",\n    \"stt:mouseaccel\": \"控制鼠标在捕捉鼠标指针时的移动精度\",\n    \"ts:mouseaccelhint\": \"在受支持的平台上覆盖原生操作系统设置。如果遇到不可预测的指针移动，请禁用此功能。\",\n    \"ts:gfxopts\": \"图形设置\",\n    \"ts:resolution\": \"分辨率\",\n    \"ts:resolutionhint\": \"最大分辨率取决于操作系统设置和浏览器选项卡/窗口的大小。您可以使用浏览器的缩放功能（[Ctrl] + [+]/[-]）进行调整。\",\n    \"ts:resolutionfit\": \"适应窗口（%s）\",\n    \"ts:resolutionfullscreen\": \"全屏（%s）\",\n    \"ts:gfxmodels\": \"模型\",\n    \"stt:gfxmodels\": \"调整 3D 体素模型的质量\",\n    \"ts:gfxshadows\": \"动态阴影\",\n    \"stt:gfxshadows\": \"调整动态渲染阴影的质量\",\n    \"ts:gfxqualityhigh\": \"高\",\n    \"ts:gfxqualitymed\": \"中\",\n    \"ts:gfxqualitylow\": \"低\",\n    \"ts:gfxqualityoff\": \"关闭\",\n    \"ts:pingvalue\": \"%d 毫秒\",\n    \"ts:serveroffline\": \"离线\",\n    \"ts:serveronline\": \"在线\",\n    \"gui:demo\": \"单机模式\",\n    \"stt:demo\": \"无需保持网络连接，即可与各类AI对战\",\n    \"gui:aidummy\": \"AI-弱智\",\n    \"gui:ainormal\": \"AI-普通\",\n    \"gui:ainormal:tooltip\": \"普通难度AI，会建设基地、训练部队并发起进攻。\",\n    \"gui:aicustom\": \"AI-自定义\",\n    \"gui:aicustom:tooltip\": \"使用上传的自定义AI机器人脚本。\",\n    \"gui:botupload\": \"上传AI机器人\",\n    \"gui:botupload:title\": \"上传AI脚本\",\n    \"gui:botupload:select\": \"选择Bot压缩包\",\n    \"gui:botupload:success\": \"AI机器人上传成功！\",\n    \"gui:botupload:fail\": \"AI机器人上传失败\",\n    \"gui:botupload:manage\": \"管理AI机器人\",\n    \"gui:botupload:remove\": \"删除\",\n    \"gui:botupload:nobot\": \"暂无自定义AI机器人\",\n    \"gui:botupload:hint\": \"上传包含 bot.ts 或 index.ts 的 .zip 文件\",\n    \"stt:skirmishbuttonuploadbot\": \"上传自定义AI机器人脚本包\",\n    \"gui:quickmatchgamemode\": \"选择模式\",\n    \"gui:ranked\": \"排位赛\",\n    \"gui:unranked\": \"非排位赛\",\n    \"gui:quickmatchplay\": \"开始快速匹配\",\n    \"wol:matchjoininggame\": \"正在加入游戏...\",\n    \"wol:matchwaitingforplayers\": \"等待对手加入游戏...\",\n    \"wol:matchavgwaittime\": \"平均等待时间：\",\n    \"wol:matchavgwaittimeminutes\": \"%s 分钟\",\n    \"wol:matchavgwaittimeunavail\": \"不可用\",\n    \"wol:matchplayersinqueue\": \"排队中的玩家数：%d\",\n    \"wol:matchstartseconds\": \"游戏将在 %d 秒后开始...\",\n    \"gui:viewladder\": \"查看排行榜\",\n    \"gui:breakingnews\": \"突发新闻\",\n    \"gui:viewrules\": \"查看排位规则\",\n    \"gui:rules\": \"排位赛规则\",\n    \"gui:laddercurrent\": \"当前赛季\",\n    \"gui:ladderprev\": \"先前赛季\",\n    \"gui:ladderseason\": \"第 %s 赛季\",\n    \"GUI:RankPrivate\": \"列兵\",\n    \"GUI:RankCorporal\": \"下士\",\n    \"GUI:RankSergeant\": \"中士\",\n    \"GUI:RankLieutenant\": \"少尉\",\n    \"GUI:RankMajor\": \"少校\",\n    \"GUI:RankColonel\": \"上校\",\n    \"GUI:RankBrigGeneral\": \"准将\",\n    \"GUI:RankGeneral\": \"将军\",\n    \"GUI:RankFiveStar\": \"五星上将\",\n    \"GUI:RankCmdInChief\": \"统帅\",\n    \"gui:team\": \"队伍\",\n    \"gui:teamgame\": \"团队联盟\",\n    \"stt:modeteamgame\": \"在\\\"团队联盟\\\"模式中，玩家应选择靠近盟友的起始位置。\",\n    \"gui:startposition\": \"起始位置\",\n    \"gui:noneassymbols\": \"--\",\n    \"stt:hostcombostart\": \"玩家的起始位置。\",\n    \"stt:hostcomboteam\": \"玩家的队伍。\",\n    \"txt_cannot_ally\": \"必须有多于一个队伍才能开始游戏！\",\n    \"gui:hostteams\": \"房主队伍\",\n    \"stt:hostcboxhostteams\": \"房主为每个玩家选择队伍和起始位置\",\n    \"gui:replays\": \"回放\",\n    \"stt:replays\": \"播放以前进行过的游戏的记录\",\n    \"gui:selectreplay\": \"选择回放：\",\n    \"gui:loadreplay\": \"加载\",\n    \"gui:deletereplay\": \"删除\",\n    \"gui:replaylisterror\": \"无法加载回放\",\n    \"gui:replayerror\": \"无法加载回放\",\n    \"gui:replayversionmismatch\": \"回放使用的是不同的游戏版本（%s），无法加载。请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"gui:replaymodmismatch\": \"回放是在不同的游戏客户端修改下录制的，无法加载。请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"gui:replayopenoldclient\": \"将在新的浏览器选项卡或窗口中使用游戏版本 %s 加载回放。\",\n    \"gui:replaywindowclose\": \"您现在可以关闭此浏览器选项卡/窗口\",\n    \"gui:confirmdeletereplay\": \"您确定要永久删除回放\\\"%s\\\"吗？\",\n    \"gui:keepreplay\": \"保留\",\n    \"stt:keepreplay\": \"永久保存回放，防止在创建新回放时自动删除\",\n    \"gui:renamereplay\": \"重命名\",\n    \"gui:replaynameprompt\": \"输入回放名称：\",\n    \"gui:exportreplay\": \"导出...\",\n    \"stt:exportreplay\": \"导出一个原始回放文件，以便以后在兼容的客户端中导入\",\n    \"gui:importreplay\": \"导入...\",\n    \"stt:importreplay\": \"导入之前从兼容客户端导出的回放\",\n    \"gui:importreplayerror\": \"无法导入回放。文件损坏或与此游戏客户端不兼容。\",\n    \"gui:savereplayerror\": \"无法保存回放。请检查您的浏览器存储配额。\",\n    \"gui:replayexistserror\": \"同名的回放已存在。\",\n    \"gui:deletereplayerror\": \"无法删除回放\",\n    \"gui:replaytime\": \"录制于\",\n    \"gui:gameversion\": \"游戏版本\",\n    \"gui:duration\": \"持续时间\",\n    \"tip:replayrewind\": \"重新开始\",\n    \"tip:play\": \"播放\",\n    \"tip:pause\": \"暂停\",\n    \"tip:replayspeed\": \"回放速度\",\n    \"ts:replayspeedconfirm\": \"回放速度改为 %s\",\n    \"gui:mods\": \"MOD\",\n    \"stt:mods\": \"管理和玩基础游戏的MOD\",\n    \"gui:selectmod\": \"选择MOD：\",\n    \"gui:modname\": \"名称\",\n    \"gui:modstatus\": \"状态\",\n    \"gui:modstatusinstalled\": \"已安装\",\n    \"gui:modstatusupdateavail\": \"有更新可用\",\n    \"gui:modstatusnotinstalled\": \"未安装\",\n    \"gui:modloaded\": \"已加载\",\n    \"gui:modactioninstall\": \"安装\",\n    \"gui:modactionupdate\": \"更新\",\n    \"gui:modactionloadanyway\": \"仍然加载\",\n    \"gui:moddescription\": \"描述\",\n    \"gui:modauthor\": \"作者\",\n    \"gui:modwebsite\": \"网站\",\n    \"gui:modversion\": \"版本\",\n    \"gui:modunsupported\": \"不受支持\",\n    \"gui:modlisterror\": \"无法加载MOD\",\n    \"gui:loadmod\": \"加载\",\n    \"gui:unloadmod\": \"卸载\",\n    \"gui:uninstallmod\": \"卸载\",\n    \"stt:uninstallmod\": \"删除所选MOD和所有其相关文件\",\n    \"gui:confirmuninstallmod\": \"您确定要卸载MOD\\\"%s\\\"吗？\\n\\n如果继续操作，所有MOD文件都将被删除。此操作无法撤销！\",\n    \"gui:uninstallmoderror\": \"无法删除MOD文件\",\n    \"gui:importmod\": \"导入...\",\n    \"stt:importmod\": \"从本地归档文件中安装MOD\",\n    \"gui:browsemod\": \"查看文件夹\",\n    \"stt:browsemod\": \"在存储资源管理器中显示MOD文件\",\n    \"gui:modsdk\": \"MOD SDK\",\n    \"stt:modsdk\": \"在新选项卡中打开用于修改开发的文档和资源\",\n    \"gui:importmoderror\": \"MOD归档无法导入。\",\n    \"gui:importmodfolderprompt\": \"为MOD选择一个名称：\\n\\n名称只能包含字母、数字字符和破折号（-）或下划线（_）。\",\n    \"gui:importmodfolderbadname\": \"名称只能包含字母、数字字符和破折号（-）或下划线（_）。\",\n    \"gui:importmodfolderexists\": \"同名的MOD已存在。请重新选择一个名称。\",\n    \"gui:importmodbadarchive\": \"提供的归档似乎不包含有效的MOD。\",\n    \"gui:importmodunsupportedwarn\": \"您要安装的MOD未针对Chrono Divide游戏引擎进行更新。您仍然可以安装任何红色井界通用MOD，但可能无法正常工作。请自行决定是否继续！\",\n    \"gui:importduplicatemoderror\": \"此MOD已安装。\",\n    \"gui:installmoddownloadprompt\": \"MOD现将从服务器下载。预计下载大小为 %d MiB。您是否希望继续？\",\n    \"gui:modupdateavail\": \"有可用的更新。\",\n    \"gui:updatemodprompt\": \"版本：%s\\n下载大小：%.1d MiB。\\n\\n您现在要下载吗？\",\n    \"gui:manualdownloadmodprompt\": \"请从以下 URL 下载MOD归档，然后从侧边栏单击\\\"导入...\\\"按钮安装它。\",\n    \"gui:installmodstoragefull\": \"浏览器存储空间不足（可用空间 %d MiB，需要空间 %d MiB）。请尝试释放一些磁盘空间并重试。\",\n    \"gui:roomdesc\": \"房间描述\",\n    \"gui:ping\": \"延迟\",\n    \"gui:hostname\": \"房主\",\n    \"gui:gamemod\": \"MOD：%s\",\n    \"gui:custommap\": \"自定义地图\",\n    \"stt:verifiedmap\": \"已认证：该地图经过认证，不存在不公平改动。\",\n    \"stt:unverifiedmap\": \"未认证：该地图未经过认证，可能存在影响游戏平衡性的改动。\",\n    \"ts:serverlabel\": \"服务器\",\n    \"ts:connectfailed\": \"无法连接到服务器，微信关注公众号 思牛逼 阅读里面文章获取解决方案\",\n    \"gui:changeserver\": \"更改服务器\",\n    \"stt:changeserver\": \"在其他游戏服务器或地区上进行游戏\",\n    \"ts:serverfull\": \"服务器已满。微信关注公众号 思牛逼 阅读里面文章获取解决方案\",\n    \"ts:loginavgwaittime\": \"平均等待时间：\",\n    \"ts:loginavgwaittimeminutes\": \"%s 分钟\",\n    \"ts:loginavgwaittimeunavail\": \"不可用\",\n    \"ts:loginpositioninqueue\": \"队列中的位置：%d\",\n    \"wol:toomanyloginattempts\": \"你尝试登陆失败太多次了，这似乎不太正常\",\n    \"wol:alreadyloggedin\": \"该用户已经处于登陆状态\",\n    \"wol:instancenotfound\": \"未找到游戏实例\",\n    \"wol:createdtoomanyinstances\": \"你最近已经创建了足够多的游戏实例，在重试之前你需要等待一会儿。\",\n    \"wol:instancenotallowed\": \"不允许连接到该游戏实例（通常是对局）\",\n    \"wol:gamealreadystarted\": \"无法加入一个已经开始的对局\",\n    \"ts:assetloaderror\": \"无法加载游戏资源，微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:gameiniterror\": \"无法初始化游戏，请微信关注公众号 思牛逼 阅读里面文章获取解决方案！\",\n    \"ts:mapnotfound\": \"未找到地图 %s\",\n    \"ts:mapmismatch\": \"回放是在与地图 %s 不同的版本上录制的，无法加载。\",\n    \"ts:mapunsupportedgamemode\": \"所选地图不支持任何可用的游戏模式。\",\n    \"ts:mapunsupportedgame\": \"所选地图似乎不是红色井界地图。\",\n    \"ts:mapunsupportedtileset\": \"所选地图使用了不受支持的地块集。\",\n    \"ts:mapunsupportedtheater\": \"所选地图使用了不受支持的战区（%s）。\",\n    \"ts:mapunsupportedoverlay\": \"所选地图使用了不受支持的覆盖图层（%d）。\",\n    \"ts:mapunsupportedweapon\": \"所选地图使用了不受支持的武器类型 (%s).\",\n    \"ts:mapunsupportedprojectile\": \"所选地图使用了不受支持的弹药类型 (%s).\",\n    \"ts:mapunsupportedwarhead\": \"所选地图使用了不受支持的弹头类型 (%s).\",\n    \"ts:mapunsupportedtechno\": \"所选地图使用了不受支持的科技类型 (%s).\",\n    \"ts:mapunsupportedtriggers\": \"所选地图包含不受支持的触发事件和/或操作，可能无法正常工作。\\n\\n您可以自行决定是否继续操作！\",\n    \"ts:gameinitoom\": \"加载游戏时内存不足。请确保您使用的是64位桌面浏览器，并且每个选项卡的内存限制至少为1 GiB。\",\n    \"ts:gamecrashoom\": \"游戏因内存不足而崩溃。请确保您使用的是64位桌面浏览器，并且每个选项卡的内存限制至少为1 GiB。\",\n    \"ts:rendererwarning\": \"检测到不受支持的图形卡。您是否要尝试低质量设置？\\n\\n为获得最佳性能，请确保您的浏览器使用了专用图形卡（例如 NVIDIA、ATI）并启用了硬件加速。对于常见设置，您可以按照{link}这些说明{/link}进行操作。\",\n    \"ts:rendereruselow\": \"使用低质量设置\",\n    \"ts:rendererignore\": \"忽略\",\n    \"ts:rendererchangedesc\": \"检测到图形卡更改。您是否要切换到高质量设置？\",\n    \"ts:rendereriniterror\": \"无法初始化 WebGL 渲染器\",\n    \"ts:guiinitfserror\": \"从浏览器存储中读取数据失败。请刷新页面。如果问题仍然存在，请清空浏览器缓存和站点数据。\",\n    \"ts:guiinitunknownerror\": \"初始化用户界面时发生未知错误\",\n    \"ts:modloaderror\": \"无法加载此MOD。请联系MOD作者。\",\n    \"gui:notready\": \"未准备就绪\",\n    \"stt:notready\": \"通知房主您尚未准备好开始游戏\",\n    \"ts:importmap\": \"自定义地图...\",\n    \"stt:importmap\": \"从本地文件系统添加自定义地图文件\",\n    \"ts:importmaperror\": \"无法导入地图。\",\n    \"ts:filenameerror\": \"文件名包含受限字符，无法保存\",\n    \"ts:importmapunsupportedtype\": \"不受支持的文件类型。\\n\\n支持的地图类型：%s\",\n    \"ts:importmapduplicateerror\": \"地图\\\"%s\\\"已存在。\",\n    \"ts:sortnone\": \"-\",\n    \"ts:sortname\": \"地图名称\",\n    \"ts:sortmaxslots\": \"最大位置数\",\n    \"stt:sortby\": \"按选择的标准对可用地图进行排序\",\n    \"ts:storage_quota_warning\": \"井告：浏览器存储已满，可能无法保存新的回放（已使用 %d MiB，总大小 %d MiB）。请检查浏览器存储配额，确保您有足够的磁盘空间，并且未在隐私模式下浏览。\",\n    \"gui:storage\": \"存储\",\n    \"gui:storageused\": \"已使用 %s / %s\",\n    \"gui:uploading\": \"正在上传\\\"%s\\\"...\",\n    \"gui:uploadfinished\": \"上传完成。\",\n    \"gui:uploadfailed\": \"上传失败。\",\n    \"gui:uploadfailedquota\": \"上传失败。存储配额已满。\",\n    \"gui:confirmdeletefiles\": \"您确定要永久删除这 %d 个项目吗？\",\n    \"gui:confirmdeletefile\": \"您确定要永久删除此项吗？\",\n    \"gui:confirmdeletesystemfile\": \"项目\\\"%s\\\"是系统文件或文件夹。如果删除它，游戏可能无法正常工作，您将不得不重新定位原始游戏文件。\\n\\n您确定要继续吗？\",\n    \"gui:exitandreload\": \"退出并重新加载\",\n    \"gui:confirmoverwritefile\": \"目标文件夹\\\"%s\\\"已经包含文件\\\"%s\\\"。\\n\\n您确定要覆盖它吗？\",\n    \"gui:yestoall\": \"全部是\",\n    \"gui:creatingarchive\": \"正在创建 ZIP 归档...\\n\\n这可能需要一分钟的时间。\",\n    \"gui:downloadfinished\": \"下载完成。\",\n    \"gui:downloadfailed\": \"下载失败。\",\n    \"gui:downloadaborted\": \"下载中止。\",\n    \"gui:newfolderprompt\": \"选择文件夹名称：\",\n    \"gui:newfoldernotallowed\": \"无法在此处创建新文件夹。\",\n    \"gui:newfolderexists\": \"已存在同名条目。\",\n    \"gui:newfolderinvalidname\": \"无效的文件夹名称。\",\n    \"ts:gameres_download_url\": \"完全体副本地址\",\n    \"ts:gameres_download_button\": \"点此自动导入\",\n    \"ts:region\": \"大区\",\n    \"gui:draws\": \"平局：\",\n    \"wol:matchmodeunavail\": \"当前游戏模式不可用\",\n    \"ts:gameres_invalid_url\": \"请输入有效的URL。\",\n    \"ts:gameres_insecure_url\": \"必须输入安全的URL（HTTPS）。\",\n    \"gui:profilemmr\": \"MMR：\",\n    \"gui:mmr\": \"MMR\",\n    \"gui:profilebonuspool\": \"奖励池：\",\n    \"gui:ladderplacement\": \"完成 %d 场排位赛以确定你的初始排名。\",\n    \"gui:gameid\": \"游戏ID\",\n    \"gui:destroyablebridges\": \"可摧毁桥梁\",\n    \"stt:destroyablebridges\": \"桥梁可以通过强制射击来摧毁。\",\n    \"gui:nodogengikills\": \"狗不能杀死工程师\",\n    \"stt:nodogengikills\": \"狗将无法杀死工程师。\",\n    \"stt:multiengineer\": \"占领敌方建筑需要%d名工程师而不是一名。\",\n    \"ts:chatrestricted\": \"您目前不能在公共游戏中发言。\",\n    \"gui:gameresultwaiting\": \"等待服务器结果...\",\n    \"gui:gameresultvictory\": \"胜利！\",\n    \"gui:gameresultdefeat\": \"失败！\",\n    \"gui:gameresultdraw\": \"平局！\",\n    \"gui:profileprovmmr\": \"临时 MMR：\",\n    \"gui:ladderpromoprogress\": \"进度：\",\n    \"gui:ladderdivision\": \"段位：%s\",\n    \"gui:ladderseasoninfo\": \"赛季信息\",\n    \"gui:laddertoptierstart\": \"将军起始：\",\n    \"gui:laddertoptierpromotions\": \"将军晋升：\",\n    \"gui:laddertoptierdemotions\": \"将军降级：\",\n    \"gui:ladderseasonlock\": \"赛季锁定：\",\n    \"gui:ladderrankedplayers\": \"排位玩家：%d\",\n    \"gui:laddertype1v1\": \"1v1\",\n    \"gui:laddertype2v2\": \"2v2\",\n    \"gui:laddertype2v2random\": \"2v2 随机\",\n    \"gui:laddertype2v2arranged\": \"2v2 组队\",\n    \"gui:teamno\": \"队伍 %s\",\n    \"gui:hostnomapupload\": \"由于新账号不允许上传自定义地图，无法传输地图。\",\n    \"ts:mapdownloadfailed\": \"下载地图 %s 失败\",\n    \"ts:mapunsupportedterrain\": \"所选地图使用了不受支持的地形（%s）。\",\n    \"GUI:OKAY\": \"确定\",\n    \"GUI:LoadingFileExplorer\": \"正在加载文件浏览器...\",\n    \"GUI:AlmostReady\": \"几乎准备就绪...\"\n}\n"
  },
  {
    "path": "public/servers.ini",
    "content": "[am-eu]\nlabel=\"Americas & Europe\"\navailable=yes\ngameVersion=0.65.1\nwolUrl=\"wss://wol-eu1.chronodivide.com\"\napiRegUrl=\"https://wol-eu1.chronodivide.com/register\"\nwladderUrl=\"https://wol-eu1.chronodivide.com/ladder\"\nwgameresUrl=\"https://wol-eu1.chronodivide.com/wgameres\"\nwolKeepAliveInGame=yes\n\n[sea]\nlabel=\"South-East Asia\"\navailable=yes\ngameVersion=0.65.1\nwolUrl=\"wss://wol-sea1.chronodivide.com\"\napiRegUrl=\"https://wol-sea1.chronodivide.com/register\"\nwladderUrl=\"https://wol-sea1.chronodivide.com/ladder\"\nwgameresUrl=\"https://wol-sea1.chronodivide.com/wgameres\"\nwolKeepAliveInGame=yes"
  },
  {
    "path": "src/App.tsx",
    "content": "import { useEffect, useRef, useState } from 'react';\nimport { Application, SplashScreenUpdateCallback } from './Application';\nimport SplashScreenComponent from './gui/component/SplashScreen';\nimport type { ComponentProps } from 'react';\nfunction App() {\n    const appRef = useRef<Application | null>(null);\n    const appInitialized = useRef<boolean>(false);\n    const [splashScreenProps, setSplashScreenProps] = useState<ComponentProps<typeof SplashScreenComponent> | null>(null);\n    const [showTestMode, setShowTestMode] = useState(false);\n    useEffect(() => {\n        if (appInitialized.current) {\n            return;\n        }\n        appInitialized.current = true;\n        console.log('App.tsx: useEffect - Initializing Application');\n        const handleSplashScreenUpdate: SplashScreenUpdateCallback = (props) => {\n            console.log('App.tsx: SplashScreen update callback received', props);\n            if (props === null) {\n                setSplashScreenProps(null);\n            }\n            else {\n                setSplashScreenProps(prevProps => ({\n                    ...prevProps,\n                    ...props\n                }));\n            }\n        };\n        const app = new Application(handleSplashScreenUpdate);\n        appRef.current = app;\n        const startApp = async () => {\n            if (document.getElementById('ra2web-root')) {\n                console.log('App.tsx: #ra2web-root found, calling app.main()');\n                try {\n                    await app.main();\n                    console.log('App.tsx: app.main() completed.');\n                }\n                catch (error) {\n                    console.error(\"Error running Application.main():\", error);\n                }\n            }\n            else {\n                console.warn('App.tsx: #ra2web-root not found yet, retrying...');\n                setTimeout(startApp, 100);\n            }\n        };\n        const urlParams = new URLSearchParams(window.location.search);\n        if (urlParams.get('test') === 'glsl') {\n            setShowTestMode(true);\n            return;\n        }\n        if (document.readyState === 'complete' || document.readyState === 'interactive') {\n            startApp();\n        }\n        else {\n            document.addEventListener('DOMContentLoaded', startApp);\n        }\n        return () => {\n            console.log('App.tsx: useEffect cleanup');\n            setSplashScreenProps(null);\n        };\n    }, []);\n    if (showTestMode) {\n        return (<div className=\"App\">\n        <div style={{\n                position: 'fixed',\n                top: '10px',\n                right: '10px',\n                zIndex: 1000\n            }}>\n          <button onClick={() => {\n                window.location.href = window.location.pathname;\n            }} style={{\n                background: '#6c757d',\n                color: 'white',\n                border: 'none',\n                padding: '8px 16px',\n                borderRadius: '4px',\n                cursor: 'pointer'\n            }}>\n            返回正常模式\n          </button>\n        </div>\n      </div>);\n    }\n    return (<div className=\"App\">\n      {splashScreenProps && splashScreenProps.parentElement && (<SplashScreenComponent {...splashScreenProps}/>)}\n    </div>);\n}\nexport default App;\n"
  },
  {
    "path": "src/Application.ts",
    "content": "import { BoxedVar } from './util/BoxedVar';\nimport { EventDispatcher } from './util/event';\nimport { Routing } from './util/Routing';\nimport { Config } from './Config';\nimport { IniFile } from './data/IniFile';\nimport { IniSection } from './data/IniSection';\nimport SplashScreenComponent from './gui/component/SplashScreen';\nimport type { ComponentProps } from 'react';\nimport { CsfFile, CsfLanguage, csfLocaleMap } from './data/CsfFile';\nimport { Strings } from './data/Strings';\nimport { VirtualFile } from './data/vfs/VirtualFile';\nimport { DataStream } from './data/DataStream';\nimport { version as appVersion } from './version';\nimport { MixFile } from './data/MixFile';\nimport { GameRes } from './engine/gameRes/GameRes';\nimport { GameResConfig } from './engine/gameRes/GameResConfig';\nimport { GameResSource } from './engine/gameRes/GameResSource';\nimport { LocalPrefs, StorageKey } from './LocalPrefs';\nimport type { Viewport, ViewportRect } from './gui/Viewport';\nimport { Gui } from './Gui';\nimport { BasicErrorBoxApi } from './gui/component/BasicErrorBoxApi';\nimport { Engine } from './engine/Engine';\nimport { ResourceLoader } from './engine/ResourceLoader';\nimport { ImageContext } from './gui/component/ImageContext';\nimport { ConsoleVars } from './ConsoleVars';\nimport { GeneralOptions } from './gui/screen/options/GeneralOptions';\nimport { FullScreen } from './gui/FullScreen';\nimport { browserFileSystemAccess } from './engine/gameRes/browserFileSystemAccess';\nimport type { TestToolRuntimeContext } from './tools/TestToolSupport';\nimport { attachPerformanceOptions, installPerformanceDebugApi } from './performance/PerformanceRuntime';\n\nconst optionalDevModuleImporters: Record<string, () => Promise<any>> = {\n    './tools/VxlTester': () => import('./tools/VxlTester'),\n    './tools/LobbyFormTester': () => import('./tools/LobbyFormTester'),\n    './tools/SoundTester': () => import('./tools/SoundTester'),\n    './tools/BuildingTester': () => import('./tools/BuildingTester'),\n    './tools/InfantryTester': () => import('./tools/InfantryTester'),\n    './tools/AircraftTester': () => import('./tools/AircraftTester'),\n    './tools/VehicleTester': () => import('./tools/VehicleTester'),\n    './tools/TestToolSupport': () => import('./tools/TestToolSupport'),\n    './tools/ShpTester': () => import('./tools/ShpTester'),\n    './tools/WorldSceneTester': () => import('./tools/WorldSceneTester'),\n    './tools/UnitMovementTester': () => import('./tools/UnitMovementTester'),\n    './tools/PerformanceTester': () => import('./tools/PerformanceTester'),\n    './tools/LiveInteractionTester': () => import('./tools/LiveInteractionTester'),\n};\n\nexport type SplashScreenUpdateCallback = (props: ComponentProps<typeof SplashScreenComponent> | null) => void;\nclass MockLocalPrefs extends LocalPrefs {\n    constructor(storage: Storage) {\n        super(storage);\n        console.log('MockLocalPrefs initialized');\n    }\n}\nclass MockConsoleVars extends ConsoleVars {\n    constructor() {\n        super();\n        console.log('MockConsoleVars initialized');\n    }\n}\nclass MockDevToolsApi {\n    static registerCommand(name: string, cmd: Function) { console.log(`MockDevToolsApi: registerCommand ${name}`); }\n    static registerVar(name: string, bv: BoxedVar<any>) { console.log(`MockDevToolsApi: registerVar ${name}`); }\n    static listCommands(): string[] { return []; }\n    static listVars(): string[] { return []; }\n}\nconst mockSentry = {\n    captureException: (e: any) => console.error(\"Sentry Mock: captureException\", e),\n    configureScope: (cb: Function) => cb({ setTag: () => { }, setExtra: () => { } }),\n};\nclass ViewportAdapter implements Viewport {\n    constructor(private boxedVar: BoxedVar<ViewportRect>) { }\n    get value(): ViewportRect {\n        return this.boxedVar.value;\n    }\n    getValue(): ViewportRect {\n        return this.boxedVar.value;\n    }\n    rootElement?: HTMLElement;\n}\nexport class Application {\n    private static readonly MOBILE_BASE_VIEWPORT = { width: 800, height: 600 };\n    private static readonly MIN_DESKTOP_VIEWPORT = { width: 800, height: 600 };\n    private async importOptionalDevModule<T = any>(path: string): Promise<T> {\n        const importer = optionalDevModuleImporters[path];\n        if (!importer) {\n            throw new Error(`Unknown optional dev module: ${path}`);\n        }\n        return importer();\n    }\n    private formatString(template: string, ...args: any[]): string {\n        if (!args || args.length === 0)\n            return template;\n        let result = template;\n        for (let i = 0; i < args.length; i++) {\n            const placeholder = new RegExp(`%s|%d`, 'i');\n            result = result.replace(placeholder, String(args[i]));\n        }\n        return result;\n    }\n    public viewport: BoxedVar<ViewportRect>;\n    private viewportAdapter: ViewportAdapter;\n    public config!: Config;\n    private strings!: Strings;\n    private localPrefs: LocalPrefs;\n    private rootEl: HTMLElement | null = null;\n    private runtimeVars: MockConsoleVars;\n    private fullScreen: FullScreen;\n    private generalOptions: GeneralOptions;\n    public routing: Routing;\n    private sentry: typeof mockSentry | undefined = mockSentry;\n    private currentLocale: string = 'en-US';\n    private fsAccessLib: any;\n    private gameResConfig: GameResConfig | undefined;\n    private cdnResourceLoader: any;\n    private gpuTier: any;\n    private splashScreenUpdateCallback?: SplashScreenUpdateCallback;\n    private gui?: Gui;\n    private preferredViewportSize?: {\n        width: number;\n        height: number;\n    } | null;\n    private hasLoadedGeneralOptionsFromStorage: boolean = false;\n    private readonly handleViewportEnvironmentChange = () => this.updateViewportSize();\n    private readonly handlePreferredResolutionChange = (resolution?: {\n        width: number;\n        height: number;\n    }) => this.setPreferredViewportSize(resolution);\n    private readonly handleForceResolutionChange = (value?: string) => {\n        if (!value?.match(/^\\d+x\\d+$/)) {\n            return;\n        }\n        const [width, height] = value.split('x').map(Number);\n        this.setPreferredViewportSize({ width, height });\n    };\n    constructor(splashScreenUpdateCallback?: SplashScreenUpdateCallback) {\n        this.viewport = new BoxedVar({ x: 0, y: 0, width: window.innerWidth, height: window.innerHeight });\n        this.viewportAdapter = new ViewportAdapter(this.viewport);\n        this.routing = new Routing();\n        this.splashScreenUpdateCallback = splashScreenUpdateCallback;\n        this.localPrefs = new MockLocalPrefs(localStorage);\n        this.runtimeVars = new MockConsoleVars();\n        this.generalOptions = new GeneralOptions();\n        this.loadGeneralOptions();\n        this.bindPerformanceRuntimeVars();\n        this.fullScreen = new FullScreen(document);\n        this.updateViewportSize();\n        console.log('Application constructor finished.');\n    }\n    public getVersion(): string {\n        return appVersion;\n    }\n    private async loadConfig(): Promise<void> {\n        console.log('[Application] Attempting to load config.ini...');\n        try {\n            const response = await fetch('/config.ini');\n            if (!response.ok) {\n                throw new Error(`Failed to fetch config.ini: ${response.status} ${response.statusText}`);\n            }\n            const iniString = await response.text();\n            const iniFileInstance = new IniFile(iniString);\n            this.config = new Config();\n            this.config.load(iniFileInstance);\n            console.log('[Application] config.ini loaded and parsed successfully.');\n            console.log('[Application] Config object dump:', this.config);\n            console.log('[Application] Verification: Default Locale from config:', this.config.defaultLocale);\n            console.log('[Application] Verification: Viewport Width from config:', this.config.viewport.width);\n            console.log('[Application] Verification: Dev Mode from config:', this.config.devMode);\n            console.log('[Application] Verification: Servers URL from config:', this.config.serversUrl);\n        }\n        catch (error) {\n            console.error('[Application] Failed to parse config:', error);\n            console.error('[Application] Config parsing failed. Using minimal defaults.');\n            this.config = new Config();\n            console.error(\"Failed to load application configuration (config.ini). Using minimal defaults. Some features may not work.\");\n        }\n    }\n    private async loadTranslations(): Promise<void> {\n        const currentConfig = this.config;\n        if (!currentConfig) {\n            console.error(\"[Application] Config not loaded before loadTranslations. Skipping.\");\n            this.strings = new Strings();\n            this.currentLocale = 'en-US';\n            return;\n        }\n        let csfFileValue = currentConfig.getGeneralData().get('csfFile') || 'ra2/general.csf';\n        const csfFileName = Array.isArray(csfFileValue) ? csfFileValue[0] : csfFileValue;\n        console.log(`[Application] Attempting to load CSF file: ${csfFileName}`);\n        try {\n            const csfResponse = await fetch(`/${csfFileName}`);\n            if (!csfResponse.ok) {\n                throw new Error(`Failed to fetch CSF file ${csfFileName}: ${csfResponse.status} ${csfResponse.statusText}`);\n            }\n            const arrayBuffer = await csfResponse.arrayBuffer();\n            const dataStream = new DataStream(arrayBuffer, 0, DataStream.LITTLE_ENDIAN);\n            dataStream.dynamicSize = false;\n            const virtualFile = new VirtualFile(dataStream, csfFileName);\n            const csfFileInstance = new CsfFile(virtualFile);\n            this.strings = new Strings(csfFileInstance);\n            this.currentLocale = csfFileInstance.getIsoLocale() || currentConfig.defaultLocale;\n            console.log(`[Application] CSF file \"${csfFileName}\" loaded. Detected/Set Locale: ${this.currentLocale}. Loaded ${Object.keys(this.strings.getKeys()).length} keys from CSF.`);\n        }\n        catch (error) {\n            console.error(`[Application] Failed to load or parse CSF file \"${csfFileName}\":`, error);\n            console.warn('[Application] Falling back to empty Strings object for CSF part.');\n            this.strings = new Strings();\n            this.currentLocale = currentConfig.defaultLocale;\n        }\n        const jsonLocaleFile = `res/locale/${this.currentLocale}.json?v=${this.getVersion()}`;\n        console.log(`[Application] Attempting to load JSON locale file: ${jsonLocaleFile}`);\n        try {\n            const jsonResponse = await fetch(`/${jsonLocaleFile}`);\n            if (!jsonResponse.ok) {\n                throw new Error(`Failed to fetch JSON locale ${jsonLocaleFile}: ${jsonResponse.status} ${jsonResponse.statusText}`);\n            }\n            const jsonData = await jsonResponse.json();\n            if (jsonData) {\n                this.strings.fromJson(jsonData);\n                console.log(`[Application] JSON locale file \"${jsonLocaleFile}\" loaded and merged. Total keys now: ${Object.keys(this.strings.getKeys()).length}.`);\n            }\n            else {\n                console.warn(`[Application] JSON locale file \"${jsonLocaleFile}\" parsed to null or undefined data.`);\n            }\n        }\n        catch (error) {\n            console.error(`[Application] Failed to load or parse JSON locale file \"${jsonLocaleFile}\":`, error);\n            console.warn(`[Application] Continuing without strings from ${jsonLocaleFile}.`);\n        }\n        console.log('[Application] Translations loading finished. Final locale: ', this.currentLocale);\n        console.log('[Application] Sample string GUI:OKAY ->', this.strings.get('GUI:OKAY'));\n        console.log('[Application] Sample string GUI:Cancel ->', this.strings.get('GUI:Cancel'));\n        console.log('[Application] Sample string GUI:LoadingEx ->', this.strings.get('GUI:LoadingEx'));\n        console.log('[Application] First 20 keys in Strings:', this.strings.getKeys().slice(0, 20));\n    }\n    private checkGlobalLibs(): void {\n        console.log('[MVP] Skipping Application.checkGlobalLibs().');\n    }\n    private async initLogging(): Promise<void> {\n        console.log('[MVP] Skipping Application.initLogging().');\n    }\n    private loadGeneralOptions(): void {\n        const optionsData = this.localPrefs.getItem(StorageKey.Options);\n        if (optionsData) {\n            try {\n                this.generalOptions.unserialize(optionsData);\n                this.hasLoadedGeneralOptionsFromStorage = true;\n                console.log('[Application] Loaded general options from local storage');\n            }\n            catch (error) {\n                console.warn('[Application] Failed to read general options from local storage', error);\n            }\n        }\n    }\n    private initializePreferredViewportSize(): void {\n        if (!this.hasLoadedGeneralOptionsFromStorage && this.generalOptions.graphics.resolution.value === undefined) {\n            const defaultViewportSize = {\n                width: this.config?.viewport?.width ?? 1024,\n                height: this.config?.viewport?.height ?? 768,\n            };\n            this.generalOptions.graphics.resolution.value = defaultViewportSize;\n            this.preferredViewportSize = defaultViewportSize;\n            console.log('[Application] Initialized preferred viewport size from config defaults', defaultViewportSize);\n            return;\n        }\n        this.preferredViewportSize = this.generalOptions.graphics.resolution.value\n            ? { ...this.generalOptions.graphics.resolution.value }\n            : null;\n        console.log('[Application] Initialized preferred viewport size from options', this.preferredViewportSize);\n    }\n    private bindPerformanceRuntimeVars(): void {\n        const performanceOptions = this.generalOptions.performance;\n        this.runtimeVars.perfRaycastHelperReuse = performanceOptions.raycastHelperReuse;\n        this.runtimeVars.perfEntityIntersectTraversal = performanceOptions.entityIntersectTraversal;\n        this.runtimeVars.perfMapTileHitTest = performanceOptions.mapTileHitTest;\n        this.runtimeVars.perfWorldViewportCache = performanceOptions.worldViewportCache;\n        this.runtimeVars.perfWorldSoundLoopCache = performanceOptions.worldSoundLoopCache;\n        this.runtimeVars.perfTelemetry = performanceOptions.telemetry;\n        attachPerformanceOptions(performanceOptions);\n        const debugRoot = ((window as any).__ra2debug ??= {});\n        debugRoot.generalOptions = this.generalOptions;\n        debugRoot.runtimeVars = this.runtimeVars;\n        installPerformanceDebugApi(debugRoot);\n    }\n    private setPreferredViewportSize(resolution?: {\n        width: number;\n        height: number;\n    }): void {\n        this.preferredViewportSize = resolution ? { ...resolution } : null;\n        console.log('[Application] setPreferredViewportSize', this.preferredViewportSize ?? 'fit-window');\n        this.updateViewportSize(this.fullScreen.isFullScreen());\n    }\n    private getRequestedResolution(): { width: number; height: number; } | undefined {\n        if (this.preferredViewportSize === undefined) {\n            return this.generalOptions.graphics.resolution.value;\n        }\n        return this.preferredViewportSize ?? undefined;\n    }\n    private isMobileLayout(): boolean {\n        return !!window.matchMedia?.('(pointer: coarse)')?.matches;\n    }\n    private getAvailableDisplaySize(): { width: number; height: number; } {\n        const viewport = window.visualViewport;\n        const width = Math.floor(viewport?.width ?? window.innerWidth ?? document.documentElement.clientWidth ?? Application.MOBILE_BASE_VIEWPORT.width);\n        const height = Math.floor(viewport?.height ?? window.innerHeight ?? document.documentElement.clientHeight ?? Application.MOBILE_BASE_VIEWPORT.height);\n        return {\n            width: Math.max(1, width),\n            height: Math.max(1, height),\n        };\n    }\n    private normalizeViewportDimension(value: number, minimum: number): number {\n        const normalized = Math.max(minimum, Math.floor(value));\n        return normalized - (normalized % 2);\n    }\n    private computeDesktopViewportSize(availableSize: {\n        width: number;\n        height: number;\n    }): {\n        width: number;\n        height: number;\n    } {\n        if (this.preferredViewportSize === null) {\n            return {\n                width: this.normalizeViewportDimension(availableSize.width, Application.MIN_DESKTOP_VIEWPORT.width),\n                height: this.normalizeViewportDimension(availableSize.height, Application.MIN_DESKTOP_VIEWPORT.height),\n            };\n        }\n        const requestedResolution = this.getRequestedResolution();\n        const defaultWidth = this.config?.viewport?.width ?? 1024;\n        const defaultHeight = this.config?.viewport?.height ?? 768;\n        const targetWidth = requestedResolution?.width ?? defaultWidth;\n        const targetHeight = requestedResolution?.height ?? defaultHeight;\n        return {\n            width: this.normalizeViewportDimension(Math.min(availableSize.width, targetWidth), Application.MIN_DESKTOP_VIEWPORT.width),\n            height: this.normalizeViewportDimension(Math.min(availableSize.height, targetHeight), Application.MIN_DESKTOP_VIEWPORT.height),\n        };\n    }\n    private computeViewportSize(isFullScreen: boolean, availableSize: { width: number; height: number; }, mobileLayout: boolean): {\n        width: number;\n        height: number;\n    } {\n        if (isFullScreen) {\n            if (mobileLayout) {\n                const requestedResolution = this.getRequestedResolution();\n                const mobileWidth = requestedResolution?.width ?? Application.MOBILE_BASE_VIEWPORT.width;\n                const mobileHeight = requestedResolution?.height ?? Application.MOBILE_BASE_VIEWPORT.height;\n                const availableAspect = availableSize.width / availableSize.height;\n                const mobileAspect = mobileWidth / mobileHeight;\n                let width: number;\n                let height: number;\n                if (availableAspect > mobileAspect) {\n                    height = Math.max(mobileHeight, availableSize.height);\n                    width = Math.round(height * availableAspect);\n                } else {\n                    width = Math.max(mobileWidth, availableSize.width);\n                    height = Math.round(width / availableAspect);\n                }\n                return {\n                    width: this.normalizeViewportDimension(width, Application.MOBILE_BASE_VIEWPORT.width),\n                    height: this.normalizeViewportDimension(height, Application.MOBILE_BASE_VIEWPORT.height),\n                };\n            }\n            return {\n                width: this.normalizeViewportDimension(availableSize.width, 2),\n                height: this.normalizeViewportDimension(availableSize.height, 2),\n            };\n        }\n        if (mobileLayout) {\n            const requestedResolution = this.getRequestedResolution();\n            const mobileWidth = requestedResolution?.width ?? Application.MOBILE_BASE_VIEWPORT.width;\n            const mobileHeight = requestedResolution?.height ?? Application.MOBILE_BASE_VIEWPORT.height;\n            return {\n                width: this.normalizeViewportDimension(mobileWidth, Application.MOBILE_BASE_VIEWPORT.width),\n                height: this.normalizeViewportDimension(mobileHeight, Application.MOBILE_BASE_VIEWPORT.height),\n            };\n        }\n        return this.computeDesktopViewportSize(availableSize);\n    }\n    private computeViewportLayout(isFullScreen: boolean): ViewportRect {\n        const availableSize = this.getAvailableDisplaySize();\n        const mobileLayout = this.isMobileLayout();\n        const logicalSize = this.computeViewportSize(isFullScreen, availableSize, mobileLayout);\n        const scale = Math.min(1, availableSize.width / logicalSize.width, availableSize.height / logicalSize.height);\n        return {\n            x: 0,\n            y: 0,\n            width: logicalSize.width,\n            height: logicalSize.height,\n            displayWidth: Math.max(1, Math.round(logicalSize.width * scale)),\n            displayHeight: Math.max(1, Math.round(logicalSize.height * scale)),\n            scale,\n            isMobileLayout: mobileLayout,\n            isPortrait: availableSize.height > availableSize.width,\n        };\n    }\n    private applyRootLayout(viewport: ViewportRect): void {\n        if (!this.rootEl) {\n            return;\n        }\n        this.rootEl.style.width = `${viewport.width}px`;\n        this.rootEl.style.height = `${viewport.height}px`;\n        this.rootEl.style.transform = viewport.scale && viewport.scale < 1 ? `scale(${viewport.scale})` : '';\n        this.rootEl.style.transformOrigin = 'center center';\n        this.rootEl.style.willChange = 'transform';\n        this.rootEl.dataset.mobileLayout = String(Boolean(viewport.isMobileLayout));\n        this.rootEl.dataset.orientation = viewport.isPortrait ? 'portrait' : 'landscape';\n        this.rootEl.dataset.compactLayout = String(viewport.height <= 640 || viewport.width <= 800);\n    }\n    private isNativeFullScreen(): boolean {\n        const width = window.innerWidth ?? 0;\n        const height = window.innerHeight ?? 0;\n        return width >= screen.width && height >= screen.height;\n    }\n    private updateViewportSize(isFullScreen: boolean = this.fullScreen.isFullScreen() || this.isNativeFullScreen()): void {\n        const nextViewport = this.computeViewportLayout(isFullScreen);\n        this.viewport.value = nextViewport;\n        this.applyRootLayout(nextViewport);\n        console.log('[Application] updateViewportSize', {\n            viewport: `${nextViewport.width}x${nextViewport.height}`,\n            display: `${nextViewport.displayWidth}x${nextViewport.displayHeight}`,\n            scale: nextViewport.scale,\n            fullScreen: isFullScreen,\n            mobileLayout: nextViewport.isMobileLayout,\n            portrait: nextViewport.isPortrait,\n        });\n    }\n    private onFullScreenChange(isFullScreen: boolean): void {\n        console.log(`[Application] onFullScreenChange: ${isFullScreen}`);\n        this.updateViewportSize(isFullScreen);\n    }\n    private async loadGpuBenchmarkData(): Promise<any> {\n        console.log('[MVP] Skipping Application.loadGpuBenchmarkData()');\n        return { tier: 1, type: 'MOCK_GPU' };\n    }\n    public async main(): Promise<void> {\n        console.log('Application.main() called');\n        this.rootEl = document.getElementById(\"ra2web-root\");\n        if (!this.rootEl) {\n            console.error(\"CRITICAL: Missing root element #ra2web-root in HTML.\");\n            const errorMsg = \"CRITICAL: Missing root element #ra2web-root for the application.\";\n            if (document.body) {\n                document.body.innerHTML = `<h1>Error</h1><p>${errorMsg}</p>`;\n            }\n            else {\n                alert(errorMsg);\n            }\n            return;\n        }\n        this.viewportAdapter.rootElement = this.rootEl;\n        this.applyRootLayout(this.viewport.value);\n        if (this.splashScreenUpdateCallback) {\n            this.splashScreenUpdateCallback({\n                width: this.viewport.value.width,\n                height: this.viewport.value.height,\n                parentElement: this.rootEl,\n                loadingText: 'Initializing...'\n            });\n        }\n        try {\n            await this.loadConfig();\n            this.initializePreferredViewportSize();\n            this.updateViewportSize();\n        }\n        catch (e) {\n            console.error(\"CRITICAL: Application.loadConfig() failed. See previous errors.\", e);\n            if (this.rootEl)\n                this.rootEl.innerHTML = \"<h1>Error</h1><p>Failed to load critical application configuration. Please check console.</p>\";\n            return;\n        }\n        const locale = this.config.defaultLocale;\n        if (this.splashScreenUpdateCallback) {\n            this.splashScreenUpdateCallback({\n                width: this.viewport.value.width,\n                height: this.viewport.value.height,\n                parentElement: this.rootEl,\n                loadingText: 'Loading translations...'\n            });\n        }\n        try {\n            await this.loadTranslations();\n        }\n        catch (e) {\n            console.error(`Missing translation ${locale}.`, e);\n            return;\n        }\n        if (this.splashScreenUpdateCallback) {\n            this.splashScreenUpdateCallback({\n                width: this.viewport.value.width,\n                height: this.viewport.value.height,\n                parentElement: this.rootEl,\n                loadingText: this.strings.get(\"gui:loadingex\"),\n                copyrightText: this.strings.get(\"txt_copyright\") + \"\\n\" + this.strings.get(\"gui:wwbrand\"),\n                disclaimerText: this.strings.get(\"ts:disclaimer\")\n            });\n        }\n        try {\n            this.checkGlobalLibs();\n        }\n        catch (e: any) {\n            console.error(\"Global library check failed:\", e);\n            const errorMsg = this.strings.get(\"TS:DownloadFailed\");\n            const errorBox = new BasicErrorBoxApi(this.viewport, this.strings, this.rootEl!);\n            await errorBox.show(errorMsg, true);\n            return;\n        }\n        this.runtimeVars = new MockConsoleVars();\n        this.bindPerformanceRuntimeVars();\n        MockDevToolsApi.registerVar(\"freecamera\", this.runtimeVars.freeCamera);\n        await this.initLogging();\n        this.fullScreen.init();\n        this.fullScreen.onChange.subscribe((isFS: boolean) => {\n            this.onFullScreenChange(isFS);\n        });\n        this.runtimeVars.forceResolution.onChange.subscribe(this.handleForceResolutionChange);\n        if (typeof window !== 'undefined') {\n            window.addEventListener('resize', this.handleViewportEnvironmentChange);\n            window.addEventListener('orientationchange', this.handleViewportEnvironmentChange);\n            window.visualViewport?.addEventListener('resize', this.handleViewportEnvironmentChange);\n            this.generalOptions.graphics.resolution.onChange.subscribe(this.handlePreferredResolutionChange);\n            this.updateViewportSize();\n        }\n        this.loadGpuBenchmarkData()\n            .then(gpuData => this.gpuTier = gpuData)\n            .catch(e => this.sentry?.captureException(e));\n        this.fsAccessLib = browserFileSystemAccess;\n        const urlParams = new URLSearchParams(window.location.search);\n        const modName = urlParams.get('mod');\n        let gameResConfig = this.loadGameResConfig(this.localPrefs);\n        try {\n            const gameRes = new GameRes(this.getVersion(), modName || undefined, this.fsAccessLib, this.localPrefs, this.strings, this.rootEl, this.createSplashScreenInterface(), this.viewportAdapter, this.config, \"res/\", this.sentry);\n            const { configToPersist, cdnResLoader } = await gameRes.init(gameResConfig, (error, strings) => this.handleGameResLoadError(error, strings), (error, strings) => this.handleGameResImportError(error, strings));\n            try {\n                const vfsAny: any = (Engine as any).vfs;\n                if (vfsAny?.debugListFileOwners) {\n                    vfsAny.debugListFileOwners('rules.ini');\n                    vfsAny.debugListFileOwners('art.ini');\n                    vfsAny.debugListFileOwners('rulescd.ini');\n                    vfsAny.debugListFileOwners('artcd.ini');\n                }\n                try {\n                    const rulesFile = (Engine as any).vfs.openFile('rules.ini');\n                    const rulesHead = rulesFile.readAsString().split(/\\r?\\n/).slice(0, 40);\n                    console.log('[Diag] rules.ini head (first 40 lines):', rulesHead);\n                }\n                catch (e) {\n                    console.warn('[Diag] Failed to read rules.ini head:', e);\n                }\n                try {\n                    const rulesCdFile = (Engine as any).vfs.openFile('rulescd.ini');\n                    const rulesCdHead = rulesCdFile.readAsString().split(/\\r?\\n/).slice(0, 40);\n                    console.log('[Diag] rulescd.ini head (first 40 lines):', rulesCdHead);\n                }\n                catch (e) {\n                    console.warn('[Diag] Failed to read rulescd.ini head:', e);\n                }\n            }\n            catch (e) {\n                console.warn('[Diag] VFS ownership diagnostics failed:', e);\n            }\n            if (configToPersist) {\n                if (configToPersist.isCdn()) {\n                    this.localPrefs.removeItem(StorageKey.GameRes);\n                }\n                else {\n                    this.localPrefs.setItem(StorageKey.GameRes, configToPersist.serialize());\n                }\n                gameResConfig = configToPersist;\n            }\n            this.gameResConfig = gameResConfig;\n            this.cdnResourceLoader = cdnResLoader;\n            ImageContext.cdnBaseUrl = this.gameResConfig?.isCdn()\n                ? this.gameResConfig.getCdnBaseUrl()\n                : undefined;\n            ImageContext.vfs = Engine.vfs;\n            try {\n                console.log(\"Engine.iniFiles.has('rules.ini'):\", Engine.iniFiles.has(\"rules.ini\"));\n                console.log(\"Engine.iniFiles.has('art.ini'):\", Engine.iniFiles.has(\"art.ini\"));\n                console.log(\"[Diag] Engine.iniFiles.has('rulescd.ini'):\", Engine.iniFiles.has(\"rulescd.ini\"));\n                console.log(\"[Diag] Engine.iniFiles.has('artcd.ini'):\", Engine.iniFiles.has(\"artcd.ini\"));\n                Engine.loadRules();\n                try {\n                    const rulesIniUsed = Engine.getFileNameVariant('rules.ini');\n                    const artIniUsed = Engine.getFileNameVariant('art.ini');\n                    console.log('[Diag] Using base INIs:', { rulesIniUsed, artIniUsed });\n                    const hasAPSplashSection = !!Engine.getIni(artIniUsed).getSection('APSplash') || !!Engine.getIni(rulesIniUsed).getSection('APSplash');\n                    console.log('[Diag] APSplash section present in INIs:', hasAPSplashSection);\n                    try {\n                        const orderedSections: any[] = (Engine.getRules() as any).getOrderedSections?.() ?? [];\n                        console.log('[Diag] Merged rules - first sections:', orderedSections.slice(0, 120).map(s => s.name));\n                    }\n                    catch (e) {\n                        console.warn('[Diag] Failed to dump ordered sections from merged rules:', e);\n                    }\n                    try {\n                        const mergedRules: any = Engine.getRules();\n                        const warheads = mergedRules?.getSection?.('Warheads');\n                        const listed: string[] = [];\n                        if (warheads?.entries) {\n                            warheads.entries.forEach((v: any) => {\n                                if (typeof v === 'string')\n                                    listed.push(v);\n                                else if (Array.isArray(v))\n                                    listed.push(...v);\n                            });\n                        }\n                        console.log('[Diag] Warheads listed (sample):', listed.slice(0, 40), 'total=', listed.length);\n                    }\n                    catch (e) {\n                        console.warn('[Diag] Failed to dump Warheads list from merged rules:', e);\n                    }\n                    try {\n                        const mergedRules: any = Engine.getRules();\n                        const ObjectType: any = (Engine as any).ObjectType || (window as any).ra2web?.engine?.type?.ObjectType;\n                        const hasCdestSection = !!mergedRules.getSection?.('CDEST');\n                        console.log('[Diag] CDEST section exists in merged rules:', hasCdestSection);\n                        try {\n                            const s = mergedRules.getSection?.('CDEST');\n                            if (s?.entries) {\n                                const keys: string[] = [];\n                                s.entries.forEach((_v: any, k: string) => keys.push(k));\n                                console.log('[Diag] CDEST merged keys (sample):', keys.slice(0, 30));\n                                console.log('[Diag] CDEST Primary/Secondary/ElitePrimary/EliteSecondary:', s.get?.('Primary'), s.get?.('Secondary'), s.get?.('ElitePrimary'), s.get?.('EliteSecondary'));\n                            }\n                        }\n                        catch (e) {\n                            console.warn('[Diag] CDEST merged entries dump failed:', e);\n                        }\n                        const cdest = mergedRules.getObject?.('CDEST', ObjectType?.Vehicle ?? 2);\n                        let weaponName: string | undefined = cdest?.primary || cdest?.elitePrimary || cdest?.secondary || cdest?.eliteSecondary;\n                        if (weaponName) {\n                            const wpn = mergedRules.getWeapon(weaponName);\n                            console.log('[Diag] CDEST weapon mapping:', { weapon: wpn.name, warhead: wpn.warhead });\n                            const hasWh = !!mergedRules.getSection?.(wpn.warhead);\n                            console.log('[Diag] CDEST warhead section exists in merged rules:', hasWh);\n                        }\n                        else {\n                            console.log('[Diag] CDEST weapon mapping: no primary/secondary found');\n                        }\n                        try {\n                            const cdestSection = mergedRules.getSection?.('CDEST');\n                            const spawnsName = cdestSection?.get?.('Spawns');\n                            console.log('[Diag] CDEST Spawns:', spawnsName);\n                            if (spawnsName) {\n                                try {\n                                    const spawnedRules = mergedRules.getObject?.(spawnsName, ObjectType?.Aircraft ?? 1);\n                                    const spawnedPrimary = spawnedRules?.primary || spawnedRules?.elitePrimary || spawnedRules?.secondary || spawnedRules?.eliteSecondary;\n                                    if (spawnedPrimary) {\n                                        const spawnedWpn = mergedRules.getWeapon(spawnedPrimary);\n                                        console.log('[Diag] Spawned unit primary:', { weapon: spawnedWpn.name, warhead: spawnedWpn.warhead });\n                                    }\n                                    else {\n                                        console.log('[Diag] Spawned unit has no primary/secondary');\n                                    }\n                                }\n                                catch (e) {\n                                    console.warn('[Diag] Spawned unit probe failed:', e);\n                                }\n                            }\n                        }\n                        catch (e) {\n                            console.warn('[Diag] CDEST Spawns probe failed:', e);\n                        }\n                        try {\n                            const aswMerged = mergedRules.getSection?.('ASWLauncher');\n                            const aswMergedWh = aswMerged?.get?.('Warhead');\n                            console.log('[Diag] ASWLauncher in merged rules: warhead=', aswMergedWh);\n                        }\n                        catch (e) {\n                            console.warn('[Diag] ASWLauncher merged probe failed:', e);\n                        }\n                        try {\n                            const baseAsw = Engine.getIni('rules.ini').getSection('ASWLauncher');\n                            const baseAswWh = baseAsw?.get?.('Warhead');\n                            console.log('[Diag] ASWLauncher in base rules.ini: warhead=', baseAswWh);\n                        }\n                        catch (e) {\n                            console.warn('[Diag] ASWLauncher base probe failed:', e);\n                        }\n                        try {\n                            const apsInRulesCd = !!Engine.getIni('rulescd.ini').getSection('APSplash');\n                            const apsInArtCd = !!Engine.getIni('artcd.ini').getSection('APSplash');\n                            console.log('[Diag] APSplash in rulescd.ini:', apsInRulesCd, 'APSplash in artcd.ini:', apsInArtCd);\n                        }\n                        catch (e) {\n                            console.warn('[Diag] APSplash custom INI presence probe failed:', e);\n                        }\n                        try {\n                            const mergedHasAPSplash = !!mergedRules.getSection?.('APSplash');\n                            console.log('[Diag] APSplash section exists in merged rules:', mergedHasAPSplash);\n                        }\n                        catch (e) {\n                            console.warn('[Diag] APSplash merged presence check failed:', e);\n                        }\n                        try {\n                            const baseCdest = Engine.getIni('rules.ini').getSection('CDEST');\n                            const customCdest = Engine.getIni('rulescd.ini').getSection('CDEST');\n                            console.log('[Diag] Base CDEST present:', !!baseCdest, 'Custom CDEST present:', !!customCdest);\n                            if (baseCdest) {\n                                console.log('[Diag] Base CDEST Primary/Secondary:', baseCdest.get?.('Primary'), baseCdest.get?.('Secondary'));\n                            }\n                            if (customCdest) {\n                                console.log('[Diag] Custom CDEST Primary/Secondary:', customCdest.get?.('Primary'), customCdest.get?.('Secondary'));\n                            }\n                        }\n                        catch (e) {\n                            console.warn('[Diag] Base/Custom CDEST probe failed:', e);\n                        }\n                    }\n                    catch (e) {\n                        console.warn('[Diag] CDEST mapping diagnostics failed:', e);\n                    }\n                }\n                catch (e) {\n                    console.warn('[Diag] INI presence diagnostics failed:', e);\n                }\n                try {\n                    const baseArt = Engine.getIni('art.ini');\n                    const customArt = Engine.getIni('artcd.ini');\n                    const mergedArt = Engine.getArt();\n                    const probe = (name: string) => ({\n                        name,\n                        base: !!baseArt?.getSection(name),\n                        custom: !!customArt?.getSection(name),\n                        merged: !!mergedArt?.getSection(name),\n                    });\n                    console.log('[Diag] Art sections presence:', probe('GI'), probe('CONS'), probe('SEAL'), probe('ENGINEER'), probe('ROCK'));\n                }\n                catch (e) {\n                    console.warn('[Diag] Art presence diagnostics failed:', e);\n                }\n            }\n            catch (err) {\n                console.error('[Application] Engine.loadRules() failed:', err);\n            }\n            if (typeof window.gtag === 'function') {\n                window.gtag('event', 'app_init', {\n                    res: this.gameResConfig?.source || 'unknown',\n                    modName: modName || '<none>'\n                });\n            }\n            this.sentry?.configureScope((scope: any) => {\n                scope.setTag('mod', modName || '<none>');\n                scope.setExtra('mod', modName || '<none>');\n                let modHash: string | number = 'unknown';\n                try {\n                    modHash = Engine.getModHash();\n                }\n                catch { }\n                scope.setExtra('modHash', modHash);\n            });\n        }\n        catch (e) {\n            console.error(\"Failed to initialize GameRes:\", e);\n            await this.handleGameResLoadError(e as Error, this.strings, true);\n            return;\n        }\n        this.initRouting();\n        if (this.splashScreenUpdateCallback) {\n            this.splashScreenUpdateCallback(null);\n        }\n    }\n    private loadGameResConfig(prefs: LocalPrefs): GameResConfig | undefined {\n        const serializedConfig = prefs.getItem(StorageKey.GameRes);\n        if (serializedConfig) {\n            try {\n                const config = new GameResConfig(this.config.gameresBaseUrl || \"\");\n                config.unserialize(serializedConfig);\n                if (config.isCdn() && !config.getCdnBaseUrl()) {\n                    return undefined;\n                }\n                return config;\n            }\n            catch (e) {\n                console.error(\"Failed to load GameResConfig from preferences:\", e);\n            }\n        }\n        return undefined;\n    }\n    private createTestToolContext(): TestToolRuntimeContext {\n        return {\n            cdnResourceLoader: this.cdnResourceLoader,\n            mapResourceLoader: new ResourceLoader(this.config.mapsBaseUrl ?? ''),\n            rootElement: this.rootEl ?? undefined,\n        };\n    }\n    private initRouting(): void {\n        let currentHandler: any = null;\n        this.routing.addRoute(\"*\", async () => {\n            if (currentHandler && currentHandler.destroy) {\n                console.log('[Application] Destroying current handler');\n                await currentHandler.destroy();\n                currentHandler = null;\n            }\n        });\n        this.routing.addRoute(\"/\", async () => {\n            console.log('[Application] Initializing main page');\n            this.applyRootLayout(this.viewport.value);\n            this.gui = new Gui(this.getVersion(), this.strings, this.config, this.viewport, this.rootEl!, this.cdnResourceLoader, this.gameResConfig, this.runtimeVars, this.generalOptions, this.fullScreen);\n            await this.gui.init();\n            currentHandler = this;\n        });\n        this.routing.addRoute(\"/vxltest\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing VxlTester');\n            const { VxlTester } = await this.importOptionalDevModule('./tools/VxlTester');\n            await VxlTester.main(Engine.vfs, this.runtimeVars, this.createTestToolContext());\n            currentHandler = VxlTester;\n        });\n        this.routing.addRoute(\"/lobbytest\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing LobbyFormTester');\n            const { LobbyFormTester } = await this.importOptionalDevModule('./tools/LobbyFormTester');\n            await LobbyFormTester.main(this.rootEl!, this.strings, this.createTestToolContext());\n            currentHandler = LobbyFormTester;\n        });\n        this.routing.addRoute(\"/soundtest\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing SoundTester');\n            const { SoundTester } = await this.importOptionalDevModule('./tools/SoundTester');\n            await SoundTester.main(Engine.vfs, this.rootEl!, this.createTestToolContext());\n            currentHandler = SoundTester;\n        });\n        this.routing.addRoute(\"/buildtest\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing BuildingTester');\n            const { BuildingTester } = await this.importOptionalDevModule('./tools/BuildingTester');\n            await BuildingTester.main([], this.createTestToolContext());\n            currentHandler = BuildingTester;\n        });\n        this.routing.addRoute(\"/inftest\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing InfantryTester');\n            const { InfantryTester } = await this.importOptionalDevModule('./tools/InfantryTester');\n            await InfantryTester.main(this.runtimeVars, this.createTestToolContext());\n            currentHandler = InfantryTester;\n        });\n        this.routing.addRoute(\"/airtest\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing AircraftTester');\n            const { AircraftTester } = await this.importOptionalDevModule('./tools/AircraftTester');\n            await AircraftTester.main(this.runtimeVars, this.createTestToolContext());\n            currentHandler = AircraftTester;\n        });\n        this.routing.addRoute(\"/vehicletest\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing VehicleTester');\n            const { VehicleTester } = await this.importOptionalDevModule('./tools/VehicleTester');\n            await VehicleTester.main(this.runtimeVars, this.createTestToolContext());\n            currentHandler = VehicleTester;\n        });\n        this.routing.addRoute(\"/shptest\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing ShpTester');\n            const { TestToolSupport } = await this.importOptionalDevModule('./tools/TestToolSupport');\n            const gameMap = await TestToolSupport.loadMap(this.createTestToolContext().mapResourceLoader!, \"mp03t4.map\");\n            const { ShpTester } = await this.importOptionalDevModule('./tools/ShpTester');\n            await ShpTester.main(Engine.vfs, gameMap, this.rootEl!, this.strings, this.createTestToolContext());\n            currentHandler = ShpTester;\n        });\n        this.routing.addRoute(\"/worldscenetest\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing WorldSceneTester');\n            const { TestToolSupport } = await this.importOptionalDevModule('./tools/TestToolSupport');\n            const gameMap = await TestToolSupport.loadMap(this.createTestToolContext().mapResourceLoader!, \"mp03t4.map\");\n            const { WorldSceneTester } = await this.importOptionalDevModule('./tools/WorldSceneTester');\n            await WorldSceneTester.main(Engine.vfs, gameMap, this.rootEl!, this.strings, this.createTestToolContext());\n            currentHandler = WorldSceneTester;\n        });\n        this.routing.addRoute(\"/unitmovementtest\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing UnitMovementTester');\n            const { TestToolSupport } = await this.importOptionalDevModule('./tools/TestToolSupport');\n            const gameMap = await TestToolSupport.loadMap(this.createTestToolContext().mapResourceLoader!, \"mp03t4.map\");\n            const { UnitMovementTester } = await this.importOptionalDevModule('./tools/UnitMovementTester');\n            await UnitMovementTester.main(Engine.vfs, gameMap, this.rootEl!, this.strings, this.createTestToolContext());\n            currentHandler = UnitMovementTester;\n        });\n        this.routing.addRoute(\"/perftest\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing PerformanceTester');\n            const { PerformanceTester } = await this.importOptionalDevModule('./tools/PerformanceTester');\n            await PerformanceTester.main(this.rootEl!, this.strings, this.runtimeVars, this.generalOptions, this.createTestToolContext());\n            currentHandler = PerformanceTester;\n        });\n        this.routing.addRoute(\"/liveinteraction\", async () => {\n            if (!Engine.vfs) {\n                throw new Error(\"Original game files must be provided.\");\n            }\n            console.log('[Application] Initializing LiveInteractionTester');\n            const { TestToolSupport } = await this.importOptionalDevModule('./tools/TestToolSupport');\n            const gameMap = await TestToolSupport.loadMap(this.createTestToolContext().mapResourceLoader!, \"2_reconcile.map\");\n            const { LiveInteractionTester } = await this.importOptionalDevModule('./tools/LiveInteractionTester');\n            await LiveInteractionTester.main(Engine.vfs, gameMap, this.rootEl!, this.strings, this.createTestToolContext(), {\n                generalOptions: this.generalOptions,\n                runtimeVars: this.runtimeVars,\n            });\n            currentHandler = LiveInteractionTester;\n        });\n        this.routing.init();\n    }\n    async destroy(): Promise<void> {\n        console.log('[Application] Destroying Application');\n        if (typeof window !== 'undefined') {\n            window.removeEventListener('resize', this.handleViewportEnvironmentChange);\n            window.removeEventListener('orientationchange', this.handleViewportEnvironmentChange);\n            window.visualViewport?.removeEventListener('resize', this.handleViewportEnvironmentChange);\n        }\n        this.generalOptions.graphics.resolution.onChange.unsubscribe(this.handlePreferredResolutionChange);\n        this.runtimeVars.forceResolution.onChange.unsubscribe(this.handleForceResolutionChange);\n        this.fullScreen.dispose();\n        if (this.gui) {\n            if (this.gui.destroy) {\n                await this.gui.destroy();\n            }\n            this.gui = undefined;\n        }\n    }\n    private createSplashScreenInterface() {\n        return {\n            setLoadingText: (text: string) => {\n                console.log(`[Application] Splash Loading: \"${text}\"`);\n                if (this.splashScreenUpdateCallback) {\n                    this.splashScreenUpdateCallback({\n                        width: this.viewport.value.width,\n                        height: this.viewport.value.height,\n                        parentElement: this.rootEl,\n                        loadingText: text\n                    });\n                }\n            },\n            setBackgroundImage: (url: string) => {\n                console.log(`[Application] Splash Background: ${url}`);\n                if (this.splashScreenUpdateCallback) {\n                    this.splashScreenUpdateCallback({\n                        width: this.viewport.value.width,\n                        height: this.viewport.value.height,\n                        parentElement: this.rootEl,\n                        backgroundImage: url\n                    });\n                }\n            },\n            setCopyrightText: (text: string) => {\n                console.log(`[Application] Splash Copyright: ${text}`);\n                if (this.splashScreenUpdateCallback) {\n                    this.splashScreenUpdateCallback({\n                        width: this.viewport.value.width,\n                        height: this.viewport.value.height,\n                        parentElement: this.rootEl,\n                        copyrightText: text\n                    });\n                }\n            },\n            setDisclaimerText: (text: string) => {\n                console.log(`[Application] Splash Disclaimer: ${text}`);\n                if (this.splashScreenUpdateCallback) {\n                    this.splashScreenUpdateCallback({\n                        width: this.viewport.value.width,\n                        height: this.viewport.value.height,\n                        parentElement: this.rootEl,\n                        disclaimerText: text\n                    });\n                }\n            },\n            destroy: () => {\n                console.log('[Application] Splash screen destroyed');\n                if (this.splashScreenUpdateCallback) {\n                    this.splashScreenUpdateCallback(null);\n                }\n            },\n            element: { style: { display: 'none' } }\n        };\n    }\n    private async handleGameResLoadError(error: Error, strings: Strings, fatal: boolean = false): Promise<void> {\n        let errorMessage = strings.get(\"ts:import_load_files_failed\");\n        if (error.name === \"ChecksumError\") {\n            const fileField = (error as any).file || '';\n            const template = strings.get(\"ts:import_checksum_mismatch\");\n            const replaced = template.indexOf(\"%s\") >= 0 ?\n                template.replace(/%s/g, fileField) :\n                template + \" \" + fileField;\n            errorMessage += \"\\n\\n\" + replaced;\n        }\n        else if (error.name === \"FileNotFoundError\") {\n            const fileField = (error as any).file || '';\n            const template = strings.get(\"ts:import_file_not_found\");\n            const replaced = template.indexOf(\"%s\") >= 0 ?\n                template.replace(/%s/g, fileField) :\n                template + \" \" + fileField;\n            errorMessage += \"\\n\\n\" + replaced;\n        }\n        else if (error.name === \"DownloadError\" || error.message?.match(/XHR error|Failed to fetch/i)) {\n            errorMessage += \"\\n\\n\" + strings.get(\"ts:downloadfailed\");\n        }\n        else if (error.name === \"NoStorageError\") {\n            errorMessage += \"\\n\\n\" + strings.get(\"ts:import_no_storage\");\n        }\n        else if (error.message?.match(/out of memory|allocation/i)) {\n            errorMessage += \"\\n\\n\" + strings.get(\"ts:gameinitoom\");\n        }\n        else if (error.name === \"QuotaExceededError\" || error.name === \"StorageQuotaError\") {\n            errorMessage += \"\\n\\n\" + strings.get(\"ts:storage_quota_exceeded\");\n        }\n        else if (error.name === \"IOError\") {\n            errorMessage += \"\\n\\n\" + strings.get(\"ts:storage_io_error\");\n            fatal = true;\n        }\n        else {\n            console.error(\"Unrecognized GameRes error:\", error);\n            const wrappedError = new Error(`Game res load failed (${error.message ?? error.name})`);\n            (wrappedError as any).cause = error;\n            this.sentry?.captureException(wrappedError);\n        }\n        if (this.gui && this.gui.getRootController()) {\n            try {\n                const messageBoxApi = this.gui.getMessageBoxApi();\n                await messageBoxApi.alert(errorMessage, strings.get(\"GUI:OK\"));\n            }\n            catch (e) {\n                console.error(\"Failed to show error dialog:\", e);\n                const errorBox = new BasicErrorBoxApi(this.viewport, strings, this.rootEl!);\n                await errorBox.show(errorMessage, fatal);\n            }\n        }\n        else {\n            const errorBox = new BasicErrorBoxApi(this.viewport, strings, this.rootEl!);\n            await errorBox.show(errorMessage, fatal);\n        }\n    }\n    private async handleGameResImportError(error: Error, strings: Strings): Promise<void> {\n        let errorMessage = strings.get(\"ts:import_failed\");\n        if (error.name === \"FileNotFoundError\") {\n            const fileField = (error as any).file || '';\n            const template = strings.get(\"ts:import_file_not_found\");\n            const replaced = template.indexOf(\"%s\") >= 0 ?\n                template.replace(/%s/g, fileField) :\n                template + \" \" + fileField;\n            errorMessage += \"\\n\\n\" + replaced;\n        }\n        else if (error.name === \"InvalidArchiveError\") {\n            errorMessage += \"\\n\\n\" + strings.get(\"ts:import_invalid_archive\");\n        }\n        else if (error.name === \"ArchiveExtractionError\") {\n            if ((error as any).cause?.message?.match(/out of memory|allocation/i)) {\n                errorMessage += \"\\n\\n\" + strings.get(\"ts:import_out_of_memory\");\n            }\n            else {\n                errorMessage += \"\\n\\n\" + strings.get(\"ts:import_archive_extract_failed\");\n                const wrappedError = new Error(`Game res import failed (${error.message ?? error.name})`);\n                (wrappedError as any).cause = error;\n                this.sentry?.captureException(wrappedError);\n            }\n        }\n        else if (error.name === \"NoWebAssemblyError\") {\n            errorMessage += \"\\n\\n\" + strings.get(\"ts:import_no_web_assembly\");\n        }\n        else if (error.name === \"ChecksumError\") {\n            const fileField = (error as any).file || '';\n            const template = strings.get(\"ts:import_checksum_mismatch\");\n            const replaced = template.indexOf(\"%s\") >= 0 ?\n                template.replace(/%s/g, fileField) :\n                template + \" \" + fileField;\n            errorMessage += \"\\n\\n\" + replaced;\n        }\n        else if (error.name === \"DownloadError\" || error.message?.match(/XHR error|Failed to fetch|CompileError: WebAssembly|SystemJS|NetworkError|Load failed/i)) {\n            errorMessage += \"\\n\\n\" + strings.get(\"ts:downloadfailed\");\n        }\n        else if (error.name === \"ArchiveDownloadError\") {\n            const urlField = (error as any).url || '';\n            const template = strings.get(\"ts:import_archive_download_failed\");\n            const replaced = template.indexOf(\"%s\") >= 0 ?\n                template.replace(/%s/g, urlField) :\n                template + \" \" + urlField;\n            errorMessage = replaced;\n        }\n        else if (error.name === \"NoStorageError\") {\n            errorMessage += \"\\n\\n\" + strings.get(\"ts:import_no_storage\");\n        }\n        else if (error.message?.match(/out of memory|allocation/i) || error.name.match(/NS_ERROR_FAILURE|NS_ERROR_OUT_OF_MEMORY/)) {\n            errorMessage += \"\\n\\n\" + strings.get(\"ts:import_out_of_memory\");\n        }\n        else if (error.name === \"QuotaExceededError\" || error.name === \"StorageQuotaError\") {\n            errorMessage += \"\\n\\n\" + strings.get(\"ts:storage_quota_exceeded\");\n        }\n        else if (error.name !== \"IOError\" && error.name !== \"FileNotFoundError\" && error.name !== \"AbortError\") {\n            const wrappedError = new Error(\"Game res import failed \" + (error.message ?? error.name));\n            (wrappedError as any).cause = error;\n            this.sentry?.captureException(wrappedError);\n        }\n        if (this.gui && this.gui.getRootController()) {\n            try {\n                const messageBoxApi = this.gui.getMessageBoxApi();\n                await messageBoxApi.alert(errorMessage, strings.get(\"GUI:OK\"));\n            }\n            catch (e) {\n                console.error(\"Failed to show error dialog:\", e);\n                const errorBox = new BasicErrorBoxApi(this.viewport, strings, this.rootEl!);\n                await errorBox.show(errorMessage, false);\n            }\n        }\n        else {\n            const errorBox = new BasicErrorBoxApi(this.viewport, strings, this.rootEl!);\n            await errorBox.show(errorMessage, false);\n        }\n    }\n}\ndeclare global {\n    interface Window {\n        gtag?: (...args: any[]) => void;\n    }\n}\n"
  },
  {
    "path": "src/BattleControlApi.ts",
    "content": "declare const THREE: any;\ninterface WorldInteraction {\n    customScrollHandler: {\n        requestScroll(vector: any): void;\n        cancel(): void;\n    };\n    keyboardHandler: {\n        executeCommand(command: string): void;\n    };\n    applyKeyModifiers(modifiers: any): void;\n}\ntype ToggleCallback = (value: any) => void;\nexport class BattleControlApi {\n    private _toggleCallbacks = new Set<ToggleCallback>();\n    private _worldInteraction?: WorldInteraction;\n    constructor() {\n    }\n    _setWorldInteraction(worldInteraction: WorldInteraction): void {\n        this._worldInteraction = worldInteraction;\n    }\n    _notifyToggle(value: any): void {\n        for (const callback of this._toggleCallbacks) {\n            try {\n                callback(value);\n            }\n            catch (error) {\n                console.error(error);\n            }\n        }\n    }\n    onToggle(callback: ToggleCallback): () => void {\n        this._toggleCallbacks.add(callback);\n        return () => {\n            this._toggleCallbacks.delete(callback);\n        };\n    }\n    requestPan(x: number, y: number): void {\n        const vector = new THREE.Vector2(x, y);\n        this._worldInteraction?.customScrollHandler.requestScroll(vector);\n    }\n    cancelPan(): void {\n        this._worldInteraction?.customScrollHandler.cancel();\n    }\n    executeKeyCommand(command: string): void {\n        this._worldInteraction?.keyboardHandler.executeCommand(command);\n    }\n    applyKeyModifiers(modifiers: any): void {\n        this._worldInteraction?.applyKeyModifiers(modifiers);\n    }\n}\n"
  },
  {
    "path": "src/ClientApi.ts",
    "content": "import { BattleControlApi } from './BattleControlApi';\nexport class ClientApi {\n    public battleControl: BattleControlApi;\n    constructor() {\n        this.battleControl = new BattleControlApi();\n    }\n}\n"
  },
  {
    "path": "src/Config.ts",
    "content": "import { IniFile } from './data/IniFile';\nimport { IniSection } from './data/IniSection';\ninterface ViewportConfig {\n    width: number;\n    height: number;\n}\ninterface SentryConfig {\n    dsn: string;\n    env: string;\n    defaultIntegrations: boolean;\n    lazyLoad: boolean;\n}\nexport class Config {\n    private generalData!: IniSection;\n    public viewport!: ViewportConfig;\n    public sentry?: SentryConfig;\n    public corsProxies: [\n        string,\n        string\n    ][] = [];\n    constructor() {\n        this.corsProxies = [];\n    }\n    public load(iniFile: IniFile): void {\n        const generalSection = iniFile.getSection(\"General\");\n        if (!generalSection) {\n            throw new Error(\"Missing [General] section in application config\");\n        }\n        this.generalData = generalSection;\n        this.viewport = {\n            width: generalSection.getNumber(\"viewport.width\"),\n            height: generalSection.getNumber(\"viewport.height\"),\n        };\n        const sentrySection = iniFile.getSection(\"Sentry\");\n        if (sentrySection) {\n            this.sentry = {\n                dsn: sentrySection.getString(\"dsn\"),\n                env: sentrySection.getString(\"env\"),\n                defaultIntegrations: sentrySection.getBool(\"defaultIntegrations\"),\n                lazyLoad: sentrySection.getBool(\"lazyLoad\", true),\n            };\n        }\n        const corsProxySection = iniFile.getSection(\"CorsProxy\");\n        if (corsProxySection) {\n            this.corsProxies = [];\n            corsProxySection.entries.forEach((value, key) => {\n                if (typeof value === 'string') {\n                    this.corsProxies.push([key, value]);\n                }\n                else if (Array.isArray(value)) {\n                    console.warn(`[Config] CorsProxy key '${key}' has an array value, using first entry: ${value[0]}`);\n                    this.corsProxies.push([key, value[0]]);\n                }\n            });\n        }\n    }\n    public getGeneralData(): IniSection {\n        if (!this.generalData) {\n            console.warn(\"[Config] getGeneralData called before config was properly loaded. Returning empty section.\");\n            return new IniSection(\"General\");\n        }\n        return this.generalData;\n    }\n    get defaultLocale(): string {\n        return this.generalData.getString(\"defaultLanguage\", \"en-US\");\n    }\n    get serversUrl(): string {\n        return this.generalData.getString(\"serversUrl\", \"servers.ini\");\n    }\n    get gameresBaseUrl(): string | undefined {\n        const url = this.generalData.getString(\"gameresBaseUrl\");\n        return url === \"\" ? undefined : url;\n    }\n    get gameResArchiveUrl(): string | undefined {\n        const url = this.generalData.getString(\"gameResArchiveUrl\");\n        return url === \"\" ? undefined : url;\n    }\n    get mapsBaseUrl(): string | undefined {\n        const url = this.generalData.getString(\"mapsBaseUrl\");\n        return url === \"\" ? undefined : url;\n    }\n    get modsBaseUrl(): string | undefined {\n        const url = this.generalData.getString(\"modsBaseUrl\");\n        return url === \"\" ? undefined : url;\n    }\n    get devMode(): boolean {\n        return this.generalData.getBool(\"dev\");\n    }\n    get discordUrl(): string | undefined {\n        const url = this.generalData.getString(\"discordUrl\");\n        return url.length > 0 ? url : undefined;\n    }\n    get patchNotesUrl(): string | undefined {\n        const url = this.generalData.getString(\"patchNotesUrl\");\n        return url.length > 0 ? url : undefined;\n    }\n    get ladderRulesUrl(): string | undefined {\n        const url = this.generalData.getString(\"ladderRulesUrl\");\n        return url.length > 0 ? url : undefined;\n    }\n    get modSdkUrl(): string | undefined {\n        const url = this.generalData.getString(\"modSdkUrl\");\n        return url.length > 0 ? url : undefined;\n    }\n    get breakingNewsUrl(): string | undefined {\n        const url = this.generalData.getString(\"breakingNewsUrl\");\n        return url.length > 0 ? url : undefined;\n    }\n    get quickMatchEnabled(): boolean {\n        return this.generalData.getBool(\"quickMatchEnabled\");\n    }\n    get unrankedQueueEnabled(): boolean {\n        return this.generalData.getBool(\"unrankedQueueEnabled\", true);\n    }\n    get botsEnabled(): boolean {\n        return this.generalData.getBool(\"botsEnabled\");\n    }\n    get oldClientsBaseUrl(): string | undefined {\n        const url = this.generalData.getString(\"oldClientsBaseUrl\");\n        return url.length > 0 ? url : undefined;\n    }\n    get debugGameState(): boolean {\n        return this.generalData.getBool(\"debugGameState\");\n    }\n    get debugLogging(): boolean | string | undefined {\n        const strVal = this.generalData.getString(\"debugLogging\");\n        if (strVal === \"\")\n            return undefined;\n        const boolVal = this.generalData.getBool(\"debugLogging\");\n        if (boolVal)\n            return true;\n        if (strVal.toLowerCase() === 'false' || strVal === '0' || strVal.toLowerCase() === 'no' || strVal.toLowerCase() === 'off')\n            return false;\n        return strVal;\n    }\n    public getCorsProxy(urlToMatch: string): string | undefined {\n        let wildcardProxy: string | undefined = undefined;\n        for (const [pattern, proxyUrl] of this.corsProxies) {\n            if (pattern.startsWith(\".\")) {\n                if (urlToMatch.endsWith(pattern)) {\n                    return proxyUrl;\n                }\n            }\n            else if (pattern === \"*\") {\n                wildcardProxy = proxyUrl;\n            }\n            else {\n                if (urlToMatch === pattern) {\n                    return proxyUrl;\n                }\n            }\n        }\n        return wildcardProxy;\n    }\n}\n"
  },
  {
    "path": "src/ConsoleVars.ts",
    "content": "import { BoxedVar } from './util/BoxedVar';\nexport class ConsoleVars {\n    public readonly debugWireframes: BoxedVar<boolean>;\n    public readonly debugPaths: BoxedVar<boolean>;\n    public readonly debugText: BoxedVar<boolean>;\n    public readonly debugBotIndex: BoxedVar<number>;\n    public readonly debugLogging: BoxedVar<boolean>;\n    public readonly debugGameState: BoxedVar<boolean>;\n    public readonly forceResolution: BoxedVar<string | undefined>;\n    public readonly freeCamera: BoxedVar<boolean>;\n    public readonly fps: BoxedVar<boolean>;\n    public readonly persistentHoverTags: BoxedVar<boolean>;\n    public readonly cheatsEnabled: BoxedVar<boolean>;\n    public readonly fullScreenZoomOut: BoxedVar<number>;\n    public perfRaycastHelperReuse?: BoxedVar<boolean>;\n    public perfEntityIntersectTraversal?: BoxedVar<boolean>;\n    public perfMapTileHitTest?: BoxedVar<boolean>;\n    public perfWorldViewportCache?: BoxedVar<boolean>;\n    public perfWorldSoundLoopCache?: BoxedVar<boolean>;\n    public perfTelemetry?: BoxedVar<boolean>;\n    constructor() {\n        this.debugWireframes = new BoxedVar<boolean>(false);\n        this.debugPaths = new BoxedVar<boolean>(false);\n        this.debugText = new BoxedVar<boolean>(false);\n        this.debugBotIndex = new BoxedVar<number>(0);\n        this.debugLogging = new BoxedVar<boolean>(false);\n        this.debugGameState = new BoxedVar<boolean>(false);\n        this.forceResolution = new BoxedVar<string | undefined>(undefined);\n        this.freeCamera = new BoxedVar<boolean>(false);\n        this.fps = new BoxedVar<boolean>(false);\n        this.persistentHoverTags = new BoxedVar<boolean>(false);\n        this.cheatsEnabled = new BoxedVar<boolean>(false);\n        this.fullScreenZoomOut = new BoxedVar<number>(1.3);\n    }\n}\n"
  },
  {
    "path": "src/ErrorHandler.ts",
    "content": "interface MessageBoxApi {\n    show(message: string, buttonText?: string, callback?: () => void): void;\n}\ninterface StringsApi {\n    get(key: string): string;\n}\nexport class ErrorHandler {\n    private messageBoxApi: MessageBoxApi;\n    private strings: StringsApi;\n    private isErrorState: boolean = false;\n    constructor(messageBoxApi: MessageBoxApi, strings: StringsApi) {\n        this.messageBoxApi = messageBoxApi;\n        this.strings = strings;\n    }\n    handle(error: any, message: string, callback?: () => void): void {\n        if (!this.isErrorState) {\n            if (callback) {\n                this.messageBoxApi.show(message, this.strings.get(\"GUI:Ok\"), () => {\n                    this.isErrorState = false;\n                    callback();\n                });\n            }\n            else {\n                this.messageBoxApi.show(message);\n            }\n        }\n        console.error(\"Handled error:\", error);\n        this.isErrorState = true;\n    }\n}\n"
  },
  {
    "path": "src/Gui.ts",
    "content": "import { Renderer } from './engine/gfx/Renderer.js';\nimport { UiScene } from './gui/UiScene.js';\nimport { JsxRenderer } from './gui/jsx/JsxRenderer.js';\nimport { BoxedVar } from './util/BoxedVar.js';\nimport { RootController } from './gui/screen/RootController.js';\nimport { ScreenType, MainMenuScreenType } from './gui/screen/ScreenType.js';\nimport { MainMenuRootScreen } from './gui/screen/mainMenu/MainMenuRootScreen.js';\nimport { HomeScreen } from './gui/screen/mainMenu/main/HomeScreen.js';\nimport { LanSetupScreen } from './gui/screen/mainMenu/lan/LanSetupScreen.js';\nimport { StorageScreen } from './gui/screen/options/StorageScreen.js';\nimport { Config } from './Config.js';\nimport { Strings } from './data/Strings.js';\nimport { Engine } from './engine/Engine.js';\nimport { MusicType } from './engine/sound/Music.js';\nimport { MessageBoxApi } from './gui/component/MessageBoxApi.js';\nimport { ToastApi } from './gui/component/ToastApi';\nimport { ShpFile } from './data/ShpFile.js';\nimport { Palette } from './data/Palette.js';\nimport { UiAnimationLoop } from './engine/UiAnimationLoop.js';\nimport { Mixer } from './engine/sound/Mixer.js';\nimport { ChannelType } from './engine/sound/ChannelType.js';\nimport { AudioSystem } from './engine/sound/AudioSystem.js';\nimport { Sound } from './engine/sound/Sound.js';\nimport { SoundSpecs } from './engine/sound/SoundSpecs.js';\nimport { Music } from './engine/sound/Music.js';\nimport { MusicSpecs } from './engine/sound/MusicSpecs.js';\nimport { LocalPrefs, StorageKey } from './LocalPrefs.js';\nimport { GeneralOptions } from './gui/screen/options/GeneralOptions.js';\nimport { FullScreen } from './gui/FullScreen.js';\nimport { Pointer } from './gui/Pointer.js';\nimport { CanvasMetrics } from './gui/CanvasMetrics.js';\nimport { createMobileTouchControls } from './gui/MobileTouchControls.js';\nimport { ErrorHandler } from './ErrorHandler.js';\nimport { ResourceLoader } from './engine/ResourceLoader.js';\nimport { MapFileLoader } from './gui/screen/game/MapFileLoader.js';\nimport { LoadingScreenApiFactory } from './gui/screen/game/loadingScreen/LoadingScreenApiFactory.js';\nimport { GameLoader } from './gui/screen/game/GameLoader.js';\nimport { Rules } from './game/rules/Rules.js';\nimport { VxlGeometryPool } from './engine/renderable/builder/vxlGeometry/VxlGeometryPool.js';\nimport { VxlGeometryCache } from './engine/gfx/geometry/VxlGeometryCache.js';\nimport { GameResConfig } from './engine/gameRes/GameResConfig.js';\nimport { KeyBinds } from './gui/screen/game/worldInteraction/keyboard/KeyBinds.js';\nimport { ClientApi } from './ClientApi.js';\nimport type { ViewportRect } from './gui/Viewport.js';\nimport { attachPerformanceOptions, installPerformanceDebugApi } from './performance/PerformanceRuntime.js';\nexport class Gui {\n    private appVersion: string;\n    private strings: Strings;\n    private config: Config;\n    private viewport: BoxedVar<ViewportRect>;\n    private rootEl: HTMLElement;\n    private renderer?: Renderer;\n    private uiScene?: UiScene;\n    private jsxRenderer?: JsxRenderer;\n    private uiAnimationLoop?: UiAnimationLoop;\n    private rootController?: RootController;\n    private messageBoxApi?: MessageBoxApi;\n    private toastApi?: any;\n    private runtimeVars?: any;\n    private pointer?: Pointer;\n    private canvasMetrics?: CanvasMetrics;\n    private cdnResourceLoader?: any;\n    private gameResConfig?: GameResConfig;\n    private mixer?: Mixer;\n    private audioSystem?: AudioSystem;\n    private sound?: Sound;\n    private music?: Music;\n    private localPrefs: LocalPrefs;\n    private generalOptions?: GeneralOptions;\n    private fullScreen?: FullScreen;\n    private keyBinds?: any;\n    private images: Map<string, ShpFile> = new Map();\n    private palettes: Map<string, Palette> = new Map();\n    private animationId?: number;\n    private lastTime: number = 0;\n    constructor(appVersion: string, strings: Strings, config: Config, viewport: BoxedVar<ViewportRect>, rootEl: HTMLElement, cdnResourceLoader?: any, gameResConfig?: GameResConfig, runtimeVars?: any, generalOptions?: GeneralOptions, fullScreen?: FullScreen) {\n        this.appVersion = appVersion;\n        this.strings = strings;\n        this.config = config;\n        this.viewport = viewport;\n        this.rootEl = rootEl;\n        this.localPrefs = new LocalPrefs(localStorage);\n        this.cdnResourceLoader = cdnResourceLoader;\n        this.gameResConfig = gameResConfig;\n        this.runtimeVars = runtimeVars;\n        this.generalOptions = generalOptions;\n        this.fullScreen = fullScreen;\n    }\n    async init(): Promise<void> {\n        console.log('[Gui] Initializing GUI system');\n        this.initRenderer();\n        this.initUiScene();\n        await this.loadGameResources();\n        await this.initAudioSystem();\n        await this.initOptionsSystem();\n        this.initPointer();\n        this.initJsxRenderer();\n        this.initRootController();\n        this.startAnimationLoop();\n        await this.routeToInitialScreen();\n        createMobileTouchControls(this.rootEl);\n    }\n    private initRenderer(): void {\n        console.log('[Gui] Initializing renderer');\n        const { width, height } = this.viewport.value;\n        this.renderer = new Renderer(width, height);\n        this.renderer.init(this.rootEl);\n        this.uiAnimationLoop = new UiAnimationLoop(this.renderer);\n        this.uiAnimationLoop.start();\n        console.log('[Gui] UiAnimationLoop started');\n        this.viewport.onChange.subscribe(this.handleViewportChange.bind(this));\n    }\n    private handleViewportChange(newViewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }): void {\n        console.log('[Gui] Viewport changed:', newViewport);\n        this.renderer?.setSize(newViewport.width, newViewport.height);\n        if (this.uiScene) {\n            const newCamera = UiScene.createCamera(newViewport);\n            this.uiScene.setCamera(newCamera);\n            this.uiScene.setViewport(newViewport);\n            if (this.jsxRenderer) {\n                this.jsxRenderer.setCamera(newCamera);\n            }\n            this.rootController?.rerenderCurrentScreen();\n            this.canvasMetrics?.notifyViewportChange();\n        }\n    }\n    private initUiScene(): void {\n        console.log('[Gui] Initializing UI scene');\n        this.uiScene = UiScene.factory(this.viewport.value);\n    }\n    private initJsxRenderer(): void {\n        console.log('[Gui] Initializing JSX renderer');\n        if (!this.uiScene) {\n            throw new Error('UiScene must be initialized before JsxRenderer');\n        }\n        this.jsxRenderer = new JsxRenderer(Engine.images, Engine.palettes, this.uiScene.getCamera(), this.pointer?.pointerEvents);\n        this.messageBoxApi = new MessageBoxApi(this.viewport, this.uiScene, this.jsxRenderer);\n        this.toastApi = new ToastApi(this.viewport, this.uiScene, this.jsxRenderer);\n    }\n    private initPointer(): void {\n        if (!this.renderer || !this.uiScene || !this.generalOptions)\n            return;\n        const canvasMetrics = new CanvasMetrics(this.renderer.getCanvas(), window);\n        canvasMetrics.init();\n        this.canvasMetrics = canvasMetrics;\n        const pointer = Pointer.factory(Engine.images.get('mouse.shp'), Engine.palettes.get('mousepal.pal'), this.renderer, document, canvasMetrics, this.generalOptions.mouseAcceleration);\n        pointer.init();\n        this.pointer = pointer;\n        this.uiScene.add(pointer.getSprite());\n    }\n    private initRootController(): void {\n        console.log('[Gui] Initializing root controller');\n        const serverRegions = { loaded: true } as any;\n        this.rootController = new RootController(serverRegions);\n    }\n    private async loadGameResources(): Promise<void> {\n        console.log('[Gui] Loading game resources');\n        if (!Engine.vfs) {\n            console.warn('[Gui] Engine.vfs not available - skipping resource loading');\n            return;\n        }\n        Engine.images.setVfs(Engine.vfs);\n        Engine.palettes.setVfs(Engine.vfs);\n        console.log('[Gui] Engine LazyResourceCollections configured with VFS');\n        const testImages = ['mnscrnl.shp', 'lwscrnl.shp', 'sdtp.shp'];\n        for (const imageName of testImages) {\n            try {\n                const shpFile = Engine.images.get(imageName);\n                if (shpFile) {\n                    console.log(`[Gui] Successfully loaded test image: ${imageName} (${shpFile.width}x${shpFile.height})`);\n                }\n                else {\n                    console.warn(`[Gui] Failed to load test image: ${imageName}`);\n                }\n            }\n            catch (error) {\n                console.warn(`[Gui] Error loading test image ${imageName}:`, error);\n            }\n        }\n    }\n    private async getMainMenuVideoUrl(): Promise<string | File | undefined> {\n        console.log('[Gui] Getting main menu video URL');\n        const videoFileName = Engine.rfsSettings.menuVideoFileName;\n        console.log('[Gui] Video file name:', videoFileName);\n        try {\n            if (Engine.rfs) {\n                console.log('[Gui] Checking RFS for video file...');\n                try {\n                    const rfsContainsVideo = await Engine.rfs.containsEntry(videoFileName);\n                    console.log(`[Gui] RFS contains ${videoFileName}:`, rfsContainsVideo);\n                    if (rfsContainsVideo) {\n                        console.log('[Gui] Found video file in RFS:', videoFileName);\n                        const fileData = await Engine.rfs.getRawFile(videoFileName);\n                        const videoFile = new File([fileData], videoFileName, { type: \"video/webm\" });\n                        console.log('[Gui] Created video File object from RFS:', videoFile.name, videoFile.size, 'bytes');\n                        if (videoFile.size === 0) {\n                            console.warn('[Gui] Video file from RFS is empty!');\n                        }\n                        else {\n                            return videoFile;\n                        }\n                    }\n                }\n                catch (error) {\n                    console.warn('[Gui] Error checking RFS for video file:', error);\n                }\n            }\n            else {\n                console.warn('[Gui] Engine.rfs not available');\n            }\n            if (!Engine.vfs) {\n                console.warn('[Gui] Engine.vfs not available - cannot load video');\n                return undefined;\n            }\n            console.log('[Gui] Checking if video file exists in VFS...');\n            console.log('[Gui] Available archives:', Engine.vfs.listArchives());\n            console.log(`[Gui] Checking for video file: ${videoFileName}`);\n            console.log(`[Gui] VFS fileExists result:`, Engine.vfs.fileExists(videoFileName));\n            if (Engine.vfs.fileExists(videoFileName)) {\n                console.log('[Gui] Found video file in VFS:', videoFileName);\n                const fileData = Engine.vfs.openFile(videoFileName).asFile();\n                const videoFile = new File([fileData], videoFileName, { type: \"video/webm\" });\n                console.log('[Gui] Created video File object:', videoFile.name, videoFile.size, 'bytes');\n                if (videoFile.size === 0) {\n                    console.warn('[Gui] Video file is empty!');\n                    return undefined;\n                }\n                return videoFile;\n            }\n            else {\n                console.warn('[Gui] Video file not found in VFS:', videoFileName);\n                const alternativeNames = ['ra2ts_l.bik', 'ra2ts_l.mp4', 'menu.webm', 'menu.mp4', 'ra2ts_l.avi'];\n                for (const altName of alternativeNames) {\n                    console.log(`[Gui] Checking alternative video file: ${altName}`);\n                    if (Engine.vfs.fileExists(altName)) {\n                        console.log('[Gui] Found alternative video file:', altName);\n                        if (altName.endsWith('.bik')) {\n                            console.warn(`[Gui] Found .bik file but cannot play directly: ${altName}`);\n                            console.warn('[Gui] .bik files need to be converted to .webm during import process');\n                            continue;\n                        }\n                        const fileData = Engine.vfs.openFile(altName).asFile();\n                        const videoFile = new File([fileData], altName, {\n                            type: altName.endsWith('.mp4') ? \"video/mp4\" : \"video/webm\"\n                        });\n                        console.log('[Gui] Created alternative video File object:', videoFile.name, videoFile.size, 'bytes');\n                        return videoFile;\n                    }\n                }\n                console.warn('[Gui] No playable video file found, will proceed without video');\n                return undefined;\n            }\n        }\n        catch (error) {\n            console.error('[Gui] Failed to read video file from VFS:', error);\n            return undefined;\n        }\n    }\n    private async routeToInitialScreen(): Promise<void> {\n        console.log('[Gui] Routing to initial screen');\n        if (!this.rootController || !this.uiScene || !this.jsxRenderer || !this.renderer || !this.messageBoxApi) {\n            throw new Error('GUI components not properly initialized');\n        }\n        this.renderer.addScene(this.uiScene);\n        this.rootEl.appendChild(this.uiScene.getHtmlContainer().getElement()!);\n        console.log('[Gui] Added UiScene HTML container to DOM');\n        let hasShownDialog = false;\n        if (this.music && !hasShownDialog && this.audioSystem?.isSuspended()) {\n            console.log('[Gui] Audio system is suspended, requesting permission');\n            await new Promise<void>((resolve) => {\n                this.messageBoxApi!.show(this.strings.get(\"GUI:RequestAudioPermission\"), this.strings.get(\"GUI:OK\"), async () => {\n                    try {\n                        await this.audioSystem!.initMusicLoop();\n                        console.log('[Gui] Audio permission granted and music loop initialized');\n                    }\n                    catch (error) {\n                        console.error('[Gui] Failed to initialize music loop:', error);\n                    }\n                    resolve();\n                });\n            });\n            hasShownDialog = true;\n        }\n        await this.navigateToMainMenu();\n    }\n    private async navigateToMainMenu(): Promise<void> {\n        console.log('[Gui] Navigating to main menu');\n        if (!this.rootController || !this.uiScene || !this.jsxRenderer || !this.renderer || !this.messageBoxApi) {\n            throw new Error('GUI components not properly initialized');\n        }\n        const videoSrc = await this.getMainMenuVideoUrl();\n        console.log('[Gui] Video source:', videoSrc);\n        const subScreens = new Map<MainMenuScreenType, any>();\n        subScreens.set(MainMenuScreenType.Home, HomeScreen);\n        subScreens.set(MainMenuScreenType.OptionsStorage, StorageScreen);\n        const { SkirmishScreen } = await import('./gui/screen/mainMenu/lobby/SkirmishScreen.js');\n        subScreens.set(MainMenuScreenType.Skirmish, SkirmishScreen);\n        const { MapSelScreen } = await import('./gui/screen/mainMenu/mapSel/MapSelScreen.js');\n        subScreens.set(MainMenuScreenType.MapSelection, MapSelScreen);\n        const { TestEntryScreen } = await import('./gui/screen/mainMenu/main/TestEntryScreen.js');\n        subScreens.set(MainMenuScreenType.TestEntry, TestEntryScreen);\n        subScreens.set(MainMenuScreenType.LanSetup, LanSetupScreen);\n        const { InfoAndCreditsScreen } = await import('./gui/screen/mainMenu/infoAndCredits/InfoAndCreditsScreen.js');\n        const { CreditsScreen } = await import('./gui/screen/mainMenu/credits/CreditsScreen.js');\n        subScreens.set(MainMenuScreenType.InfoAndCredits, InfoAndCreditsScreen);\n        subScreens.set(MainMenuScreenType.Credits, CreditsScreen);\n        const { OptionsScreen } = await import('./gui/screen/options/OptionsScreen.js');\n        const { SoundOptsScreen } = await import('./gui/screen/options/SoundOptsScreen.js');\n        const { KeyboardScreen } = await import('./gui/screen/options/KeyboardScreen.js');\n        subScreens.set(MainMenuScreenType.Options, OptionsScreen);\n        subScreens.set(MainMenuScreenType.OptionsSound, SoundOptsScreen);\n        subScreens.set(MainMenuScreenType.OptionsKeyboard, KeyboardScreen);\n        const { ReplaySelScreen } = await import('./gui/screen/replay/ReplaySelScreen.js');\n        subScreens.set(MainMenuScreenType.ReplaySelection, ReplaySelScreen);\n        const { ReplayManager } = await import('./gui/ReplayManager.js');\n        let replayManager: any;\n        try {\n            const replayDirHandle = await Engine.getReplayDir();\n            if (replayDirHandle) {\n                const { RealFileSystemDir } = await import('./data/vfs/RealFileSystemDir.js');\n                const { ReplayStorageFileSystem } = await import('./gui/replay/ReplayStorageFileSystem.js');\n                replayManager = new ReplayManager(new ReplayStorageFileSystem(new RealFileSystemDir(replayDirHandle) as any));\n            }\n        }\n        catch (error) {\n            console.error('[Gui] Failed to initialize persistent replay storage', error);\n        }\n        if (!replayManager) {\n            const { ReplayStorageMemStorage } = await import('./gui/replay/ReplayStorageMemStorage.js');\n            replayManager = new ReplayManager(new ReplayStorageMemStorage());\n        }\n        const mainMenuRootScreen = new MainMenuRootScreen(subScreens, this.uiScene, this.strings, Engine.images, this.jsxRenderer, this.messageBoxApi, this.appVersion, this.config, videoSrc, this.sound, this.music, this.generalOptions, this.localPrefs, this.fullScreen, this.mixer, this.keyBinds, this.rootController);\n        (mainMenuRootScreen as any).replayManager = replayManager;\n        this.rootController.addScreen(ScreenType.MainMenuRoot, mainMenuRootScreen);\n        const { GameScreen } = await import('./gui/screen/game/GameScreen.js');\n        const errorHandler = new ErrorHandler(this.messageBoxApi, this.strings);\n        const gameResBaseUrl = this.config.gameresBaseUrl ?? '';\n        const mapsBaseUrl = this.config.mapsBaseUrl ?? '';\n        console.log('[Gui] Creating game loaders', { gameResBaseUrl, mapsBaseUrl });\n        const gameResLoader = this.cdnResourceLoader ?? new ResourceLoader(gameResBaseUrl);\n        const mapResLoader = new ResourceLoader(mapsBaseUrl);\n        const mapFileLoader = new MapFileLoader(mapResLoader, (Engine as any).vfs);\n        const rules = new Rules(Engine.getRules(), undefined);\n        const loadingScreenApiFactory = new LoadingScreenApiFactory(rules, this.strings, this.uiScene, this.jsxRenderer!, this.gameResConfig!, undefined as any);\n        const gameModes = Engine.getMpModes();\n        const speedCheat = new BoxedVar<boolean>(false);\n        const mutedPlayers = new Set<string>();\n        const tauntsEnabled = new BoxedVar<boolean>(this.localPrefs.getBool(StorageKey.TauntsEnabled, true));\n        tauntsEnabled.onChange.subscribe((value: boolean) => {\n            this.localPrefs.setItem(StorageKey.TauntsEnabled, String(Number(value)));\n        });\n        const clientApi = new ClientApi();\n        window.dispatchEvent(new CustomEvent('CdApiReady', { detail: clientApi }));\n        (window as any).CdApi = clientApi;\n        const gameMenuSubScreens = new Map<number, any>();\n        gameMenuSubScreens.set((await import('./gui/screen/game/gameMenu/ScreenType.js')).ScreenType.Home, new (await import('./gui/screen/game/gameMenu/GameMenuHomeScreen.js')).GameMenuHomeScreen(this.strings, this.fullScreen!));\n        gameMenuSubScreens.set((await import('./gui/screen/game/gameMenu/ScreenType.js')).ScreenType.Diplo, new (await import('./gui/screen/game/gameMenu/DiploScreen.js')).DiploScreen(this.strings, this.jsxRenderer!, this.renderer!, Engine.getMpModes() as any, tauntsEnabled, mutedPlayers));\n        gameMenuSubScreens.set((await import('./gui/screen/game/gameMenu/ScreenType.js')).ScreenType.ConnectionInfo, new (await import('./gui/screen/game/gameMenu/ConnectionInfoScreen.js')).ConnectionInfoScreen(this.strings, this.jsxRenderer!));\n        gameMenuSubScreens.set((await import('./gui/screen/game/gameMenu/ScreenType.js')).ScreenType.QuitConfirm, new (await import('./gui/screen/game/gameMenu/QuitConfirmScreen.js')).QuitConfirmScreen(this.strings));\n        gameMenuSubScreens.set((await import('./gui/screen/game/gameMenu/ScreenType.js')).ScreenType.Options, new (await import('./gui/screen/options/OptionsScreen.js')).OptionsScreen(this.strings, this.jsxRenderer!, this.generalOptions!, this.localPrefs, this.fullScreen!, true, false));\n        gameMenuSubScreens.set((await import('./gui/screen/game/gameMenu/ScreenType.js')).ScreenType.OptionsSound, new (await import('./gui/screen/options/SoundOptsScreen.js')).SoundOptsScreen(this.strings, this.jsxRenderer!, this.mixer!, this.music!, this.localPrefs));\n        gameMenuSubScreens.set((await import('./gui/screen/game/gameMenu/ScreenType.js')).ScreenType.OptionsKeyboard, new (await import('./gui/screen/options/KeyboardScreen.js')).KeyboardScreen(this.strings, this.jsxRenderer!, this.keyBinds!));\n        const sharedVxlGeometryPool = new VxlGeometryPool(new VxlGeometryCache(null, Engine.getActiveMod?.() ?? null), this.generalOptions!.graphics.models.value);\n        const buildingImageDataCache = new Map();\n        const gameScreen = new GameScreen(undefined, undefined, undefined, undefined, undefined, this.appVersion, '', errorHandler, gameMenuSubScreens, loadingScreenApiFactory, undefined, undefined, this.config, this.strings, this.renderer, this.uiScene, this.runtimeVars || {}, this.messageBoxApi, this.toastApi, this.uiAnimationLoop, this.viewport, this.jsxRenderer, this.pointer, this.sound, this.music, this.mixer, this.keyBinds, this.generalOptions, this.localPrefs, undefined, undefined, replayManager, this.fullScreen, mapFileLoader, undefined, Engine.getMapList?.(), new GameLoader(this.appVersion, undefined, gameResLoader, gameResLoader, rules, gameModes, this.sound, (console as any), undefined, speedCheat, this.gameResConfig!, sharedVxlGeometryPool, buildingImageDataCache, (this as any).runtimeVars?.debugBotIndex, this.config.devMode ?? false), sharedVxlGeometryPool, buildingImageDataCache, mutedPlayers, tauntsEnabled, speedCheat, undefined, clientApi.battleControl);\n        (gameScreen as any).setController?.(this.rootController);\n        this.rootController.addScreen(ScreenType.Game, gameScreen as any);\n        const { ReplayScreen } = await import('./gui/screen/replay/ReplayScreen.js');\n        const replayGameLoader = new GameLoader(this.appVersion, undefined, gameResLoader, gameResLoader, rules, gameModes, this.sound, (console as any), undefined, speedCheat, this.gameResConfig!, sharedVxlGeometryPool, buildingImageDataCache, (this as any).runtimeVars?.debugBotIndex, this.config.devMode ?? false);\n        const replayScreen = new ReplayScreen(this.appVersion, '', errorHandler, gameMenuSubScreens, loadingScreenApiFactory, this.config as any, this.strings, this.renderer as any, this.uiScene as any, this.runtimeVars || {} as any, this.messageBoxApi as any, this.uiAnimationLoop as any, this.viewport as any, this.jsxRenderer as any, this.pointer as any, this.sound as any, this.music as any, this.keyBinds as any, this.generalOptions as any, undefined as any, this.fullScreen as any, mapFileLoader as any, replayGameLoader as any, sharedVxlGeometryPool as any, buildingImageDataCache as any, (params?: any) => {\n            this.rootController!.goToScreen(ScreenType.MainMenuRoot, params);\n        }, clientApi.battleControl);\n        this.rootController.addScreen(ScreenType.Replay, replayScreen as any);\n        this.rootController.goToScreen(ScreenType.MainMenuRoot);\n    }\n    private startAnimationLoop(): void {\n        console.log('[Gui] Animation loop already started by UiAnimationLoop');\n    }\n    getRootController(): RootController {\n        if (!this.rootController) {\n            throw new Error('Root controller is not initialized');\n        }\n        return this.rootController;\n    }\n    getMessageBoxApi(): MessageBoxApi {\n        if (!this.messageBoxApi) {\n            throw new Error('MessageBoxApi is not initialized');\n        }\n        return this.messageBoxApi;\n    }\n    async destroy(): Promise<void> {\n        console.log('[Gui] Destroying GUI system');\n        try {\n            const { ShpBuilder } = await import('./engine/renderable/builder/ShpBuilder.js');\n            if (ShpBuilder?.clearCaches) {\n                ShpBuilder.clearCaches();\n                console.log('[Gui] Cleared ShpBuilder caches');\n            }\n            const TexUtils = await import('./engine/gfx/TextureUtils.js');\n            if (TexUtils?.TextureUtils?.cache) {\n                TexUtils.TextureUtils.cache.forEach((tex: any) => tex.dispose?.());\n                TexUtils.TextureUtils.cache.clear();\n                console.log('[Gui] Cleared TextureUtils caches');\n            }\n        }\n        catch (e) {\n            console.warn('[Gui] Failed to clear caches during destroy:', e);\n        }\n        if (this.messageBoxApi) {\n            this.messageBoxApi.destroy();\n        }\n        if (this.music) {\n            this.music.stopPlaying();\n            this.music.dispose();\n        }\n        if (this.sound) {\n            this.sound.dispose();\n        }\n        if (this.audioSystem) {\n            this.audioSystem.dispose();\n        }\n        if (this.mixer) {\n            this.localPrefs.setItem(StorageKey.Mixer, this.mixer.serialize());\n        }\n        if (this.music) {\n            this.localPrefs.setItem(StorageKey.MusicOpts, this.music.serializeOptions());\n        }\n        const debugRoot = (window as any).__ra2debug;\n        if (debugRoot) {\n            debugRoot.audioSystem = undefined;\n            debugRoot.mixer = undefined;\n            debugRoot.music = undefined;\n            debugRoot.generalOptions = undefined;\n            debugRoot.keyBinds = undefined;\n            debugRoot.fullScreen = undefined;\n            debugRoot.localPrefs = undefined;\n        }\n        if (this.uiAnimationLoop) {\n            this.uiAnimationLoop.destroy();\n        }\n        if (this.rootController) {\n            this.rootController.destroy();\n        }\n        if (this.uiScene) {\n            const htmlElement = this.uiScene.getHtmlContainer().getElement();\n            if (htmlElement && this.rootEl.contains(htmlElement)) {\n                this.rootEl.removeChild(htmlElement);\n            }\n            this.uiScene.destroy();\n        }\n        if (this.renderer) {\n            this.rootEl.removeChild(this.renderer.getCanvas());\n            this.renderer.dispose();\n        }\n    }\n    private async initAudioSystem(): Promise<void> {\n        console.log('[Gui] Initializing audio system');\n        try {\n            let mixer: Mixer;\n            const mixerData = this.localPrefs.getItem(StorageKey.Mixer);\n            if (mixerData) {\n                try {\n                    mixer = new Mixer().unserialize(mixerData);\n                    console.log('[Gui] Loaded mixer settings from local storage');\n                }\n                catch (error) {\n                    console.warn('Failed to read mixer values from local storage', error);\n                    mixer = this.createDefaultMixer();\n                }\n            }\n            else {\n                mixer = this.createDefaultMixer();\n            }\n            this.mixer = mixer;\n            this.audioSystem = new AudioSystem(mixer as any);\n            const debugRoot = ((window as any).__ra2debug ??= {});\n            debugRoot.audioSystem = this.audioSystem;\n            debugRoot.mixer = this.mixer;\n            if (Engine.vfs) {\n                const soundIni = Engine.getIni('sound.ini');\n                const soundSpecs = new SoundSpecs(soundIni);\n                const audioVisualRules = {\n                    ini: {\n                        getString: (key: string) => {\n                            try {\n                                const rulesIni = Engine.getIni('rules.ini');\n                                const audioVisualSection = rulesIni.getSection('AudioVisual');\n                                if (audioVisualSection) {\n                                    return audioVisualSection.getString(key);\n                                }\n                            }\n                            catch (error) {\n                                console.warn(`[Gui] Failed to get AudioVisual setting for key \"${key}\":`, error);\n                            }\n                            return undefined;\n                        }\n                    }\n                };\n                const soundAudioSystemAdapter = {\n                    initialize: () => this.audioSystem!.initialize(),\n                    dispose: () => this.audioSystem!.dispose(),\n                    playWavFile: (file: any, channel: ChannelType, volume?: number, pan?: number, delay?: number, rate?: number, loop?: boolean) => {\n                        return this.audioSystem!.playWavFile(file, channel, volume, pan, delay, rate, loop);\n                    },\n                    playWavSequence: (files: any[], channel: ChannelType, volume?: number, pan?: number, delay?: number, rate?: number) => {\n                        return this.audioSystem!.playWavSequence(files, channel, volume, pan, delay, rate);\n                    },\n                    playWavLoop: (files: any[], channel: ChannelType, volume?: number, pan?: number, delayMs?: {\n                        min: number;\n                        max: number;\n                    }, rate?: number, attack?: boolean, decay?: boolean, loops?: number) => {\n                        return this.audioSystem!.playWavLoop(files, channel, volume, pan, delayMs, rate, attack, decay, loops);\n                    },\n                    setMuted: (muted: boolean) => this.audioSystem!.setMuted(muted)\n                };\n                this.sound = new Sound(soundAudioSystemAdapter, Engine.getSounds(), soundSpecs, audioVisualRules, document);\n                this.sound.initialize();\n                console.log('[Gui] Sound system initialized');\n            }\n            await this.initMusicSystem();\n            console.log('[Gui] Audio system initialization completed');\n        }\n        catch (error) {\n            console.error('[Gui] Failed to initialize audio system:', error);\n        }\n    }\n    private createDefaultMixer(): Mixer {\n        const mixer = new Mixer();\n        mixer.setVolume(ChannelType.Master, 0.4);\n        mixer.setVolume(ChannelType.CreditTicks, 0.2);\n        mixer.setVolume(ChannelType.Music, 0.3);\n        mixer.setVolume(ChannelType.Ambient, 0.3);\n        mixer.setVolume(ChannelType.Effect, 0.5);\n        mixer.setVolume(ChannelType.Voice, 0.7);\n        mixer.setVolume(ChannelType.Ui, 0.5);\n        console.log('[Gui] Created default mixer settings');\n        return mixer;\n    }\n    private async initMusicSystem(): Promise<void> {\n        if (!this.audioSystem || !Engine.vfs) {\n            console.warn('[Gui] Cannot initialize music system - missing dependencies');\n            return;\n        }\n        try {\n            let hasMusicDir = false;\n            try {\n                hasMusicDir = !!(await Engine.rfs?.containsEntry(Engine.rfsSettings.musicDir));\n            }\n            catch (error) {\n                console.warn('Could not check music directory:', error);\n                hasMusicDir = false;\n            }\n            if (hasMusicDir) {\n                const themeIniFileName = Engine.getFileNameVariant('theme.ini');\n                const themeIni = Engine.getIni(themeIniFileName);\n                const musicSpecs = new MusicSpecs(themeIni);\n                const musicAudioSystemAdapter = {\n                    playMusicFile: async (file: any, repeat: boolean, onEnded?: () => void): Promise<boolean> => {\n                        try {\n                            await this.audioSystem!.playMusicFile(file, repeat, onEnded);\n                            return true;\n                        }\n                        catch (error) {\n                            console.error('Failed to play music file:', error);\n                            return false;\n                        }\n                    },\n                    stopMusic: () => this.audioSystem!.stopMusic()\n                };\n                this.music = new Music(musicAudioSystemAdapter, Engine.getThemes(), musicSpecs);\n                const musicOptions = this.localPrefs.getItem(StorageKey.MusicOpts);\n                if (musicOptions) {\n                    try {\n                        this.music.unserializeOptions(musicOptions);\n                        console.log('[Gui] Loaded music options from local storage');\n                    }\n                    catch (error) {\n                        console.warn('Failed to read music options from local storage', error);\n                    }\n                }\n                const debugRoot = ((window as any).__ra2debug ??= {});\n                debugRoot.music = this.music;\n                console.log('[Gui] Music system initialized');\n            }\n            else {\n                console.warn('[Gui] No music directory found - music system disabled');\n            }\n        }\n        catch (error) {\n            console.error('[Gui] Failed to initialize music system:', error);\n        }\n    }\n    private async initOptionsSystem(): Promise<void> {\n        console.log('[Gui] Initializing options system');\n        if (!this.generalOptions) {\n            this.generalOptions = new GeneralOptions();\n            const optionsData = this.localPrefs.getItem(StorageKey.Options);\n            if (optionsData) {\n                try {\n                    this.generalOptions.unserialize(optionsData);\n                    console.log('[Gui] Loaded general options from local storage');\n                }\n                catch (error) {\n                    console.warn('Failed to read general options from local storage', error);\n                }\n            }\n        }\n        if (!this.fullScreen) {\n            this.fullScreen = new FullScreen(document);\n            this.fullScreen.init();\n        }\n        const keyboardIniFileName = Engine.getFileNameVariant('keyboard.ini');\n        this.keyBinds = new KeyBinds(Engine.rfs?.getRootDirectory?.(), keyboardIniFileName, Engine.getIni(keyboardIniFileName));\n        await this.keyBinds.load();\n        const debugRoot = ((window as any).__ra2debug ??= {});\n        debugRoot.generalOptions = this.generalOptions;\n        debugRoot.keyBinds = this.keyBinds;\n        debugRoot.fullScreen = this.fullScreen;\n        debugRoot.localPrefs = this.localPrefs;\n        const performanceOptions = this.generalOptions.performance;\n        attachPerformanceOptions(performanceOptions);\n        const runtimeVars = this.runtimeVars ?? {};\n        this.runtimeVars = Object.assign(runtimeVars, {\n            debugWireframes: runtimeVars.debugWireframes ?? new BoxedVar<boolean>(false),\n            debugPaths: runtimeVars.debugPaths ?? new BoxedVar<boolean>(false),\n            debugText: runtimeVars.debugText ?? new BoxedVar<boolean>(false),\n            debugBotIndex: runtimeVars.debugBotIndex ?? new BoxedVar<number>(0),\n            debugLogging: runtimeVars.debugLogging ?? new BoxedVar<boolean>(false),\n            debugGameState: runtimeVars.debugGameState ?? new BoxedVar<boolean>(false),\n            forceResolution: runtimeVars.forceResolution ?? new BoxedVar<string | undefined>(undefined),\n            freeCamera: runtimeVars.freeCamera ?? new BoxedVar<boolean>(false),\n            fps: runtimeVars.fps ?? new BoxedVar<boolean>(false),\n            persistentHoverTags: runtimeVars.persistentHoverTags ?? new BoxedVar<boolean>(false),\n            cheatsEnabled: runtimeVars.cheatsEnabled ?? new BoxedVar<boolean>(false),\n            fullScreenZoomOut: runtimeVars.fullScreenZoomOut ?? new BoxedVar<number>(1.3),\n            perfRaycastHelperReuse: performanceOptions.raycastHelperReuse,\n            perfEntityIntersectTraversal: performanceOptions.entityIntersectTraversal,\n            perfMapTileHitTest: performanceOptions.mapTileHitTest,\n            perfWorldViewportCache: performanceOptions.worldViewportCache,\n            perfWorldSoundLoopCache: performanceOptions.worldSoundLoopCache,\n            perfTelemetry: performanceOptions.telemetry,\n        });\n        debugRoot.runtimeVars = this.runtimeVars;\n        installPerformanceDebugApi(debugRoot);\n        console.log('[Gui] Runtime vars ready', Object.keys(this.runtimeVars));\n        console.log('[Gui] Options system initialized');\n    }\n}\n"
  },
  {
    "path": "src/LocalPrefs.ts",
    "content": "export enum StorageKey {\n    GameRes = \"_r_gameRes\",\n    Options = \"_r_opts_v3\",\n    Mixer = \"_r_mixer_v3\",\n    MusicOpts = \"_r_opts_music\",\n    LastGpuTier = \"_r_last_gpu\",\n    LastSeenPatch = \"_r_last_patch\",\n    LastMap = \"_r_lastMap\",\n    LastMode = \"_r_lastMode\",\n    LastSortMap = \"_r_lastSortMap\",\n    LastPlayerCountry = \"_r_lastCountry\",\n    LastPlayerColor = \"_r_lastColor\",\n    LastPlayerStartPos = \"_r_lastStartPos\",\n    LastPlayerTeam = \"_r_lastTeam\",\n    LastQueueRanked = \"_r_lastRanked\",\n    LastQueueType = \"_r_lastQueueType\",\n    LastBots = \"_r_lastBots\",\n    PreferredGameOpts = \"_r_hostOpts\",\n    LastConnection = \"_r_lastCon\",\n    PreferredServerRegion = \"_r_region\",\n    TauntsEnabled = \"_r_taunts\",\n    LanPlayerName = \"_r_lanPlayerName\",\n    LanRecentPlays = \"_r_lanRecentPlays\",\n    UploadedBots = \"_r_uploadedBots\"\n}\nexport class LocalPrefs {\n    protected storage: Storage;\n    constructor(storage: Storage) {\n        this.storage = storage;\n    }\n    getItem(key: StorageKey | string): string | undefined {\n        try {\n            return this.storage?.getItem(String(key)) ?? undefined;\n        }\n        catch (e) {\n            console.warn(`Unable to read key ${key} from storage.`, e);\n            return undefined;\n        }\n    }\n    setItem(key: StorageKey | string, value: string): boolean {\n        try {\n            this.storage?.setItem(String(key), value);\n            return true;\n        }\n        catch (e) {\n            console.warn(`Unable to write key ${key} to storage.`, e);\n            return false;\n        }\n    }\n    removeItem(key: StorageKey | string): void {\n        try {\n            this.storage?.removeItem(String(key));\n        }\n        catch (e) {\n            console.warn(`Unable to remove key ${key} from storage.`, e);\n        }\n    }\n    listItems(): string[] {\n        if (this.storage && typeof this.storage.length === 'number') {\n            const keys: string[] = [];\n            for (let i = 0; i < this.storage.length; i++) {\n                const key = this.storage.key(i);\n                if (key !== null) {\n                    keys.push(key);\n                }\n            }\n            return keys;\n        }\n        return [];\n    }\n    getBool(key: StorageKey | string, defaultValue: boolean = false): boolean {\n        const value = this.getItem(key);\n        if (value === undefined)\n            return defaultValue;\n        return value === \"true\" || value === \"1\";\n    }\n    getNumber(key: StorageKey | string, defaultValue: number = 0): number {\n        const value = this.getItem(key);\n        if (value === undefined)\n            return defaultValue;\n        const num = parseFloat(value);\n        return isNaN(num) ? defaultValue : num;\n    }\n}\n"
  },
  {
    "path": "src/RouteHelper.ts",
    "content": "import { Base64 } from '@/util/Base64';\ninterface GameParams {\n    gameId: string;\n    gameTimestamp: number;\n    gservUrl: string;\n    playerName: string;\n    gameOpts: any;\n    tournament?: any;\n}\nexport class RouteHelper {\n    static modQueryStringName = \"mod\";\n    static getGameRoute(params: GameParams): string {\n        return (\"#/game/\" +\n            Base64.encode(JSON.stringify({\n                gameId: params.gameId,\n                gameTimestamp: params.gameTimestamp,\n                gservUrl: params.gservUrl,\n                playerName: params.playerName,\n                gameOpts: params.gameOpts,\n                tournament: params.tournament,\n            })));\n    }\n    static extractGameParams(encodedParams: string): GameParams {\n        return JSON.parse(Base64.decode(encodedParams));\n    }\n}\n"
  },
  {
    "path": "src/data/AudioBagFile.ts",
    "content": "import { DataStream } from \"./DataStream\";\nimport { VirtualFile } from \"./vfs/VirtualFile\";\nimport type { IdxFile } from \"./IdxFile\";\nimport type { IdxEntry } from \"./IdxEntry\";\nexport class AudioBagFile {\n    private fileData: Map<string, DataStream>;\n    constructor() {\n        this.fileData = new Map<string, DataStream>();\n    }\n    public async fromVirtualFile(bagFile: VirtualFile, idx: IdxFile): Promise<this> {\n        for (const [filename, entry] of idx.entries) {\n            const wavDataStream = this.buildWavData(bagFile.stream, entry);\n            wavDataStream.dynamicSize = false;\n            this.fileData.set(filename, wavDataStream);\n        }\n        return this;\n    }\n    public getFileList(): string[] {\n        return [...this.fileData.keys()];\n    }\n    public containsFile(filename: string): boolean {\n        return this.fileData.has(filename);\n    }\n    public openFile(filename: string): VirtualFile {\n        if (!this.containsFile(filename)) {\n            throw new Error(`File \"${filename}\" not found in AudioBagFile`);\n        }\n        const dataStream = this.fileData.get(filename)!;\n        dataStream.seek(0);\n        return new VirtualFile(dataStream, filename);\n    }\n    private buildWavData(sourceStream: DataStream, idxEntry: IdxEntry): DataStream {\n        const outStream = new DataStream();\n        outStream.littleEndian();\n        const channels = (idxEntry.flags & 0x01) > 0 ? 2 : 1;\n        let paddingBytes = 0;\n        if ((idxEntry.flags & 0x02) > 0) {\n            outStream.writeString(\"RIFF\");\n            outStream.writeUint32(idxEntry.length + 36);\n            outStream.writeString(\"WAVE\");\n            outStream.writeString(\"fmt \");\n            outStream.writeUint32(16);\n            outStream.writeUint16(1);\n            outStream.writeUint16(channels);\n            outStream.writeUint32(idxEntry.sampleRate);\n            outStream.writeUint32(idxEntry.sampleRate * channels * 2);\n            outStream.writeUint16(channels * 2);\n            outStream.writeUint16(16);\n            outStream.writeString(\"data\");\n            outStream.writeUint32(idxEntry.length);\n        }\n        else if ((idxEntry.flags & 0x08) > 0) {\n            const byteRate = 11100 * channels * Math.floor(idxEntry.sampleRate / 22050);\n            const blockAlign = idxEntry.chunkSize;\n            const samplesPerBlock = 1017;\n            const numBlocks = Math.max(2, Math.ceil(idxEntry.length / blockAlign));\n            const totalDataBytesInAdpcm = numBlocks * blockAlign;\n            paddingBytes = totalDataBytesInAdpcm - idxEntry.length;\n            outStream.writeString(\"RIFF\");\n            outStream.writeUint32(52 + totalDataBytesInAdpcm);\n            outStream.writeString(\"WAVE\");\n            outStream.writeString(\"fmt \");\n            outStream.writeUint32(20);\n            outStream.writeUint16(17);\n            outStream.writeUint16(channels);\n            outStream.writeUint32(idxEntry.sampleRate);\n            outStream.writeUint32(byteRate);\n            outStream.writeUint16(blockAlign);\n            outStream.writeUint16(4);\n            outStream.writeUint16(2);\n            outStream.writeUint16(samplesPerBlock);\n            outStream.writeString(\"fact\");\n            outStream.writeUint32(4);\n            outStream.writeUint32(samplesPerBlock * numBlocks);\n            outStream.writeString(\"data\");\n            outStream.writeUint32(totalDataBytesInAdpcm);\n        }\n        else {\n            console.warn(`AudioBagFile: Unknown flags ${idxEntry.flags} for WAV header generation for entry referencing offset ${idxEntry.offset}.`);\n        }\n        sourceStream.seek(idxEntry.offset);\n        const audioData = sourceStream.readUint8Array(idxEntry.length);\n        outStream.writeUint8Array(audioData);\n        for (let i = 0; i < paddingBytes; i++) {\n            outStream.writeUint8(0);\n        }\n        outStream.seek(0);\n        return outStream;\n    }\n}\n"
  },
  {
    "path": "src/data/Bitmap.ts",
    "content": "export enum PixelFormat {\n    Rgb = 1,\n    Rgba = 2,\n    Indexed = 3\n}\nfunction getBytesPerPixel(format: PixelFormat): number {\n    switch (format) {\n        case PixelFormat.Indexed:\n            return 1;\n        case PixelFormat.Rgb:\n            return 3;\n        case PixelFormat.Rgba:\n            return 4;\n        default:\n            throw new Error(\"Unsupported pixel format \" + format);\n    }\n}\nexport class Bitmap {\n    public data: Uint8Array;\n    public pixelFormat: PixelFormat;\n    public width: number;\n    public height: number;\n    constructor(width: number, height: number, data?: Uint8Array, pixelFormat: PixelFormat = PixelFormat.Rgba) {\n        const bytesPerPixel = getBytesPerPixel(pixelFormat);\n        this.data = data || new Uint8Array(bytesPerPixel * width * height);\n        if (this.data.length < bytesPerPixel * width * height && data) {\n        }\n        this.pixelFormat = pixelFormat;\n        this.width = width;\n        this.height = height;\n    }\n    drawIndexedImage(sourceBitmap: IndexedBitmap, x: number, y: number): void {\n        const destBpp = getBytesPerPixel(this.pixelFormat);\n        const destData = this.data;\n        const destStride = this.width * destBpp;\n        const destBufferLimit = destData.length;\n        let destOffset = y * destStride + x * destBpp;\n        let sourceOffset = 0;\n        for (let sy = 0; sy < sourceBitmap.height; sy++) {\n            let currentDestRowOffset = destOffset;\n            for (let sx = 0; sx < sourceBitmap.width; sx++) {\n                const sourceIndexValue = sourceBitmap.data[sourceOffset];\n                if (sourceIndexValue !== 0 && currentDestRowOffset >= 0 && (currentDestRowOffset + destBpp - 1) < destBufferLimit) {\n                    destData[currentDestRowOffset] = sourceIndexValue;\n                    if (destBpp >= 3) {\n                        destData[currentDestRowOffset + 1] = 0;\n                        destData[currentDestRowOffset + 2] = 0;\n                    }\n                    if (destBpp === 4) {\n                        destData[currentDestRowOffset + 3] = 255;\n                    }\n                }\n                currentDestRowOffset += destBpp;\n                sourceOffset++;\n            }\n            destOffset += destStride;\n        }\n    }\n}\nexport class IndexedBitmap extends Bitmap {\n    constructor(width: number, height: number, data?: Uint8Array) {\n        super(width, height, data, PixelFormat.Indexed);\n    }\n}\nexport class RgbBitmap extends Bitmap {\n    constructor(width: number, height: number, data?: Uint8Array) {\n        super(width, height, data, PixelFormat.Rgb);\n    }\n}\nexport class RgbaBitmap extends Bitmap {\n    constructor(width: number, height: number, data?: Uint8Array) {\n        super(width, height, data, PixelFormat.Rgba);\n    }\n    drawRgbaImage(sourceBitmap: RgbaBitmap, x: number, y: number, destWidth?: number, destHeight?: number): void {\n        const destData = this.data;\n        const destStride = this.width * 4;\n        const destBufferLimit = destData.length;\n        const effectiveDestWidth = destWidth ?? sourceBitmap.width;\n        const effectiveDestHeight = destHeight ?? sourceBitmap.height;\n        let destOffset = y * destStride + x * 4;\n        let sourceOffset = 0;\n        const drawHeight = Math.min(effectiveDestHeight, sourceBitmap.height, Math.max(0, this.height - y));\n        const drawWidth = Math.min(effectiveDestWidth, sourceBitmap.width, Math.max(0, this.width - x));\n        for (let sy = 0; sy < drawHeight; sy++) {\n            let currentDestRowOffset = destOffset;\n            let currentSourceRowOffset = sourceOffset;\n            for (let sx = 0; sx < drawWidth; sx++) {\n                if (currentDestRowOffset >= 0 && (currentDestRowOffset + 3) < destBufferLimit) {\n                    destData[currentDestRowOffset] = sourceBitmap.data[currentSourceRowOffset];\n                    destData[currentDestRowOffset + 1] = sourceBitmap.data[currentSourceRowOffset + 1];\n                    destData[currentDestRowOffset + 2] = sourceBitmap.data[currentSourceRowOffset + 2];\n                    destData[currentDestRowOffset + 3] = sourceBitmap.data[currentSourceRowOffset + 3];\n                }\n                currentDestRowOffset += 4;\n                currentSourceRowOffset += 4;\n            }\n            destOffset += destStride;\n            sourceOffset += sourceBitmap.width * 4;\n        }\n    }\n}\n"
  },
  {
    "path": "src/data/Crc32.ts",
    "content": "export class Crc32 {\n    private static readonly lookUp: Uint32Array = new Uint32Array([\n        0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615,\n        3915621685, 2657392035, 249268274, 2044508324, 3772115230,\n        2547177864, 162941995, 2125561021, 3887607047, 2428444049,\n        498536548, 1789927666, 4089016648, 2227061214, 450548861,\n        1843258603, 4107580753, 2211677639, 325883990, 1684777152,\n        4251122042, 2321926636, 335633487, 1661365465, 4195302755,\n        2366115317, 997073096, 1281953886, 3579855332, 2724688242,\n        1006888145, 1258607687, 3524101629, 2768942443, 901097722,\n        1119000684, 3686517206, 2898065728, 853044451, 1172266101,\n        3705015759, 2882616665, 651767980, 1373503546, 3369554304,\n        3218104598, 565507253, 1454621731, 3485111705, 3099436303,\n        671266974, 1594198024, 3322730930, 2970347812, 795835527,\n        1483230225, 3244367275, 3060149565, 1994146192, 31158534,\n        2563907772, 4023717930, 1907459465, 112637215, 2680153253,\n        3904427059, 2013776290, 251722036, 2517215374, 3775830040,\n        2137656763, 141376813, 2439277719, 3865271297, 1802195444,\n        476864866, 2238001368, 4066508878, 1812370925, 453092731,\n        2181625025, 4111451223, 1706088902, 314042704, 2344532202,\n        4240017532, 1658658271, 366619977, 2362670323, 4224994405,\n        1303535960, 984961486, 2747007092, 3569037538, 1256170817,\n        1037604311, 2765210733, 3554079995, 1131014506, 879679996,\n        2909243462, 3663771856, 1141124467, 855842277, 2852801631,\n        3708648649, 1342533948, 654459306, 3188396048, 3373015174,\n        1466479909, 544179635, 3110523913, 3462522015, 1591671054,\n        702138776, 2966460450, 3352799412, 1504918807, 783551873,\n        3082640443, 3233442989, 3988292384, 2596254646, 62317068,\n        1957810842, 3939845945, 2647816111, 81470997, 1943803523,\n        3814918930, 2489596804, 225274430, 2053790376, 3826175755,\n        2466906013, 167816743, 2097651377, 4027552580, 2265490386,\n        503444072, 1762050814, 4150417245, 2154129355, 426522225,\n        1852507879, 4275313526, 2312317920, 282753626, 1742555852,\n        4189708143, 2394877945, 397917763, 1622183637, 3604390888,\n        2714866558, 953729732, 1340076626, 3518719985, 2797360999,\n        1068828381, 1219638859, 3624741850, 2936675148, 906185462,\n        1090812512, 3747672003, 2825379669, 829329135, 1181335161,\n        3412177804, 3160834842, 628085408, 1382605366, 3423369109,\n        3138078467, 570562233, 1426400815, 3317316542, 2998733608,\n        733239954, 1555261956, 3268935591, 3050360625, 752459403,\n        1541320221, 2607071920, 3965973030, 1969922972, 40735498,\n        2617837225, 3943577151, 1913087877, 83908371, 2512341634,\n        3803740692, 2075208622, 213261112, 2463272603, 3855990285,\n        2094854071, 198958881, 2262029012, 4057260610, 1759359992,\n        534414190, 2176718541, 4139329115, 1873836001, 414664567,\n        2282248934, 4279200368, 1711684554, 285281116, 2405801727,\n        4167216745, 1634467795, 376229701, 2685067896, 3608007406,\n        1308918612, 956543938, 2808555105, 3495958263, 1231636301,\n        1047427035, 2932959818, 3654703836, 1088359270, 936918000,\n        2847714899, 3736837829, 1202900863, 817233897, 3183342108,\n        3401237130, 1404277552, 615818150, 3134207493, 3453421203,\n        1423857449, 601450431, 3009837614, 3294710456, 1567103746,\n        711928724, 3020668471, 3272380065, 1510334235, 755167117,\n    ]);\n    private crc: number;\n    private readonly initialCrcValue: number;\n    constructor(initialValue: number = 0xFFFFFFFF) {\n        this.initialCrcValue = initialValue;\n        this.crc = initialValue;\n    }\n    public static calculateCrc(data: Uint8Array, initialValue: number = 0xFFFFFFFF): number {\n        let currentCrc = initialValue;\n        for (let i = 0; i < data.length; i++) {\n            currentCrc = ((currentCrc >>> 8) ^ Crc32.lookUp[(currentCrc & 0xFF) ^ data[i]]) >>> 0;\n        }\n        return (currentCrc ^ initialValue) >>> 0;\n    }\n    public append(data: Uint8Array): void {\n        for (let i = 0; i < data.length; i++) {\n            this.crc = ((this.crc >>> 8) ^ Crc32.lookUp[(this.crc & 0xFF) ^ data[i]]) >>> 0;\n        }\n    }\n    public get(): number {\n        return (this.crc ^ this.initialCrcValue) >>> 0;\n    }\n}\n"
  },
  {
    "path": "src/data/CsfFile.ts",
    "content": "import { DataStream } from './DataStream';\nimport { VirtualFile } from './vfs/VirtualFile';\nconst strwChars = Array.from(\"STRW\");\nconst CSF_LABEL_HAS_VALUE_MAGIC = new Uint32Array(new Uint8Array(strwChars.map((e: string) => e.charCodeAt(0)).reverse()).buffer)[0];\nconst xorDecodeArray = (arr: Uint8Array): Uint8Array => {\n    return arr.map(byte => ~byte & 0xFF);\n};\nconst byteArrayToUnicodeString = (arr: Uint8Array): string => {\n    let result = \"\";\n    for (let i = 0; i < arr.length; i += 2) {\n        result += String.fromCharCode(arr[i] | (arr[i + 1] << 8));\n    }\n    return result;\n};\nexport enum CsfLanguage {\n    EnglishUS = 0,\n    EnglishUK = 1,\n    German = 2,\n    French = 3,\n    Spanish = 4,\n    Italian = 5,\n    Japanese = 6,\n    Jabberwockie = 7,\n    Korean = 8,\n    Unknown = 9,\n    ChineseCN = 100,\n    ChineseTW = 101\n}\nexport const csfLocaleMap = new Map<CsfLanguage, string>([\n    [CsfLanguage.EnglishUS, \"en-US\"],\n    [CsfLanguage.EnglishUK, \"en-GB\"],\n    [CsfLanguage.German, \"de-DE\"],\n    [CsfLanguage.French, \"fr-FR\"],\n    [CsfLanguage.Spanish, \"es-ES\"],\n    [CsfLanguage.Italian, \"it-IT\"],\n    [CsfLanguage.Japanese, \"ja-JP\"],\n    [CsfLanguage.Korean, \"ko-KR\"],\n    [CsfLanguage.ChineseCN, \"zh-CN\"],\n    [CsfLanguage.ChineseTW, \"zh-TW\"],\n]);\nexport class CsfFile {\n    public language: CsfLanguage = CsfLanguage.Unknown;\n    public data: {\n        [key: string]: string;\n    } = {};\n    constructor(virtualFile?: VirtualFile) {\n        if (virtualFile) {\n            this.fromVirtualFile(virtualFile);\n        }\n    }\n    public fromVirtualFile(file: VirtualFile): void {\n        const stream = file.stream;\n        if (!stream) {\n            console.error(\"[CsfFile] VirtualFile does not have a valid stream.\");\n            return;\n        }\n        console.log(`[CsfFile] Parsing CSF file: ${file.filename}`);\n        stream.readInt32();\n        stream.readInt32();\n        const numLabels = stream.readInt32();\n        stream.readInt32();\n        stream.readInt32();\n        this.language = stream.readInt32() as CsfLanguage;\n        console.log(`[CsfFile] Header parsed. Stream position: ${stream.position}, Declared labels: ${numLabels}, Declared lang ID: ${this.language}`);\n        for (let i = 0; i < numLabels; i++) {\n            if (stream.position + 4 > stream.byteLength) {\n                console.error(`[CsfFile] Entry ${i}/${numLabels}: Not enough data for LBL magic. Stopping.`);\n                break;\n            }\n            stream.readInt32();\n            if (stream.position + 4 > stream.byteLength) {\n                console.error(`[CsfFile] Entry ${i}/${numLabels}: Not enough data for numPairs. Stopping.`);\n                break;\n            }\n            const numPairs = stream.readInt32();\n            if (stream.position + 4 > stream.byteLength) {\n                console.error(`[CsfFile] Entry ${i}/${numLabels}: Not enough data for labelNameLength. Stopping.`);\n                break;\n            }\n            const labelNameLength = stream.readInt32();\n            if (labelNameLength < 0) {\n                console.error(`[CsfFile] Entry ${i}/${numLabels}: Invalid negative labelNameLength ${labelNameLength}. Stopping parse.`);\n                break;\n            }\n            const MAX_REASONABLE_LABEL_LENGTH = 1024;\n            if (labelNameLength > MAX_REASONABLE_LABEL_LENGTH || stream.position + labelNameLength > stream.byteLength) {\n                console.error(`[CsfFile] Entry ${i}/${numLabels}: labelNameLength ${labelNameLength} is invalid or would read past EOF. Pos: ${stream.position}, Total: ${stream.byteLength}. Stopping.`);\n                break;\n            }\n            const labelName = stream.readString(labelNameLength);\n            if (numPairs !== 1) {\n                console.warn(`[CsfFile] Entry ${i}/${numLabels}: Label '${labelName}' has ${numPairs} pairs (expected 1). Treating as empty string.`);\n                this.data[labelName.toUpperCase()] = \"\";\n                continue;\n            }\n            if (stream.position + 4 > stream.byteLength) {\n                console.error(`[CsfFile] Entry ${i}/${numLabels} ('${labelName}'): Not enough data for valueFlagsOrMagic. Stopping.`);\n                break;\n            }\n            const valueFlagsOrMagic = stream.readInt32();\n            if (stream.position + 4 > stream.byteLength) {\n                console.error(`[CsfFile] Entry ${i}/${numLabels} ('${labelName}'): Not enough data for charsOrPairsLength. Stopping.`);\n                break;\n            }\n            const charsOrPairsLength = stream.readInt32();\n            if (charsOrPairsLength < 0) {\n                console.error(`[CsfFile] Entry ${i}/${numLabels} ('${labelName}'): Negative charsOrPairsLength ${charsOrPairsLength}. Stopping.`);\n                break;\n            }\n            const bytesToReadForValue = charsOrPairsLength * 2;\n            if (bytesToReadForValue < 0) {\n                console.error(`[CsfFile] Entry ${i}/${numLabels} ('${labelName}'): Negative bytesToReadForValue ${bytesToReadForValue}. Stopping.`);\n                break;\n            }\n            if (stream.position + bytesToReadForValue > stream.byteLength) {\n                console.error(`[CsfFile] Entry ${i}/${numLabels} ('${labelName}'): bytesToReadForValue ${bytesToReadForValue} would read past EOF. Pos: ${stream.position}, Total: ${stream.byteLength}. Stopping.`);\n                break;\n            }\n            let actualValueString = \"\";\n            if (bytesToReadForValue > 0) {\n                const valueBytesRaw = stream.readUint8Array(bytesToReadForValue);\n                const valueBytesDecoded = xorDecodeArray(valueBytesRaw);\n                actualValueString = byteArrayToUnicodeString(valueBytesDecoded);\n            }\n            this.data[labelName.toUpperCase()] = actualValueString;\n            if (valueFlagsOrMagic === CSF_LABEL_HAS_VALUE_MAGIC) {\n                if (stream.position + 4 > stream.byteLength) {\n                    console.warn(`[CsfFile] Entry ${i}/${numLabels} ('${labelName}'): Not enough data for extraWstrLenBytes field (STRW). Assuming no extra string.`);\n                }\n                else {\n                    const extraWstrLenBytes = stream.readInt32();\n                    if (extraWstrLenBytes < 0) {\n                        console.error(`[CsfFile] Entry ${i}/${numLabels} ('${labelName}'): Invalid STRW extraWstrLenBytes ${extraWstrLenBytes}. Stopping.`);\n                        break;\n                    }\n                    if (extraWstrLenBytes > 0) {\n                        if (stream.position + extraWstrLenBytes > stream.byteLength) {\n                            console.error(`[CsfFile] Entry ${i}/${numLabels} ('${labelName}'): STRW extraWstrLenBytes ${extraWstrLenBytes} would read past EOF. Stopping.`);\n                            break;\n                        }\n                        stream.readString(extraWstrLenBytes);\n                    }\n                }\n            }\n        }\n        if (this.language === CsfLanguage.Unknown || this.language === 0) {\n            this.autoDetectLocale();\n        }\n        console.log(`[CsfFile] Finished parsing ${file.filename}. Loaded ${Object.keys(this.data).length} labels. Detected/Set language: ${CsfLanguage[this.language]} (${this.getIsoLocale() || 'N/A'})`);\n    }\n    private autoDetectLocale(): void {\n        const introTheme = this.data[\"THEME:INTRO\"];\n        if (introTheme === \"開場\") {\n            this.language = CsfLanguage.ChineseTW;\n        }\n        else if (introTheme === \"开场\") {\n            this.language = CsfLanguage.ChineseCN;\n        }\n        else if (introTheme) {\n            if (this.language === CsfLanguage.Unknown || this.language === 0) {\n                this.language = CsfLanguage.EnglishUS;\n            }\n        }\n    }\n    public getIsoLocale(): string | undefined {\n        return csfLocaleMap.get(this.language);\n    }\n}\n"
  },
  {
    "path": "src/data/DataStream.ts",
    "content": "type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array;\nexport class DataStream {\n    public static LITTLE_ENDIAN: boolean = true;\n    public static BIG_ENDIAN: boolean = false;\n    public static readonly endianness: boolean = new Int8Array(new Int16Array([1]).buffer)[0] > 0;\n    private _buffer: ArrayBuffer;\n    private _dataView: DataView;\n    private _byteOffset: number;\n    private _byteLength: number;\n    private _dynamicSize: boolean;\n    public position: number;\n    public endianness: boolean;\n    constructor(bufferOrSize: ArrayBuffer | DataView | TypedArray | number = 0, byteOffset: number = 0, endianness: boolean = DataStream.LITTLE_ENDIAN) {\n        this.endianness = endianness;\n        this.position = 0;\n        this._dynamicSize = true;\n        this._byteLength = 0;\n        this._byteOffset = byteOffset || 0;\n        if (bufferOrSize instanceof ArrayBuffer) {\n            this.buffer = bufferOrSize;\n        }\n        else if (typeof bufferOrSize === 'object') {\n            this.dataView = bufferOrSize as DataView | TypedArray;\n            if (byteOffset) {\n                this._byteOffset += byteOffset;\n            }\n        }\n        else {\n            this.buffer = new ArrayBuffer((bufferOrSize as number) || 0);\n        }\n    }\n    get dynamicSize(): boolean {\n        return this._dynamicSize;\n    }\n    set dynamicSize(value: boolean) {\n        if (!value) {\n            this._trimAlloc();\n        }\n        this._dynamicSize = value;\n    }\n    get byteLength(): number {\n        return this._byteLength - this._byteOffset;\n    }\n    get buffer(): ArrayBuffer {\n        this._trimAlloc();\n        return this._buffer;\n    }\n    set buffer(newBuffer: ArrayBuffer) {\n        this._buffer = newBuffer;\n        this._dataView = new DataView(this._buffer, this._byteOffset);\n        this._byteLength = this._buffer.byteLength;\n    }\n    get byteOffset(): number {\n        return this._byteOffset;\n    }\n    set byteOffset(newOffset: number) {\n        this._byteOffset = newOffset;\n        this._dataView = new DataView(this._buffer, this._byteOffset);\n    }\n    get dataView(): DataView {\n        return this._dataView;\n    }\n    set dataView(newDataView: DataView | TypedArray) {\n        this._byteOffset = newDataView.byteOffset;\n        this._buffer = newDataView.buffer as ArrayBuffer;\n        this._dataView = new DataView(this._buffer, this._byteOffset);\n        this._byteLength = this._byteOffset + newDataView.byteLength;\n    }\n    public bigEndian(): this {\n        this.endianness = DataStream.BIG_ENDIAN;\n        return this;\n    }\n    public littleEndian(): this {\n        this.endianness = DataStream.LITTLE_ENDIAN;\n        return this;\n    }\n    private _realloc(bytesNeededForOperation: number): void {\n        const currentStreamPosRelativeToView = this.position;\n        const requiredEffectivePos = currentStreamPosRelativeToView + bytesNeededForOperation;\n        if (!this._dynamicSize) {\n            if (requiredEffectivePos > this.byteLength) {\n                throw new Error(\"DataStream buffer overflow: dynamicSize is false and operation exceeds buffer limit.\");\n            }\n            return;\n        }\n        const requiredTotalAbsoluteOffset = this._byteOffset + requiredEffectivePos;\n        if (requiredTotalAbsoluteOffset <= this._buffer.byteLength) {\n            if (requiredTotalAbsoluteOffset > this._byteLength) {\n                this._byteLength = requiredTotalAbsoluteOffset;\n            }\n            this._dataView = new DataView(this._buffer, this._byteOffset, this._byteLength - this._byteOffset);\n            return;\n        }\n        let newCapacity = this._buffer.byteLength < 1 ? 1 : this._buffer.byteLength;\n        while (requiredTotalAbsoluteOffset > newCapacity) {\n            newCapacity *= 2;\n        }\n        const newBuffer = new ArrayBuffer(newCapacity);\n        const oldUint8Array = new Uint8Array(this._buffer, 0, this._byteLength);\n        const newUint8Array = new Uint8Array(newBuffer);\n        newUint8Array.set(oldUint8Array);\n        this._buffer = newBuffer;\n        this._byteLength = requiredTotalAbsoluteOffset;\n        this._dataView = new DataView(this._buffer, this._byteOffset, this._byteLength - this._byteOffset);\n    }\n    private _trimAlloc(): void {\n        const viewLength = this.byteLength;\n        if (this._byteOffset === 0 && this._buffer.byteLength === viewLength) {\n            return;\n        }\n        const newBuffer = new ArrayBuffer(viewLength);\n        const newUint8Array = new Uint8Array(newBuffer);\n        const oldUint8Array = new Uint8Array(this._buffer, this._byteOffset, viewLength);\n        newUint8Array.set(oldUint8Array);\n        this._buffer = newBuffer;\n        this._byteOffset = 0;\n        this._byteLength = viewLength;\n        this._dataView = new DataView(this._buffer, 0, this._byteLength);\n    }\n    public seek(offset: number): void {\n        const newPosition = Math.max(0, Math.min(offset, this.byteLength));\n        this.position = isNaN(newPosition) || !isFinite(newPosition) ? 0 : newPosition;\n    }\n    public isEof(): boolean {\n        return this.position >= this.byteLength;\n    }\n    public readInt8(): number {\n        const value = this._dataView.getInt8(this.position);\n        this.position += 1;\n        return value;\n    }\n    public readUint8(): number {\n        const value = this._dataView.getUint8(this.position);\n        this.position += 1;\n        return value;\n    }\n    public readInt16(endianness?: boolean): number {\n        const value = this._dataView.getInt16(this.position, endianness ?? this.endianness);\n        this.position += 2;\n        return value;\n    }\n    public readUint16(endianness?: boolean): number {\n        const value = this._dataView.getUint16(this.position, endianness ?? this.endianness);\n        this.position += 2;\n        return value;\n    }\n    public readInt32(endianness?: boolean): number {\n        const value = this._dataView.getInt32(this.position, endianness ?? this.endianness);\n        this.position += 4;\n        return value;\n    }\n    public readUint32(endianness?: boolean): number {\n        const value = this._dataView.getUint32(this.position, endianness ?? this.endianness);\n        this.position += 4;\n        return value;\n    }\n    public readFloat32(endianness?: boolean): number {\n        const value = this._dataView.getFloat32(this.position, endianness ?? this.endianness);\n        this.position += 4;\n        return value;\n    }\n    public readFloat64(endianness?: boolean): number {\n        const value = this._dataView.getFloat64(this.position, endianness ?? this.endianness);\n        this.position += 8;\n        return value;\n    }\n    public writeInt8(value: number): void {\n        this._realloc(1);\n        this._dataView.setInt8(this.position, value);\n        this.position += 1;\n    }\n    public writeUint8(value: number): void {\n        this._realloc(1);\n        this._dataView.setUint8(this.position, value);\n        this.position += 1;\n    }\n    public writeInt16(value: number, endianness?: boolean): void {\n        this._realloc(2);\n        this._dataView.setInt16(this.position, value, endianness ?? this.endianness);\n        this.position += 2;\n    }\n    public writeUint16(value: number, endianness?: boolean): void {\n        this._realloc(2);\n        this._dataView.setUint16(this.position, value, endianness ?? this.endianness);\n        this.position += 2;\n    }\n    public writeInt32(value: number, endianness?: boolean): void {\n        this._realloc(4);\n        this._dataView.setInt32(this.position, value, endianness ?? this.endianness);\n        this.position += 4;\n    }\n    public writeUint32(value: number, endianness?: boolean): void {\n        this._realloc(4);\n        this._dataView.setUint32(this.position, value, endianness ?? this.endianness);\n        this.position += 4;\n    }\n    public writeFloat32(value: number, endianness?: boolean): void {\n        this._realloc(4);\n        this._dataView.setFloat32(this.position, value, endianness ?? this.endianness);\n        this.position += 4;\n    }\n    public writeFloat64(value: number, endianness?: boolean): void {\n        this._realloc(8);\n        this._dataView.setFloat64(this.position, value, endianness ?? this.endianness);\n        this.position += 8;\n    }\n    public mapInt32Array(count: number, endianness?: boolean): Int32Array {\n        this._realloc(4 * count);\n        const result = new Int32Array(this._buffer, this.byteOffset + this.position, count);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += 4 * count;\n        return result;\n    }\n    public mapInt16Array(count: number, endianness?: boolean): Int16Array {\n        this._realloc(2 * count);\n        const result = new Int16Array(this._buffer, this.byteOffset + this.position, count);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += 2 * count;\n        return result;\n    }\n    public mapInt8Array(count: number): Int8Array {\n        this._realloc(count);\n        const result = new Int8Array(this._buffer, this.byteOffset + this.position, count);\n        this.position += count;\n        return result;\n    }\n    public mapUint32Array(count: number, endianness?: boolean): Uint32Array {\n        this._realloc(4 * count);\n        const result = new Uint32Array(this._buffer, this.byteOffset + this.position, count);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += 4 * count;\n        return result;\n    }\n    public mapUint16Array(count: number, endianness?: boolean): Uint16Array {\n        this._realloc(2 * count);\n        const result = new Uint16Array(this._buffer, this.byteOffset + this.position, count);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += 2 * count;\n        return result;\n    }\n    public mapUint8Array(count: number): Uint8Array {\n        this._realloc(count);\n        const result = new Uint8Array(this._buffer, this.byteOffset + this.position, count);\n        this.position += count;\n        return result;\n    }\n    public mapFloat64Array(count: number, endianness?: boolean): Float64Array {\n        this._realloc(8 * count);\n        const result = new Float64Array(this._buffer, this.byteOffset + this.position, count);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += 8 * count;\n        return result;\n    }\n    public mapFloat32Array(count: number, endianness?: boolean): Float32Array {\n        this._realloc(4 * count);\n        const result = new Float32Array(this._buffer, this.byteOffset + this.position, count);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += 4 * count;\n        return result;\n    }\n    public readInt32Array(count?: number, endianness?: boolean): Int32Array {\n        const actualCount = count === undefined ? this.byteLength - this.position / 4 : count;\n        const result = new Int32Array(actualCount);\n        DataStream.memcpy(result.buffer, 0, this.buffer, this.byteOffset + this.position, actualCount * result.BYTES_PER_ELEMENT);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += result.byteLength;\n        return result;\n    }\n    public readInt16Array(count?: number, endianness?: boolean): Int16Array {\n        const actualCount = count === undefined ? this.byteLength - this.position / 2 : count;\n        const result = new Int16Array(actualCount);\n        DataStream.memcpy(result.buffer, 0, this.buffer, this.byteOffset + this.position, actualCount * result.BYTES_PER_ELEMENT);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += result.byteLength;\n        return result;\n    }\n    public readInt8Array(count?: number): Int8Array {\n        const actualCount = count === undefined ? this.byteLength - this.position : count;\n        const result = new Int8Array(actualCount);\n        DataStream.memcpy(result.buffer, 0, this.buffer, this.byteOffset + this.position, actualCount * result.BYTES_PER_ELEMENT);\n        this.position += result.byteLength;\n        return result;\n    }\n    public readUint32Array(count?: number, endianness?: boolean): Uint32Array {\n        const actualCount = count === undefined ? this.byteLength - this.position / 4 : count;\n        const result = new Uint32Array(actualCount);\n        DataStream.memcpy(result.buffer, 0, this.buffer, this.byteOffset + this.position, actualCount * result.BYTES_PER_ELEMENT);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += result.byteLength;\n        return result;\n    }\n    public readUint16Array(count?: number, endianness?: boolean): Uint16Array {\n        const actualCount = count === undefined ? this.byteLength - this.position / 2 : count;\n        const result = new Uint16Array(actualCount);\n        DataStream.memcpy(result.buffer, 0, this.buffer, this.byteOffset + this.position, actualCount * result.BYTES_PER_ELEMENT);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += result.byteLength;\n        return result;\n    }\n    public readUint8Array(count?: number): Uint8Array {\n        const actualCount = count === undefined ? this.byteLength - this.position : count;\n        const result = new Uint8Array(actualCount);\n        DataStream.memcpy(result.buffer, 0, this.buffer, this.byteOffset + this.position, actualCount * result.BYTES_PER_ELEMENT);\n        this.position += result.byteLength;\n        return result;\n    }\n    public readFloat64Array(count?: number, endianness?: boolean): Float64Array {\n        const actualCount = count === undefined ? this.byteLength - this.position / 8 : count;\n        const result = new Float64Array(actualCount);\n        DataStream.memcpy(result.buffer, 0, this.buffer, this.byteOffset + this.position, actualCount * result.BYTES_PER_ELEMENT);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += result.byteLength;\n        return result;\n    }\n    public readFloat32Array(count?: number, endianness?: boolean): Float32Array {\n        const actualCount = count === undefined ? this.byteLength - this.position / 4 : count;\n        const result = new Float32Array(actualCount);\n        DataStream.memcpy(result.buffer, 0, this.buffer, this.byteOffset + this.position, actualCount * result.BYTES_PER_ELEMENT);\n        DataStream.arrayToNative(result, endianness ?? this.endianness);\n        this.position += result.byteLength;\n        return result;\n    }\n    public writeUint8Array(array: Uint8Array): void {\n        this._realloc(array.length);\n        new Uint8Array(this._dataView.buffer, this._dataView.byteOffset + this.position).set(array);\n        this.position += array.length;\n    }\n    public readString(length?: number, encoding?: string): string {\n        if (encoding === undefined || encoding === 'ASCII') {\n            return DataStream.createStringFromArray(this.mapUint8Array(length === undefined ? this.byteLength - this.position : length));\n        }\n        else {\n            return new TextDecoder(encoding).decode(this.mapUint8Array(length!));\n        }\n    }\n    public writeString(str: string, encoding?: string, fixedLength?: number): this {\n        if (encoding === undefined || encoding === 'ASCII') {\n            if (fixedLength !== undefined) {\n                const actualLength = Math.min(str.length, fixedLength);\n                let i = 0;\n                for (; i < actualLength; i++) {\n                    this.writeUint8(str.charCodeAt(i));\n                }\n                for (; i < fixedLength; i++) {\n                    this.writeUint8(0);\n                }\n            }\n            else {\n                for (let i = 0; i < str.length; i++) {\n                    this.writeUint8(str.charCodeAt(i));\n                }\n            }\n        }\n        else {\n            this.writeUint8Array(new TextEncoder().encode(str.substring(0, fixedLength)));\n        }\n        return this;\n    }\n    public readCString(maxLength?: number): string {\n        const remainingBytes = this.byteLength - this.position;\n        const buffer = new Uint8Array(this._buffer, this._byteOffset + this.position);\n        let searchLength = remainingBytes;\n        if (maxLength !== undefined) {\n            searchLength = Math.min(maxLength, remainingBytes);\n        }\n        let nullIndex = 0;\n        while (nullIndex < searchLength && buffer[nullIndex] !== 0) {\n            nullIndex++;\n        }\n        const result = DataStream.createStringFromArray(this.mapUint8Array(nullIndex));\n        if (maxLength !== undefined) {\n            this.position += searchLength - nullIndex;\n        }\n        else if (nullIndex !== remainingBytes) {\n            this.position += 1;\n        }\n        return result;\n    }\n    public writeCString(str: string): void {\n        for (let i = 0; i < str.length; i++) {\n            this.writeUint8(str.charCodeAt(i) & 0xFF);\n        }\n        this.writeUint8(0);\n    }\n    public readUCS2String(length: number, endianness?: boolean): string {\n        return DataStream.createStringFromArray(this.readUint16Array(length, endianness));\n    }\n    public writeUCS2String(str: string, endianness?: boolean, fixedLength?: number): this {\n        const actualLength = fixedLength ?? str.length;\n        let i = 0;\n        for (; i < str.length && i < actualLength; i++) {\n            this.writeUint16(str.charCodeAt(i), endianness);\n        }\n        for (; i < actualLength; i++) {\n            this.writeUint16(0);\n        }\n        return this;\n    }\n    public writeUtf8WithLen(str: string): this {\n        const encoded = new TextEncoder().encode(str);\n        this.writeUint16(encoded.length);\n        this.writeUint8Array(encoded);\n        return this;\n    }\n    public readUtf8WithLen(): string {\n        const length = this.readUint16();\n        return new TextDecoder().decode(this.mapUint8Array(length));\n    }\n    public toUint8Array(): Uint8Array {\n        this._trimAlloc();\n        return new Uint8Array(this._dataView.buffer, this._dataView.byteOffset, this._dataView.byteLength);\n    }\n    public getBytes(): Uint8Array {\n        return this.toUint8Array();\n    }\n    public static memcpy(dst: ArrayBuffer, dstOffset: number, src: ArrayBuffer, srcOffset: number, byteLength: number): void {\n        if (byteLength === 0)\n            return;\n        const dstU8 = new Uint8Array(dst, dstOffset, byteLength);\n        const srcU8 = new Uint8Array(src, srcOffset, byteLength);\n        dstU8.set(srcU8);\n    }\n    public static flipArrayEndianness(array: TypedArray): TypedArray {\n        const bytesPerElement = array.BYTES_PER_ELEMENT;\n        if (bytesPerElement === 1)\n            return array;\n        const r = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);\n        for (let a = 0; a < array.byteLength; a += array.BYTES_PER_ELEMENT) {\n            for (let e = a + array.BYTES_PER_ELEMENT - 1, t = a; e > t; e--, t++) {\n                const s = r[t];\n                r[t] = r[e];\n                r[e] = s;\n            }\n        }\n        return array;\n    }\n    public static arrayToNative(array: TypedArray, endianness: boolean): TypedArray {\n        return endianness === this.endianness ? array : this.flipArrayEndianness(array);\n    }\n    public static nativeToEndian(array: TypedArray, endianness: boolean): TypedArray {\n        return this.endianness === endianness ? array : this.flipArrayEndianness(array);\n    }\n    public static createStringFromArray(array: Uint8Array | Uint16Array): string {\n        const chunks: string[] = [];\n        for (let i = 0; i < array.length; i += 32768) {\n            chunks.push(String.fromCharCode.apply(undefined, Array.from(array.subarray(i, i + 32768))));\n        }\n        return chunks.join(\"\");\n    }\n}\n"
  },
  {
    "path": "src/data/HvaFile.ts",
    "content": "import { Section } from './hva/Section';\nimport type { VirtualFile } from './vfs/VirtualFile';\nimport type { DataStream } from './DataStream';\nimport { Matrix4 } from 'three';\nexport class HvaFile {\n    public filename?: string;\n    public sections: Section[] = [];\n    constructor(source: VirtualFile | DataStream) {\n        if (typeof (source as VirtualFile).filename === 'string' && typeof (source as VirtualFile).stream === 'object') {\n            this.fromVirtualFile(source as VirtualFile);\n        }\n        else if (typeof (source as DataStream).readInt32 === 'function') {\n            this.parseHvaData(source as DataStream, (source as any).filename || 'unknown.hva');\n        }\n        else {\n            throw new Error('Unsupported source type for HvaFile');\n        }\n    }\n    private fromVirtualFile(file: VirtualFile): void {\n        this.filename = file.filename;\n        this.parseHvaData(file.stream as DataStream, file.filename);\n    }\n    private parseHvaData(stream: DataStream, filename: string): void {\n        this.filename = filename;\n        this.sections = [];\n        stream.readCString(16);\n        const numFrames = stream.readInt32();\n        const numSections = stream.readInt32();\n        for (let i = 0; i < numSections; ++i) {\n            const section = new Section();\n            section.name = stream.readCString(16);\n            section.matrices = new Array(numFrames);\n            this.sections.push(section);\n        }\n        for (let frameIndex = 0; frameIndex < numFrames; ++frameIndex) {\n            for (let sectionIndex = 0; sectionIndex < numSections; ++sectionIndex) {\n                this.sections[sectionIndex].matrices[frameIndex] = this.readMatrix(stream);\n            }\n        }\n    }\n    private readMatrix(stream: DataStream): Matrix4 {\n        const matrixElements: number[] = [];\n        for (let i = 0; i < 3; ++i) {\n            matrixElements.push(stream.readFloat32(), stream.readFloat32(), stream.readFloat32(), stream.readFloat32());\n        }\n        matrixElements.push(0, 0, 0, 1);\n        const matrix = new Matrix4();\n        matrix.fromArray(matrixElements);\n        matrix.transpose();\n        return matrix;\n    }\n}\n"
  },
  {
    "path": "src/data/IdxEntry.ts",
    "content": "export class IdxEntry {\n    public filename: string = \"\";\n    public offset: number = 0;\n    public length: number = 0;\n    public sampleRate: number = 0;\n    public flags: number = 0;\n    public chunkSize: number = 0;\n}\n"
  },
  {
    "path": "src/data/IdxFile.ts",
    "content": "import { DataStream } from \"./DataStream\";\nimport { IdxEntry } from \"./IdxEntry\";\nexport class IdxFile {\n    public entries: Map<string, IdxEntry>;\n    constructor(stream: DataStream) {\n        this.entries = new Map<string, IdxEntry>();\n        this.parse(stream);\n    }\n    private parse(stream: DataStream): void {\n        const magicId = stream.readCString(4);\n        if (magicId !== \"GABA\") {\n            throw new Error(`Unable to load Idx file, did not find magic id \"GABA\", found \"${magicId}\" instead`);\n        }\n        const magicNumber = stream.readInt32();\n        if (magicNumber !== 2) {\n            throw new Error(`Unable to load Idx file, did not find magic number 2, found ${magicNumber} instead`);\n        }\n        const numEntries = stream.readInt32();\n        for (let i = 0; i < numEntries; i++) {\n            const entry = new IdxEntry();\n            let rawFilenameBytes = stream.readUint8Array(16);\n            let firstNull = rawFilenameBytes.indexOf(0);\n            if (firstNull === -1)\n                firstNull = 16;\n            let filename = \"\";\n            for (let k = 0; k < firstNull; k++) {\n                filename += String.fromCharCode(rawFilenameBytes[k]);\n            }\n            entry.filename = filename + \".wav\";\n            entry.offset = stream.readUint32();\n            entry.length = stream.readUint32();\n            entry.sampleRate = stream.readUint32();\n            entry.flags = stream.readUint32();\n            entry.chunkSize = stream.readUint32();\n            this.entries.set(entry.filename, entry);\n        }\n    }\n}\n"
  },
  {
    "path": "src/data/IniFile.ts",
    "content": "import { IniSection } from './IniSection';\nimport { IniParser } from './IniParser';\nimport { VirtualFile } from './vfs/VirtualFile';\nexport { IniSection } from './IniSection';\nexport class IniFile {\n    public sections: Map<string, IniSection>;\n    constructor(source?: VirtualFile | Record<string, any> | string) {\n        this.sections = new Map();\n        if (source instanceof VirtualFile) {\n            this.fromVirtualFile(source);\n        }\n        else if (typeof source === 'string') {\n            this.fromString(source);\n        }\n        else if (typeof source === 'object' && source !== null) {\n            this.fromJson(source);\n        }\n        else if (source === undefined) {\n        }\n        else {\n            console.warn(\"IniFile: Constructor called with unknown source type.\");\n        }\n    }\n    public fromVirtualFile(virtualFile: VirtualFile): this {\n        return this.fromString(virtualFile.readAsString());\n    }\n    public fromString(iniString: string): this {\n        const parser = new IniParser();\n        const parsedSectionsObject = parser.parse(iniString);\n        return this.fromJson(parsedSectionsObject);\n    }\n    public fromJson(sectionsObject: Record<string, any>): this {\n        this.sections.clear();\n        for (const sectionName in sectionsObject) {\n            if (sectionsObject.hasOwnProperty(sectionName)) {\n                const sectionData = sectionsObject[sectionName];\n                if (sectionData instanceof IniSection) {\n                    this.sections.set(sectionName, sectionData);\n                }\n                else if (typeof sectionData === 'object' && sectionData !== null) {\n                    const newSection = new IniSection(sectionName);\n                    newSection.fromJson(sectionData);\n                    this.sections.set(sectionName, newSection);\n                }\n                else {\n                    console.warn(`IniFile.fromJson: Section data for \"${sectionName}\" is not a valid object or IniSection instance.`);\n                }\n            }\n        }\n        return this;\n    }\n    public toString(): string {\n        const sectionStrings: string[] = [];\n        this.sections.forEach(section => {\n            sectionStrings.push(section.toString());\n        });\n        return sectionStrings.join(\"\\r\\n\");\n    }\n    public clone(): IniFile {\n        const newIniFile = new IniFile();\n        this.sections.forEach((section, sectionName) => {\n            newIniFile.sections.set(sectionName, section.clone());\n        });\n        return newIniFile;\n    }\n    public getOrCreateSection(sectionName: string): IniSection {\n        let section = this.sections.get(sectionName);\n        if (!section) {\n            section = new IniSection(sectionName);\n            this.sections.set(sectionName, section);\n        }\n        return section;\n    }\n    public getSection(sectionName: string): IniSection | undefined {\n        return this.sections.get(sectionName);\n    }\n    public getOrderedSections(): IniSection[] {\n        return Array.from(this.sections.values());\n    }\n    public mergeWith(otherIniFile: IniFile): this {\n        otherIniFile.sections.forEach((otherSection, sectionName) => {\n            const localSection = this.getOrCreateSection(sectionName);\n            localSection.mergeWith(otherSection);\n        });\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/data/IniParser.ts",
    "content": "import { IniSection } from './IniSection';\nexport class IniParser {\n    private readonly lineRegex = /^\\s*\\[([^\\]]+)\\]\\s*$|^\\s*([^;=#][^=]*?)\\s*(?:=\\s*(.*)?)?\\s*$/;\n    private readonly commentRegex = /^\\s*[;#]/;\n    private readonly arrayKeyRegex = /^(.*)\\[\\]$/;\n    public parse(iniString: string): Record<string, IniSection> {\n        const sections: Record<string, IniSection> = {};\n        let currentSectionName: string = \"__ROOT__\";\n        sections[currentSectionName] = new IniSection(currentSectionName);\n        let currentSectionObj: IniSection = sections[currentSectionName];\n        const lines = iniString.split(/[\\r\\n]+/g);\n        for (const line of lines) {\n            const trimmedLine = line.trim();\n            if (!trimmedLine || this.commentRegex.test(trimmedLine)) {\n                continue;\n            }\n            let processedLine = trimmedLine;\n            if (processedLine.startsWith('[')) {\n                processedLine = processedLine.replace(/]\\s*(\\/\\/|;|#).*$/, ']');\n            }\n            const match = processedLine.match(this.lineRegex);\n            if (match) {\n                if (match[1] !== undefined) {\n                    currentSectionName = this.stripQuotesAndComments(match[1]);\n                    if (!sections[currentSectionName]) {\n                        sections[currentSectionName] = new IniSection(currentSectionName);\n                    }\n                    currentSectionObj = sections[currentSectionName];\n                }\n                else if (match[2] !== undefined) {\n                    let key = this.stripQuotesAndComments(match[2]);\n                    let value = match[3] !== undefined ? this.stripQuotesAndComments(match[3]) : \"\";\n                    const arrayKeyMatch = key.match(this.arrayKeyRegex);\n                    if (arrayKeyMatch) {\n                        key = arrayKeyMatch[1];\n                        const existingEntry = currentSectionObj.get(key);\n                        if (Array.isArray(existingEntry)) {\n                            existingEntry.push(value);\n                        }\n                        else if (existingEntry !== undefined) {\n                            currentSectionObj.set(key, [existingEntry, value]);\n                        }\n                        else {\n                            currentSectionObj.set(key, [value]);\n                        }\n                    }\n                    else {\n                        currentSectionObj.set(key, value);\n                    }\n                }\n            }\n            else {\n            }\n        }\n        return sections;\n    }\n    private stripQuotesAndComments(str: string): string {\n        let currentStr = str.trim();\n        if ((currentStr.startsWith('\"') && currentStr.endsWith('\"')) ||\n            (currentStr.startsWith('\\'') && currentStr.endsWith('\\''))) {\n            currentStr = currentStr.substring(1, currentStr.length - 1);\n        }\n        const commentMatch = currentStr.match(/^([^;#]*)(?:[;#]|$)/);\n        if (commentMatch && commentMatch[1] !== undefined) {\n            currentStr = commentMatch[1].trim();\n        }\n        return currentStr;\n    }\n}\n"
  },
  {
    "path": "src/data/IniSection.ts",
    "content": "export class IniSection {\n    public entries: Map<string, string | string[]>;\n    public sections: Map<string, IniSection>;\n    public name: string;\n    constructor(name: string) {\n        this.entries = new Map();\n        this.sections = new Map();\n        this.name = name;\n    }\n    public fromJson(json: Record<string, any>): this {\n        for (const key in json) {\n            if (json.hasOwnProperty(key)) {\n                const value = json[key];\n                if (Array.isArray(value) || typeof value !== 'object') {\n                    this.set(key, value);\n                }\n                else {\n                    this.sections.set(key, new IniSection(key).fromJson(value));\n                }\n            }\n        }\n        return this;\n    }\n    public clone(): IniSection {\n        const newSection = new IniSection(this.name);\n        this.entries.forEach((value, key) => {\n            newSection.set(key, Array.isArray(value) ? [...value] : value);\n        });\n        this.sections.forEach((section, key) => {\n            newSection.sections.set(key, section.clone());\n        });\n        return newSection;\n    }\n    public set(key: string, value: string | string[]): void {\n        this.entries.set(key, value);\n    }\n    public get(key: string): string | string[] | undefined {\n        return this.entries.get(key);\n    }\n    public has(key: string): boolean {\n        return this.entries.has(key);\n    }\n    public getString(key: string, defaultValue: string = \"\"): string {\n        const value = this.get(key);\n        return typeof value === 'string' ? value : defaultValue;\n    }\n    private parseNumber(valueStr: string): number | undefined {\n        let num;\n        if (valueStr.endsWith('%')) {\n            num = Number(valueStr.replace('%', '')) / 100;\n        }\n        else {\n            num = Number(valueStr);\n        }\n        return isNaN(num) ? undefined : num;\n    }\n    public getNumber(key: string, defaultValue: number = 0): number {\n        const value = this.getString(key);\n        if (value === \"\") {\n            return defaultValue;\n        }\n        const parsedNum = this.parseNumber(value);\n        if (parsedNum === undefined) {\n            console.warn(`[IniSection: ${this.name}] Invalid value for key \"${key}\". \"${value}\" is not a valid number or percentage string.`);\n            return defaultValue;\n        }\n        return parsedNum;\n    }\n    private toFixedPointPrecision(num: number): number {\n        return ((65536 * num) | 0) / 65536;\n    }\n    public getFixed(key: string, defaultValue: number = 0): number {\n        return this.toFixedPointPrecision(this.getNumber(key, defaultValue));\n    }\n    public getBool(key: string, defaultValue: boolean = false): boolean {\n        let valueStr = this.getString(key).trim().toLowerCase();\n        if (!valueStr) {\n            return defaultValue;\n        }\n        if ([\"yes\", \"1\", \"true\", \"on\"].includes(valueStr)) {\n            return true;\n        }\n        if ([\"no\", \"0\", \"false\", \"off\"].includes(valueStr)) {\n            return false;\n        }\n        return defaultValue;\n    }\n    public getKeyArray(key: string, defaultValue: string[] = []): string[] {\n        const value = this.get(key);\n        return Array.isArray(value) ? value : defaultValue;\n    }\n    public getArray(key: string, separator: RegExp = /,\\s*/, defaultValue: string[] = []): string[] {\n        let valueStr = this.getString(key).trim();\n        valueStr = valueStr.replace(/,$/, \"\").replace(/,+/g, \",\");\n        return valueStr ? valueStr.split(separator) : defaultValue;\n    }\n    public getNumberArray(key: string, separator: RegExp = /,\\s*/, defaultValue: number[] = []): number[] {\n        const valueStr = this.getString(key).trim();\n        if (!valueStr)\n            return defaultValue;\n        const parts = valueStr.replace(/,$/, \"\").replace(/,+/g, \",\").split(separator);\n        const numbers: number[] = [];\n        for (const part of parts) {\n            if (!part && parts.length > 1) {\n                console.warn(`[IniSection: ${this.name}] Invalid empty value in array for key \"${key}\". Original string: \"${valueStr}\"`);\n                return defaultValue;\n            }\n            if (!part && parts.length === 1) {\n                return defaultValue;\n            }\n            const num = this.parseNumber(part);\n            if (num === undefined) {\n                console.warn(`[IniSection: ${this.name}] Invalid value in array for key \"${key}\". \"${part}\" is not a valid number. Original string: \"${valueStr}\"`);\n                return defaultValue;\n            }\n            numbers.push(num);\n        }\n        return numbers;\n    }\n    public getFixedArray(key: string, separator: RegExp = /,\\s*/, defaultValue: number[] = []): number[] {\n        const numArray = this.getNumberArray(key, separator, defaultValue);\n        return numArray.map((n) => this.toFixedPointPrecision(n));\n    }\n    public getEnum<T extends object>(key: string, enumObject: T, defaultValue: T[keyof T], caseInsensitive: boolean = false): T[keyof T] {\n        let valueStr = this.getString(key).trim();\n        if (!valueStr)\n            return defaultValue;\n        let foundValue: T[keyof T] | undefined = undefined;\n        if (caseInsensitive) {\n            const lowerValueStr = valueStr.toLowerCase();\n            for (const enumKey in enumObject) {\n                if (enumObject.hasOwnProperty(enumKey) && String(enumKey).toLowerCase() === lowerValueStr) {\n                    foundValue = enumObject[enumKey as keyof T];\n                    break;\n                }\n            }\n        }\n        else {\n            if (enumObject.hasOwnProperty(valueStr)) {\n                foundValue = enumObject[valueStr as keyof T];\n            }\n        }\n        if (foundValue === undefined) {\n            console.warn(`[IniSection: ${this.name}] Invalid value for key \"${key}\". \"${valueStr}\" is not an accepted enum value.`);\n            return defaultValue;\n        }\n        return foundValue;\n    }\n    public getEnumNumeric<T extends object>(key: string, enumObject: T, defaultValue: number): number {\n        const valueStr = this.getString(key).trim();\n        if (!valueStr)\n            return defaultValue;\n        if (enumObject.hasOwnProperty(valueStr)) {\n            const enumVal = (enumObject as any)[valueStr];\n            if (typeof enumVal === 'number') {\n                return enumVal;\n            }\n            const parsedKey = parseInt(valueStr, 10);\n            if (Number.isInteger(parsedKey) && String(parsedKey) === valueStr) {\n                return parsedKey;\n            }\n        }\n        console.warn(`[IniSection: ${this.name}] Invalid value for key \"${key}\". \"${valueStr}\" is not an accepted numeric enum value.`);\n        return defaultValue;\n    }\n    public getEnumArray<T extends object>(key: string, enumObject: T, separator: RegExp = /,\\s*/, defaultValue: Array<T[keyof T]> = [], caseInsensitive: boolean = false): Array<T[keyof T]> {\n        const valueStr = this.getString(key).trim();\n        if (!valueStr)\n            return defaultValue;\n        const parts = valueStr.replace(/,$/, \"\").replace(/,+/g, \",\").split(separator);\n        const results: Array<T[keyof T]> = [];\n        for (const part of parts) {\n            if (!part && parts.length > 1) {\n                console.warn(`[IniSection: ${this.name}] Invalid empty value in enum array for key \"${key}\". Original string: \"${valueStr}\"`);\n                return defaultValue;\n            }\n            if (!part && parts.length === 1)\n                return defaultValue;\n            let found = false;\n            let foundValue: T[keyof T] | undefined = undefined;\n            if (caseInsensitive) {\n                const lowerPart = part.toLowerCase();\n                for (const enumKey in enumObject) {\n                    if (enumObject.hasOwnProperty(enumKey) && String(enumKey).toLowerCase() === lowerPart) {\n                        foundValue = enumObject[enumKey as keyof T];\n                        found = true;\n                        break;\n                    }\n                }\n            }\n            else {\n                if (enumObject.hasOwnProperty(part)) {\n                    foundValue = enumObject[part as keyof T];\n                    found = true;\n                }\n            }\n            if (found && foundValue !== undefined) {\n                results.push(foundValue);\n            }\n            else {\n                console.warn(`[IniSection: ${this.name}] Invalid value \"${part}\" in enum array for key \"${key}\". Original: \"${valueStr}\"`);\n                return defaultValue;\n            }\n        }\n        return results;\n    }\n    public getHighestNumericIndex(): number {\n        let maxIndex = -1;\n        this.entries.forEach((value, key) => {\n            const numKey = parseInt(key, 10);\n            if (!isNaN(numKey) && String(numKey) === key && numKey > maxIndex) {\n                maxIndex = numKey;\n            }\n        });\n        return maxIndex;\n    }\n    public isNumericIndexArray(): boolean {\n        for (const key of this.entries.keys()) {\n            if (/^\\d+$/.test(key)) {\n                return true;\n            }\n        }\n        return false;\n    }\n    public getConcatenatedValues(): string {\n        let result = \"\";\n        for (const value of this.entries.values()) {\n            if (typeof value === 'string') {\n                result += value;\n            }\n            else if (Array.isArray(value)) {\n                result += value.join('');\n            }\n        }\n        return result;\n    }\n    public toString(parentPrefix?: string): string {\n        const lines: string[] = [];\n        const currentPrefix = (parentPrefix ? `${parentPrefix}.` : \"\") + this.name;\n        lines.push(`[${currentPrefix}]`);\n        this.entries.forEach((value, key) => {\n            if (Array.isArray(value)) {\n                value.forEach(v => lines.push(`${key}[]=${v}`));\n            }\n            else {\n                lines.push(`${key}=${value}`);\n            }\n        });\n        lines.push(\"\");\n        const sectionStrings: string[] = [];\n        this.sections.forEach(section => {\n            sectionStrings.push(section.toString(currentPrefix));\n        });\n        return lines.join(\"\\r\\n\") + (sectionStrings.length > 0 ? \"\\r\\n\" + sectionStrings.join(\"\\r\\n\") : \"\");\n    }\n    public mergeWith(otherSection: IniSection): void {\n        if (this.isNumericIndexArray() && otherSection.isNumericIndexArray()) {\n            let nextIndex = this.getHighestNumericIndex() + 1;\n            otherSection.entries.forEach((value, key) => {\n                if (/^\\d+$/.test(key) && !Array.isArray(value)) {\n                    this.set(String(nextIndex++), value as string);\n                }\n                else {\n                    this.set(key, Array.isArray(value) ? [...value] : value);\n                }\n            });\n        }\n        else {\n            otherSection.entries.forEach((value, key) => {\n                this.set(key, Array.isArray(value) ? [...value] : value);\n            });\n        }\n        otherSection.sections.forEach((sectionToMerge, sectionName) => {\n            const existingSection = this.getOrCreateSection(sectionName);\n            existingSection.mergeWith(sectionToMerge);\n        });\n    }\n    public getOrCreateSection(sectionName: string): IniSection {\n        let section = this.sections.get(sectionName);\n        if (!section) {\n            section = new IniSection(sectionName);\n            this.sections.set(sectionName, section);\n        }\n        return section;\n    }\n    public getSection(sectionName: string): IniSection | undefined {\n        return this.sections.get(sectionName);\n    }\n    public getOrderedSections(): IniSection[] {\n        return [...this.sections.values()];\n    }\n}\n"
  },
  {
    "path": "src/data/MapFile.ts",
    "content": "import * as mapObjects from \"@/data/MapObjects\";\nimport { IniFile } from \"@/data/IniFile\";\nimport { TheaterType } from \"@/engine/TheaterType\";\nimport * as stringUtil from \"@/util/string\";\nimport { Format5 } from \"@/data/encoding/Format5\";\nimport { RgbBitmap } from \"@/data/Bitmap\";\nimport { TagsReader } from \"@/data/map/tag/TagsReader\";\nimport { TriggerReader } from \"@/data/map/trigger/TriggerReader\";\nimport { DataStream } from \"@/data/DataStream\";\nimport { MapLighting } from \"@/data/map/MapLighting\";\nimport { CellTagsReader } from \"@/data/map/tag/CellTagsReader\";\nimport { Variable } from \"@/data/map/Variable\";\nimport { SpecialFlags } from \"@/data/map/SpecialFlags\";\ntype MapTile = {\n    dx: number;\n    dy: number;\n    rx: number;\n    ry: number;\n    z: number;\n    tileNum: number;\n    subTile: number;\n};\ntype Waypoint = {\n    number: number;\n    rx: number;\n    ry: number;\n};\nexport class MapFile extends IniFile {\n    static artSectionPrefix = \"ART\";\n    declare fullSize: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    declare localSize: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    declare theaterType: TheaterType;\n    declare iniFormat: number;\n    declare tiles: MapTile[];\n    declare maxTileNum: number;\n    declare waypoints: Waypoint[];\n    declare structures: mapObjects.Structure[];\n    declare vehicles: mapObjects.Vehicle[];\n    declare infantries: mapObjects.Infantry[];\n    declare aircrafts: mapObjects.Aircraft[];\n    declare terrains: mapObjects.Terrain[];\n    declare overlays: mapObjects.Overlay[];\n    declare maxOverlayId: number;\n    declare smudges: mapObjects.Smudge[];\n    declare lighting: MapLighting;\n    declare ionLighting: MapLighting;\n    declare tags: any;\n    declare triggers: any;\n    declare unknownEventTypes: any;\n    declare unknownActionTypes: any;\n    declare cellTags: any;\n    declare variables: Map<number, Variable>;\n    declare startingLocations: {\n        x: number;\n        y: number;\n    }[];\n    declare specialFlags: SpecialFlags;\n    declare artOverrides?: IniFile;\n    fromString(iniString: string) {\n        super.fromString(iniString);\n        const mapSection = this.getSection(\"Map\");\n        if (!mapSection) {\n            throw new Error(\"[Map] section not found\");\n        }\n        const size = mapSection.getNumberArray(\"Size\");\n        this.fullSize = {\n            x: size[0],\n            y: size[1],\n            width: size[2],\n            height: size[3],\n        };\n        const localSize = mapSection.getNumberArray(\"LocalSize\");\n        this.localSize = {\n            x: localSize[0],\n            y: localSize[1],\n            width: localSize[2],\n            height: localSize[3],\n        };\n        this.theaterType = mapSection.getEnum(\"Theater\", TheaterType, TheaterType.None, true);\n        if (this.theaterType === TheaterType.None) {\n            throw new Error(`Unsupported theater type \"${mapSection.getString(\"Theater\")}\"`);\n        }\n        const basicSection = this.getSection(\"Basic\");\n        this.iniFormat = basicSection?.getNumber(\"NewINIFormat\") ?? 0;\n        this.readTiles();\n        this.readWaypoints(this.getOrCreateSection(\"Waypoints\"));\n        this.readStructures(this.getOrCreateSection(\"Structures\"));\n        this.readVehicles();\n        this.readInfantries();\n        this.readAircrafts();\n        this.readTerrains(this.getOrCreateSection(\"Terrain\"));\n        this.readOverlays();\n        this.readSmudges();\n        this.readLighting();\n        this.readTagsAndTriggers();\n        this.readCellTags(this.iniFormat);\n        this.readVariableNames();\n        this.startingLocations = this.readStartingLocations(this.waypoints);\n        this.specialFlags = new SpecialFlags().read(this.getOrCreateSection(\"SpecialFlags\"));\n        return this;\n    }\n    fromJson(i: any) {\n        if (i[MapFile.artSectionPrefix]) {\n            let { [MapFile.artSectionPrefix]: e, ...t } = i;\n            (this.artOverrides = new IniFile(e)), (i = t);\n        }\n        return super.fromJson(i);\n    }\n    readStartingLocations(waypoints: Waypoint[]) {\n        const startingLocations: {\n            x: number;\n            y: number;\n        }[] = [];\n        for (const waypoint of waypoints\n            .filter((entry) => entry.number < 8)\n            .sort((left, right) => left.number - right.number)) {\n            startingLocations.push({ x: waypoint.rx, y: waypoint.ry });\n        }\n        return startingLocations;\n    }\n    readLighting() {\n        var e = this.getOrCreateSection(\"Lighting\");\n        (this.lighting = new MapLighting().read(e)),\n            (this.ionLighting = new MapLighting().read(e, \"Ion\")),\n            (this.ionLighting.forceTint = true);\n    }\n    readTagsAndTriggers() {\n        const tagsSection = this.getOrCreateSection(\"Tags\");\n        this.tags = new TagsReader().read(tagsSection);\n        const triggersSection = this.getOrCreateSection(\"Triggers\");\n        const eventsSection = this.getOrCreateSection(\"Events\");\n        const actionsSection = this.getOrCreateSection(\"Actions\");\n        const { triggers, unknownEventTypes, unknownActionTypes, } = new TriggerReader().read(triggersSection, eventsSection, actionsSection, this.tags);\n        this.triggers = triggers;\n        this.unknownEventTypes = unknownEventTypes;\n        this.unknownActionTypes = unknownActionTypes;\n    }\n    readCellTags(e: number) {\n        this.cellTags = new CellTagsReader().read(this.getOrCreateSection(\"CellTags\"), e);\n    }\n    readVariableNames() {\n        const section = this.getOrCreateSection(\"VariableNames\");\n        const variables = new Map<number, Variable>();\n        for (const [key, rawValue] of section.entries) {\n            const index = Number(key);\n            if (Number.isNaN(index)) {\n                console.warn(`Map [VariableNames] contains non-numeric index \"${key}\". Skipping.`);\n                continue;\n            }\n            const value = this.normalizeIniEntryValue(rawValue);\n            const [name = \"\", isGlobal = \"0\"] = value.split(\",\");\n            variables.set(index, new Variable(name, Boolean(Number(isGlobal))));\n        }\n        this.variables = variables;\n    }\n    readTiles() {\n        let e = this.getSection(\"IsoMapPack5\");\n        if (!e)\n            throw new Error(\"[IsoMapPack5] section not found\");\n        var t = stringUtil.base64StringToUint8Array(e.getConcatenatedValues()), i = (2 * this.fullSize.width - 1) * this.fullSize.height, decodedData = new Uint8Array(11 * i + 4);\n        Format5.decodeInto(t, decodedData);\n        let s = new DataStream(decodedData.buffer), a = 2 * this.fullSize.width - 1;\n        var n, o, l, c, height = this.fullSize.height, h = (e: number, t: number) => t * a + e;\n        this.tiles = new Array(a * height);\n        for (let T = (this.maxTileNum = 0); T < i; T++) {\n            const rx = s.readUint16();\n            const ry = s.readUint16();\n            const tileNum = Math.max(0, s.readInt16());\n            this.maxTileNum = Math.max(this.maxTileNum, tileNum);\n            s.readInt16();\n            const subTile = s.readUint8();\n            const z = s.readUint8();\n            s.readUint8();\n            const dx = rx - ry + this.fullSize.width - 1;\n            const dy = rx + ry - this.fullSize.width - 1;\n            if (0 <= dx &&\n                dx < 2 * this.fullSize.width &&\n                0 <= dy &&\n                dy < 2 * this.fullSize.height) {\n                const tile: MapTile = {\n                    dx,\n                    dy,\n                    rx,\n                    ry,\n                    z,\n                    tileNum,\n                    subTile,\n                };\n                this.tiles[h(dx, Math.floor(dy / 2))] = tile;\n            }\n        }\n        for (let v = 0; v < this.fullSize.height; v++)\n            for (let e = 0; e <= 2 * this.fullSize.width - 2; e++)\n                this.tiles[h(e, v)] ||\n                    ((n = e),\n                        (c =\n                            (o = 2 * v + (e % 2)) -\n                                (l = (n + o) / 2 + 1) +\n                                this.fullSize.width +\n                                1),\n                        (this.tiles[h(e, v)] = {\n                            dx: n,\n                            dy: o,\n                            rx: l,\n                            ry: c,\n                            z: 0,\n                            tileNum: 0,\n                            subTile: 0,\n                        }));\n    }\n    readWaypoints(e: any) {\n        this.waypoints = [];\n        for (const [key, rawValue] of e.entries) {\n            const number = parseInt(key, 10);\n            const value = parseInt(this.normalizeIniEntryValue(rawValue), 10);\n            if (Number.isNaN(number) || Number.isNaN(value)) {\n                continue;\n            }\n            const ry = Math.floor(value / 1000);\n            const rx = value - 1000 * ry;\n            this.waypoints.push({ number, rx, ry });\n        }\n    }\n    readStructures(e: any) {\n        this.structures = [];\n        for (const [, rawValue] of e.entries) {\n            const values = this.normalizeIniEntryValue(rawValue).split(\",\");\n            if (values.length > 15) {\n                const structure = new mapObjects.Structure();\n                structure.owner = values[0];\n                structure.name = values[1];\n                structure.health = Number(values[2]);\n                structure.rx = Number(values[3]);\n                structure.ry = Number(values[4]);\n                structure.tag = this.readTagId(values[6]);\n                structure.poweredOn = Boolean(Number(values[9]));\n                this.structures.push(structure);\n            }\n        }\n    }\n    readTagId(e: string) {\n        return \"none\" !== e.toLowerCase() ? e : undefined;\n    }\n    readVehicles() {\n        this.vehicles = [];\n        const section = this.getSection(\"Units\");\n        if (!section) {\n            return;\n        }\n        for (const rawValue of section.entries.values()) {\n            const values = this.normalizeIniEntryValue(rawValue).split(\",\");\n            if (values.length <= 11) {\n                console.warn(`Invalid Vehicle entry: \"${this.normalizeIniEntryValue(rawValue)}\"`);\n                continue;\n            }\n            const vehicle = new mapObjects.Vehicle();\n            vehicle.owner = values[0];\n            vehicle.name = values[1];\n            vehicle.health = Number(values[2]);\n            vehicle.rx = Number(values[3]);\n            vehicle.ry = Number(values[4]);\n            vehicle.direction = Number(values[5]);\n            vehicle.tag = this.readTagId(values[7]);\n            vehicle.veterancy = Number(values[8]);\n            vehicle.onBridge = values[10] === \"1\";\n            this.vehicles.push(vehicle);\n        }\n    }\n    readInfantries() {\n        this.infantries = [];\n        const section = this.getSection(\"Infantry\");\n        if (!section) {\n            return;\n        }\n        for (const rawValue of section.entries.values()) {\n            const values = this.normalizeIniEntryValue(rawValue).split(\",\");\n            if (values.length <= 8) {\n                console.warn(`Invalid Infantry entry: \"${this.normalizeIniEntryValue(rawValue)}\"`);\n                continue;\n            }\n            const infantry = new mapObjects.Infantry();\n            infantry.owner = values[0];\n            infantry.name = values[1];\n            infantry.health = Number(values[2]);\n            infantry.rx = Number(values[3]);\n            infantry.ry = Number(values[4]);\n            infantry.subCell = Number(values[5]);\n            infantry.direction = Number(values[7]);\n            infantry.tag = this.readTagId(values[8]);\n            infantry.veterancy = Number(values[9]);\n            infantry.onBridge = values[11] === \"1\";\n            this.infantries.push(infantry);\n        }\n    }\n    readAircrafts() {\n        this.aircrafts = [];\n        const section = this.getSection(\"Aircraft\");\n        if (!section) {\n            return;\n        }\n        for (const rawValue of section.entries.values()) {\n            const values = this.normalizeIniEntryValue(rawValue).split(\",\");\n            const aircraft = new mapObjects.Aircraft();\n            aircraft.owner = values[0];\n            aircraft.name = values[1];\n            aircraft.health = Number(values[2]);\n            aircraft.rx = Number(values[3]);\n            aircraft.ry = Number(values[4]);\n            aircraft.direction = Number(values[5]);\n            aircraft.tag = this.readTagId(values[7]);\n            aircraft.veterancy = Number(values[8]);\n            aircraft.onBridge = values[values.length - 4] === \"1\";\n            this.aircrafts.push(aircraft);\n        }\n    }\n    readTerrains(e: any) {\n        this.terrains = [];\n        for (const [key, rawValue] of e.entries) {\n            const tileIndex = Number(key);\n            if (!Number.isNaN(tileIndex)) {\n                const terrain = new mapObjects.Terrain();\n                terrain.name = this.normalizeIniEntryValue(rawValue);\n                terrain.rx = tileIndex % 1000;\n                terrain.ry = Math.floor(tileIndex / 1000);\n                this.terrains.push(terrain);\n            }\n        }\n    }\n    readOverlays() {\n        (this.overlays = []), (this.maxOverlayId = 0);\n        let t = this.getSection(\"OverlayPack\");\n        if (t) {\n            var i = stringUtil.base64StringToUint8Array(t.getConcatenatedValues()), overlayData = new Uint8Array(1 << 18);\n            Format5.decodeInto(i, overlayData, 80);\n            let e = this.getSection(\"OverlayDataPack\");\n            if (e) {\n                var i = stringUtil.base64StringToUint8Array(e.getConcatenatedValues()), s = new Uint8Array(1 << 18);\n                Format5.decodeInto(i, s, 80);\n                for (let t = 0; t < this.fullSize.height; t++)\n                    for (let e = 2 * this.fullSize.width - 2; 0 <= e; e--) {\n                        var a = e, n = 2 * t + (e % 2), o = (a + n) / 2 + 1, l = n - o + this.fullSize.width + 1, a = o + 512 * l, n = overlayData[a];\n                        if (255 !== n) {\n                            a = s[a];\n                            let e = new mapObjects.Overlay();\n                            (e.id = n),\n                                (e.value = a),\n                                (e.rx = o),\n                                (e.ry = l),\n                                this.overlays.push(e),\n                                (this.maxOverlayId = Math.max(this.maxOverlayId, n));\n                        }\n                    }\n            }\n            else\n                console.warn(\"[OverlayDataPack] section not found. Skipping.\");\n        }\n        else\n            console.warn(\"[Overlay] section not found. Skipping.\");\n    }\n    readSmudges() {\n        this.smudges = [];\n        const section = this.getSection(\"Smudge\");\n        if (!section) {\n            return;\n        }\n        for (const rawValue of section.entries.values()) {\n            const values = this.normalizeIniEntryValue(rawValue).split(\",\");\n            if (values.length <= 2) {\n                console.warn(`Invalid Smudge entry: \"${this.normalizeIniEntryValue(rawValue)}\"`);\n                continue;\n            }\n            const smudge = new mapObjects.Smudge();\n            smudge.name = values[0];\n            smudge.rx = Number(values[1]);\n            smudge.ry = Number(values[2]);\n            this.smudges.push(smudge);\n        }\n    }\n    decodePreviewImage() {\n        let e = this.getSection(\"Preview\"), t = this.getSection(\"PreviewPack\");\n        if (e && t) {\n            var [, , i, r] = e.getArray(\"Size\").map((e) => Number(e)), s = stringUtil.base64StringToUint8Array(t.getConcatenatedValues()), bitmap = new RgbBitmap(i, r);\n            return Format5.decodeInto(s, bitmap.data), bitmap;\n        }\n    }\n    private normalizeIniEntryValue(value: string | string[]): string {\n        return Array.isArray(value) ? value.join(\",\") : value;\n    }\n}\n"
  },
  {
    "path": "src/data/MapObjects.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nexport class MapObject {\n    type: ObjectType;\n    constructor(type: ObjectType) {\n        this.type = type;\n    }\n    isStructure(): boolean {\n        return this.type === ObjectType.Building;\n    }\n    isVehicle(): boolean {\n        return this.type === ObjectType.Vehicle;\n    }\n    isInfantry(): boolean {\n        return this.type === ObjectType.Infantry;\n    }\n    isAircraft(): boolean {\n        return this.type === ObjectType.Aircraft;\n    }\n    isTerrain(): boolean {\n        return this.type === ObjectType.Terrain;\n    }\n    isSmudge(): boolean {\n        return this.type === ObjectType.Smudge;\n    }\n    isOverlay(): boolean {\n        return this.type === ObjectType.Overlay;\n    }\n    isNamed(): boolean {\n        return \"name\" in this;\n    }\n    isTechno(): boolean {\n        return \"health\" in this;\n    }\n}\nexport class PositionedMapObject extends MapObject {\n    rx = 0;\n    ry = 0;\n}\nexport class NamedMapObject extends PositionedMapObject {\n    name = \"\";\n}\nexport class TechnoObject extends NamedMapObject {\n    owner = \"\";\n    health = 0;\n    direction = 0;\n    tag?: string;\n    veterancy = 0;\n    onBridge = false;\n}\nexport class TechnoTypeObject extends TechnoObject {\n}\nexport class Structure extends TechnoTypeObject {\n    poweredOn = false;\n    constructor() {\n        super(ObjectType.Building);\n    }\n}\nexport class Vehicle extends TechnoTypeObject {\n    constructor() {\n        super(ObjectType.Vehicle);\n    }\n}\nexport class Infantry extends TechnoTypeObject {\n    subCell = 0;\n    constructor() {\n        super(ObjectType.Infantry);\n    }\n}\nexport class Aircraft extends TechnoTypeObject {\n    constructor() {\n        super(ObjectType.Aircraft);\n    }\n}\nexport class Terrain extends NamedMapObject {\n    constructor() {\n        super(ObjectType.Terrain);\n    }\n}\nexport class Smudge extends NamedMapObject {\n    constructor() {\n        super(ObjectType.Smudge);\n    }\n}\nexport class Overlay extends PositionedMapObject {\n    id = 0;\n    value = 0;\n    constructor() {\n        super(ObjectType.Overlay);\n    }\n}\n"
  },
  {
    "path": "src/data/MixEntry.ts",
    "content": "import { binaryStringToUint8Array } from '../util/string';\nimport { Crc32 } from './Crc32';\nexport class MixEntry {\n    public static readonly size: number = 12;\n    public readonly hash: number;\n    public readonly offset: number;\n    public readonly length: number;\n    constructor(hash: number, offset: number, length: number) {\n        this.hash = hash;\n        this.offset = offset;\n        this.length = length;\n    }\n    public static hashFilename(filename: string, debugLog: boolean = false): number {\n        let processedName = filename.toUpperCase();\n        const originalLength = processedName.length;\n        const R = originalLength >> 2;\n        if (debugLog)\n            console.log(`[hashFilename] Original: \"${filename}\", Uppercased: \"${processedName}\", Length: ${originalLength}`);\n        if ((originalLength & 3) !== 0) {\n            const appendCharCode = originalLength - (R << 2);\n            processedName += String.fromCharCode(appendCharCode);\n            if (debugLog)\n                console.log(`[hashFilename] Appended char code: ${appendCharCode}, Name after append: \"${processedName}\"`);\n            let numPaddingChars = 3 - (originalLength & 3);\n            const paddingCharSourceIndex = R << 2;\n            const charToPadCode = processedName.charCodeAt(paddingCharSourceIndex < processedName.length ? paddingCharSourceIndex : 0);\n            const charToPad = String.fromCharCode(charToPadCode);\n            if (debugLog)\n                console.log(`[hashFilename] numPaddingChars: ${numPaddingChars}, paddingCharSourceIndex: ${paddingCharSourceIndex}, charToPad: \"${charToPad}\" (code ${charToPadCode})`);\n            for (let i = 0; i < numPaddingChars; i++) {\n                processedName += charToPad;\n            }\n            if (debugLog)\n                console.log(`[hashFilename] Name after padding: \"${processedName}\", Final Length: ${processedName.length}`);\n        }\n        const nameBytes = binaryStringToUint8Array(processedName);\n        if (debugLog)\n            console.log(`[hashFilename] nameBytes for CRC:`, nameBytes);\n        const crc = Crc32.calculateCrc(nameBytes);\n        if (debugLog)\n            console.log(`[hashFilename] Calculated CRC: ${crc} (0x${crc.toString(16).toUpperCase()})`);\n        return crc;\n    }\n}\n"
  },
  {
    "path": "src/data/MixFile.ts",
    "content": "import { DataStream } from \"./DataStream\";\nimport { Blowfish } from \"./encoding/Blowfish\";\nimport { BlowfishKey } from \"./encoding/BlowfishKey\";\nimport { MixEntry } from \"./MixEntry\";\nimport { VirtualFile } from \"./vfs/VirtualFile\";\nenum MixFileFlags {\n    Checksum = 0x00010000,\n    Encrypted = 0x00020000\n}\nexport class MixFile {\n    private stream: DataStream;\n    private headerStart = 84;\n    private index: Map<number, MixEntry>;\n    private dataStart: number = 0;\n    constructor(stream: DataStream) {\n        this.stream = stream;\n        this.index = new Map<number, MixEntry>();\n        this.parseHeader();\n    }\n    private parseHeader(): void {\n        const flags = this.stream.readUint32();\n        const isWestwoodMix = (flags & ~(MixFileFlags.Checksum | MixFileFlags.Encrypted)) === 0;\n        if (isWestwoodMix) {\n            if ((flags & MixFileFlags.Encrypted) !== 0) {\n                this.dataStart = this.parseRaHeader();\n                return;\n            }\n        }\n        else {\n            this.stream.seek(0);\n        }\n        this.dataStart = this.parseTdHeader(this.stream);\n    }\n    private parseRaHeader(): number {\n        const e = this.stream;\n        var t: any = e.readUint8Array(80), i: any = new BlowfishKey().decryptKey(t), r: any = e.readUint32Array(2);\n        const s = new Blowfish(i);\n        let a = new DataStream(s.decrypt(r));\n        t = a.readUint16();\n        a.readUint32(), (e.position = this.headerStart);\n        (i = 6 + t * MixEntry.size),\n            (t = ((3 + i) / 4) | 0),\n            (r = e.readUint32Array(t + (t % 2)));\n        a = new DataStream(s.decrypt(r));\n        i = this.headerStart + i + ((1 + (~i >>> 0)) & 7);\n        this.parseTdHeader(a);\n        return i;\n    }\n    private parseTdHeader(e: DataStream): number {\n        var t = e.readUint16();\n        e.readUint32();\n        let successfulEntries = 0;\n        let failedEntries = 0;\n        let duplicateHashes = 0;\n        const seenHashes = new Set<number>();\n        for (let r = 0; r < t; r++) {\n            try {\n                if (e.position + 12 > e.byteLength) {\n                    console.log(`[Our] Entry ${r + 1}: Not enough data remaining. Position: ${e.position}, Remaining: ${e.byteLength - e.position}`);\n                    failedEntries++;\n                    break;\n                }\n                var i = new MixEntry(e.readUint32(), e.readUint32(), e.readUint32());\n                if (r < 5) {\n                    console.log(`[Our] Entry ${r + 1}: hash=0x${i.hash.toString(16).toUpperCase()}, offset=${i.offset}, length=${i.length}`);\n                    const currentPos = e.position - 12;\n                    const rawBytes = new Uint8Array(e.buffer, e.byteOffset + currentPos, 12);\n                    console.log(`[Our] Entry ${r + 1} raw bytes:`, Array.from(rawBytes));\n                }\n                if (seenHashes.has(i.hash)) {\n                    duplicateHashes++;\n                    if (duplicateHashes <= 10) {\n                        console.log(`[Our] Duplicate hash detected at entry ${r + 1}: 0x${i.hash.toString(16).toUpperCase()}`);\n                    }\n                }\n                else {\n                    seenHashes.add(i.hash);\n                }\n                this.index.set(i.hash, i);\n                successfulEntries++;\n            }\n            catch (error) {\n                console.log(`[Our] Entry ${r + 1}: Error reading entry:`, error);\n                failedEntries++;\n                break;\n            }\n        }\n        return e.position;\n    }\n    public containsFile(filename: string): boolean {\n        return this.index.has(MixEntry.hashFilename(filename));\n    }\n    public openFile(filename: string): VirtualFile {\n        const fileId = MixEntry.hashFilename(filename);\n        const entry = this.index.get(fileId);\n        if (!entry) {\n            throw new Error(`File \"${filename}\" not found`);\n        }\n        return VirtualFile.factory(this.stream, filename, this.dataStart + entry.offset, entry.length);\n    }\n}\n"
  },
  {
    "path": "src/data/Mp3File.ts",
    "content": "import type { VirtualFile } from \"./vfs/VirtualFile\";\nimport type { DataStream } from \"./DataStream\";\nexport class Mp3File {\n    private sourceData: VirtualFile | DataStream | Blob;\n    private fileName: string;\n    constructor(source: VirtualFile | DataStream | Blob | File, fileName?: string) {\n        this.sourceData = source;\n        if (source instanceof File) {\n            this.fileName = fileName || source.name || 'unknown.mp3';\n        }\n        else if (typeof (source as VirtualFile).filename === 'string') {\n            this.fileName = fileName || (source as VirtualFile).filename;\n        }\n        else {\n            this.fileName = fileName || 'unknown.mp3';\n        }\n    }\n    asFile(): File {\n        let blob: Blob;\n        if (this.sourceData instanceof Blob) {\n            blob = this.sourceData;\n        }\n        else if (typeof (this.sourceData as VirtualFile).getBytes === 'function') {\n            const bytes = (this.sourceData as VirtualFile).getBytes();\n            blob = new Blob([bytes as any], { type: \"audio/mp3\" });\n        }\n        else if ((this.sourceData as DataStream).buffer) {\n            const ds = this.sourceData as DataStream;\n            const bytes = new Uint8Array(ds.buffer, ds.byteOffset, ds.byteLength);\n            blob = new Blob([bytes as any], { type: \"audio/mp3\" });\n        }\n        else {\n            throw new Error(\"Mp3File: Cannot convert source data to Blob.\");\n        }\n        return new File([blob], this.fileName, {\n            type: \"audio/mp3\",\n        });\n    }\n    getBlob(): Blob {\n        if (this.sourceData instanceof Blob) {\n            return this.sourceData;\n        }\n        const file = this.asFile();\n        return file;\n    }\n}\n"
  },
  {
    "path": "src/data/Palette.ts",
    "content": "import { Color } from \"../util/Color\";\nimport { fnv32a } from \"../util/math\";\nimport { VirtualFile } from \"./vfs/VirtualFile\";\nimport { DataStream } from \"./DataStream\";\nexport class Palette {\n    public static REMAP_START_IDX = 16;\n    public colors: Color[] = [];\n    private _hash: number = 0;\n    static fromVirtualFile(file: VirtualFile): Palette {\n        const palette = new Palette({ colors: [] });\n        palette.fromVirtualFile(file);\n        return palette;\n    }\n    constructor(source?: VirtualFile | Uint8Array | number[] | {\n        colors: Color[];\n        hashVal?: number;\n    }) {\n        if (source instanceof VirtualFile) {\n            this.fromVirtualFile(source);\n        }\n        else if (source instanceof Uint8Array || Array.isArray(source)) {\n            this.fromJsonCompatible(source as Uint8Array | number[]);\n        }\n        else if (typeof source === 'object' && source !== null && 'colors' in source) {\n            this.colors = source.colors.map(c => new Color(c.r, c.g, c.b));\n            this._hash = source.hashVal ?? this.computeHash(this.colors);\n        }\n        else {\n        }\n    }\n    private fromVirtualFile(vf: VirtualFile): void {\n        const rawData = (vf.stream as DataStream).readUint8Array(768);\n        this.fromJsonCompatible(rawData);\n    }\n    private fromJsonCompatible(data: Uint8Array | number[]): void {\n        this.colors = [];\n        for (let i = 0; i < data.length / 3; ++i) {\n            this.colors.push(Color.fromRgb(data[3 * i] * 4, data[3 * i + 1] * 4, data[3 * i + 2] * 4));\n        }\n        if (this.colors.length > 256) {\n            this.colors.length = 256;\n        }\n        this._hash = this.computeHash(this.colors);\n    }\n    getColor(index: number): Color {\n        return this.colors[index] ?? Color.fromRgb(0, 0, 0);\n    }\n    getColorAsHex(index: number): number {\n        return this.getColor(index).asHex();\n    }\n    setColors(newColors: Color[]): void {\n        this.colors = newColors.map(c => c.clone());\n        if (this.colors.length > 256) {\n            this.colors.length = 256;\n        }\n        this._hash = this.computeHash(this.colors);\n    }\n    get size(): number {\n        return this.colors.length;\n    }\n    get hash(): number {\n        return this._hash;\n    }\n    private computeHash(colorArray: Color[]): number {\n        const buffer = new Uint8Array(3 * colorArray.length);\n        let j = 0;\n        for (const color of colorArray) {\n            buffer[j++] = color.r;\n            buffer[j++] = color.g;\n            buffer[j++] = color.b;\n        }\n        return fnv32a(buffer);\n    }\n    clone(): Palette {\n        return new Palette({ colors: this.colors.map((c) => c.clone()), hashVal: this._hash });\n    }\n    remap(baseColor: Color): Palette {\n        const remapFactors = [\n            63, 59, 55, 52, 48, 44, 41, 37, 33, 30, 26, 22, 19, 15, 11, 8,\n        ];\n        if (this.colors.length < Palette.REMAP_START_IDX + remapFactors.length) {\n            console.warn(\"Palette too small to remap fully.\");\n        }\n        for (let i = 0; i < remapFactors.length; ++i) {\n            const targetIndex = Palette.REMAP_START_IDX + i;\n            if (targetIndex < this.colors.length) {\n                const factor = remapFactors[i];\n                this.colors[targetIndex].r = Math.floor((baseColor.r / 255) * factor * 4);\n                this.colors[targetIndex].g = Math.floor((baseColor.g / 255) * factor * 4);\n                this.colors[targetIndex].b = Math.floor((baseColor.b / 255) * factor * 4);\n            }\n            else {\n                break;\n            }\n        }\n        this._hash = this.computeHash(this.colors);\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/data/PcxFile.ts",
    "content": "import PcxJs from '@ra2web/pcxfile';\nimport { CanvasUtils } from '../engine/gfx/CanvasUtils';\nimport type { VirtualFile } from './vfs/VirtualFile';\nimport { DataStream } from './DataStream';\nexport class PcxFile {\n    public width: number;\n    public height: number;\n    public data: Uint8Array;\n    private fileSource: VirtualFile | DataStream;\n    constructor(source: VirtualFile | DataStream) {\n        this.fileSource = source;\n        let dataViewProvider: {\n            buffer: ArrayBuffer;\n            byteOffset: number;\n            byteLength: number;\n        };\n        if ('stream' in source && source.stream instanceof DataStream) {\n            const stream = source.stream;\n            dataViewProvider = stream;\n        }\n        else if (source instanceof DataStream) {\n            dataViewProvider = source;\n        }\n        else {\n            throw new Error(\"PcxFile constructor: Unsupported source type.\");\n        }\n        const pcxData = new Uint8Array(dataViewProvider.buffer, dataViewProvider.byteOffset, dataViewProvider.byteLength);\n        const pcxParser = new PcxJs(pcxData);\n        const decoded = pcxParser.decode();\n        if (!decoded || !decoded.pixelArray) {\n            throw new Error(\"Failed to decode PCX data.\");\n        }\n        this.width = decoded.width;\n        this.height = decoded.height;\n        this.data = decoded.pixelArray;\n        this.fixAlpha(this.data);\n    }\n    static fromVirtualFile(vf: VirtualFile): PcxFile {\n        return new PcxFile(vf);\n    }\n    async toPngBlob(): Promise<Blob | null> {\n        const canvas = this.toCanvas();\n        return await CanvasUtils.canvasToBlob(canvas);\n    }\n    toDataUrl(): string {\n        return this.toCanvas().toDataURL();\n    }\n    toCanvas(): HTMLCanvasElement {\n        return CanvasUtils.canvasFromRgbaImageData(this.data, this.width, this.height);\n    }\n    private fixAlpha(rgbaPixelArray: Uint8Array | Uint8ClampedArray): void {\n        for (let i = 0; i < rgbaPixelArray.length; i += 4) {\n            if (rgbaPixelArray[i] === 255 &&\n                rgbaPixelArray[i + 1] === 0 &&\n                rgbaPixelArray[i + 2] === 255) {\n                rgbaPixelArray[i + 3] = 0;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/data/ShpFile.ts",
    "content": "import { Format3 } from \"./encoding/Format3\";\nimport { ShpImage } from \"./ShpImage\";\nimport { VirtualFile } from \"./vfs/VirtualFile\";\nimport { DataStream } from \"./DataStream\";\ninterface ShpFrameHeader {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n    compressionType: number;\n    imageDataStartOffset: number;\n}\nexport class ShpFile {\n    public width: number = 0;\n    public height: number = 0;\n    public numImages: number = 0;\n    public images: ShpImage[] = [];\n    public filename?: string;\n    static fromVirtualFile(file: VirtualFile): ShpFile {\n        const shpFile = new ShpFile();\n        shpFile.fromVirtualFile(file);\n        return shpFile;\n    }\n    constructor(file?: VirtualFile) {\n        if (file instanceof VirtualFile) {\n            this.fromVirtualFile(file);\n        }\n    }\n    private fromVirtualFile(file: VirtualFile): void {\n        this.filename = file.filename;\n        const s = file.stream as DataStream;\n        const reserved = s.readInt16();\n        if (reserved === 0) {\n            this.width = s.readInt16();\n            this.height = s.readInt16();\n            this.numImages = s.readInt16();\n        }\n        else {\n            s.seek(0);\n            this.numImages = s.readUint16();\n            console.warn(`ShpFile ${this.filename}: Non-standard SHP header (reserved field was ${reserved}). Attempting to read as potentially TS-like format.`);\n            this.width = 0;\n            this.height = 0;\n        }\n        if (this.numImages <= 0 || this.numImages > 4096) {\n            console.error(`ShpFile ${this.filename}: Invalid number of images: ${this.numImages}. Stopping parse.`);\n            this.numImages = 0;\n            return;\n        }\n        const frameHeaders: ShpFrameHeader[] = [];\n        const frameHeaderBaseOffset = s.position;\n        const frameDescriptorSize = 2 + 2 + 2 + 2 + 1 + 3 + 4 + 4 + 4;\n        for (let i = 0; i < this.numImages; ++i) {\n            frameHeaders.push(this.readFrameHeader(s));\n        }\n        this.images = [];\n        let maxWidth = 0;\n        let maxHeight = 0;\n        for (let i = 0; i < this.numImages; ++i) {\n            const header = frameHeaders[i];\n            const { x, y, width: frameWidth, height: frameHeight, compressionType, imageDataStartOffset } = header;\n            let nextOffset: number;\n            if (i < this.numImages - 1) {\n                nextOffset = frameHeaders[i + 1].imageDataStartOffset;\n            }\n            else {\n                s.seek(0);\n                nextOffset = s.byteLength;\n            }\n            if (nextOffset < imageDataStartOffset) {\n                nextOffset = s.byteLength;\n            }\n            let imageDataLength = nextOffset - imageDataStartOffset;\n            if (imageDataStartOffset + imageDataLength > s.byteLength) {\n                imageDataLength = s.byteLength - imageDataStartOffset;\n            }\n            if (imageDataLength <= 0 && !(frameWidth === 0 && frameHeight === 0)) {\n                console.warn(`ShpFile ${this.filename}, frame ${i}: Zero or negative image data length (${imageDataLength}) for non-empty frame dimensions (${frameWidth}x${frameHeight}). Skipping frame data read.`);\n                const emptyImage = new ShpImage(new Uint8Array(0), frameWidth, frameHeight, x, y);\n                this.images.push(emptyImage);\n                maxWidth = Math.max(maxWidth, x + frameWidth);\n                maxHeight = Math.max(maxHeight, y + frameHeight);\n                continue;\n            }\n            s.seek(imageDataStartOffset);\n            const imageData = this.readImageData(s, frameWidth, frameHeight, compressionType, imageDataLength);\n            const image = new ShpImage(imageData, frameWidth, frameHeight, x, y);\n            this.images.push(image);\n            maxWidth = Math.max(maxWidth, x + frameWidth);\n            maxHeight = Math.max(maxHeight, y + frameHeight);\n        }\n        if (reserved !== 0) {\n            this.width = maxWidth;\n            this.height = maxHeight;\n        }\n    }\n    private readFrameHeader(s: DataStream): ShpFrameHeader {\n        const x = s.readInt16();\n        const y = s.readInt16();\n        const width = s.readInt16();\n        const height = s.readInt16();\n        const compressionType = s.readUint8();\n        s.readUint8();\n        s.readUint8();\n        s.readUint8();\n        s.readInt32();\n        s.readInt32();\n        const imageDataStartOffset = s.readInt32();\n        return {\n            x,\n            y,\n            width,\n            height,\n            compressionType,\n            imageDataStartOffset,\n        };\n    }\n    private readImageData(s: DataStream, width: number, height: number, compressionType: number, expectedLength: number): Uint8Array {\n        const uncompressedSize = width * height;\n        if (uncompressedSize === 0)\n            return new Uint8Array(0);\n        if (expectedLength <= 0 && compressionType > 1) {\n            console.warn(`ShpFile: readImageData called with expectedLength ${expectedLength} for compressed type ${compressionType}`);\n            return new Uint8Array(uncompressedSize);\n        }\n        if (compressionType <= 1) {\n            const bytesToRead = Math.min(expectedLength, uncompressedSize);\n            if (s.position + bytesToRead > s.byteLength) {\n                console.error(`ShpFile: Not enough data in stream to read uncompressed image. Pos: ${s.position}, Need: ${bytesToRead}, Total: ${s.byteLength}`);\n                return new Uint8Array(uncompressedSize);\n            }\n            const data = s.readUint8Array(bytesToRead);\n            if (bytesToRead < uncompressedSize) {\n                const paddedData = new Uint8Array(uncompressedSize);\n                paddedData.set(data);\n                return paddedData;\n            }\n            return data;\n        }\n        else if (compressionType === 2) {\n            const decodedData = new Uint8Array(uncompressedSize);\n            let destIndex = 0;\n            for (let i = 0; i < height; ++i) {\n                if (s.position + 2 > s.byteLength)\n                    break;\n                const lineRunLength = s.readUint16() - 2;\n                if (lineRunLength < 0 || s.position + lineRunLength > s.byteLength)\n                    break;\n                const lineData = s.readUint8Array(lineRunLength);\n                if (destIndex + lineRunLength <= uncompressedSize) {\n                    decodedData.set(lineData, destIndex);\n                }\n                destIndex += lineRunLength;\n            }\n            return decodedData;\n        }\n        else if (compressionType === 3) {\n            if (s.position + expectedLength > s.byteLength) {\n                console.error(`ShpFile: Not enough data for Format3 block. Pos: ${s.position}, Expected: ${expectedLength}, Total: ${s.byteLength}`);\n                return new Uint8Array(uncompressedSize);\n            }\n            const compressedData = s.readUint8Array(expectedLength);\n            return Format3.decode(compressedData, width, height);\n        }\n        console.warn(`ShpFile: Unknown compression type ${compressionType}`);\n        return new Uint8Array(uncompressedSize);\n    }\n    getImage(index: number): ShpImage {\n        if (index < 0 || index >= this.images.length) {\n            throw new RangeError(`Image index out of bounds (file=${this.filename}, index=${index}, numImages=${this.numImages}, images.length=${this.images.length})`);\n        }\n        return this.images[index];\n    }\n    addImage(image: ShpImage): void {\n        this.images.push(image);\n        this.numImages = this.images.length;\n        this.width = Math.max(this.width, image.x + image.width);\n        this.height = Math.max(this.height, image.y + image.height);\n    }\n    clip(newWidth: number, newHeight: number): ShpFile {\n        const clippedFile = new ShpFile();\n        clippedFile.filename = this.filename;\n        clippedFile.width = newWidth;\n        clippedFile.height = newHeight;\n        clippedFile.images = this.images.map((img) => img.clip(newWidth, newHeight));\n        clippedFile.numImages = this.images.length;\n        return clippedFile;\n    }\n}\n"
  },
  {
    "path": "src/data/ShpImage.ts",
    "content": "export class ShpImage {\n    public width: number;\n    public height: number;\n    public x: number;\n    public y: number;\n    public imageData: Uint8Array;\n    constructor(imageData?: Uint8Array, width?: number, height?: number, x?: number, y?: number) {\n        this.imageData = imageData ?? new Uint8Array(0);\n        this.width = width ?? (imageData ? Math.sqrt(imageData.length) : 1);\n        this.height = height ?? (imageData ? imageData.length / this.width : 1);\n        this.x = x ?? 0;\n        this.y = y ?? 0;\n        if (this.imageData.length > 0 && this.width * this.height > this.imageData.length) {\n        }\n    }\n    clip(clipWidth: number, clipHeight: number): ShpImage {\n        const newWidth = Math.min(this.width, clipWidth);\n        const newHeight = Math.min(this.height, clipHeight);\n        const clippedImageData = new Uint8Array(newWidth * newHeight);\n        for (let r = 0; r < newHeight; r++) {\n            for (let c = 0; c < newWidth; c++) {\n                const sourceIndex = r * this.width + c;\n                const destIndex = r * newWidth + c;\n                if (sourceIndex < this.imageData.length) {\n                    clippedImageData[destIndex] = this.imageData[sourceIndex];\n                }\n                else {\n                    clippedImageData[destIndex] = 0;\n                }\n            }\n        }\n        return new ShpImage(clippedImageData, newWidth, newHeight, this.x, this.y);\n    }\n}\n"
  },
  {
    "path": "src/data/Strings.ts",
    "content": "import { CsfFile } from './CsfFile';\nimport { sprintf } from 'sprintf-js';\nexport class Strings {\n    private data: {\n        [key: string]: string;\n    } = {};\n    constructor(source?: CsfFile | {\n        [key: string]: string;\n    }) {\n        if (source) {\n            if (source instanceof CsfFile) {\n                this.fromCsf(source);\n            }\n            else if (typeof source === 'object') {\n                this.fromJson(source as {\n                    [key: string]: string;\n                });\n            }\n        }\n    }\n    public fromCsf(csfFile: CsfFile): void {\n        this.fromJson(csfFile.data);\n    }\n    public fromJson(jsonData: {\n        [key: string]: string;\n    }): void {\n        for (const key of Object.keys(jsonData)) {\n            this.setValue(key, this.sanitizeValue(jsonData[key]));\n        }\n    }\n    private sanitizeValue(value: string): string {\n        return value.replace(/%hs/g, \"%s\");\n    }\n    public setValue(key: string, value: string): void {\n        this.data[key.toLowerCase()] = value;\n    }\n    public has(key: string): boolean {\n        return !!this.data[key.toLowerCase()];\n    }\n    public get(key: string, ...args: any[]): string {\n        const name = String(key);\n        let value = this.data[name.toLowerCase()];\n        if (value) {\n            if (typeof value !== 'string') {\n                console.warn(`Invalid string value for name \"${key}\"`);\n                return key as unknown as string;\n            }\n            return args.length ? sprintf(value, ...args) : value;\n        }\n        if ((/^NOSTR:/i).test(name)) {\n            return name.replace(/^NOSTR:/i, \"\");\n        }\n        console.warn(`[Strings] String with name \"${name}\" not found\"`);\n        return name as unknown as string;\n    }\n    public getKeys(): string[] {\n        return Object.keys(this.data);\n    }\n}\n"
  },
  {
    "path": "src/data/TmpFile.ts",
    "content": "import { TmpImage } from \"./TmpImage\";\nimport { VirtualFile } from \"./vfs/VirtualFile\";\nimport { DataStream } from \"./DataStream\";\nexport class TmpFile {\n    public images: TmpImage[] = [];\n    public width: number = 0;\n    public height: number = 0;\n    public blockWidth: number = 0;\n    public blockHeight: number = 0;\n    constructor(file?: VirtualFile) {\n        if (file instanceof VirtualFile) {\n            this.fromVirtualFile(file);\n        }\n    }\n    private fromVirtualFile(file: VirtualFile): void {\n        const stream = file.stream as DataStream;\n        this.width = stream.readInt32();\n        this.height = stream.readInt32();\n        this.blockWidth = stream.readInt32();\n        this.blockHeight = stream.readInt32();\n        const numberOfTiles = this.width * this.height;\n        if (numberOfTiles <= 0)\n            return;\n        const imageOffsets: number[] = [];\n        for (let i = 0; i < numberOfTiles; i++) {\n            imageOffsets.push(stream.readInt32());\n        }\n        this.images = [];\n        for (let i = 0; i < numberOfTiles; i++) {\n            let offset = imageOffsets[i];\n            if (offset < 0) {\n                offset = 0;\n            }\n            stream.seek(offset);\n            const image = new TmpImage(stream, this.blockWidth, this.blockHeight);\n            this.images.push(image);\n        }\n    }\n    public getTile(tileX: number, tileY: number): TmpImage | undefined {\n        if (tileX < 0 || tileX >= this.width || tileY < 0 || tileY >= this.height) {\n            return undefined;\n        }\n        const index = tileY * this.width + tileX;\n        return this.images[index];\n    }\n}\n"
  },
  {
    "path": "src/data/TmpImage.ts",
    "content": "import type { DataStream } from \"./DataStream\";\nimport { Color } from \"three\";\nexport enum TmpImageFlags {\n    ExtraData = 1,\n    ZData = 2,\n    DamagedData = 4\n}\nconst signedByteToUnsigned = (signedByte: number): number => {\n    return signedByte < 0 ? signedByte + 256 : signedByte;\n};\nexport class TmpImage {\n    public x: number = 0;\n    public y: number = 0;\n    private dataBlockSize: number = 0;\n    public extraX: number = 0;\n    public extraY: number = 0;\n    public extraWidth: number = 0;\n    public extraHeight: number = 0;\n    private flags: number = 0;\n    public height: number = 0;\n    public terrainType: number = 0;\n    public rampType: number = 0;\n    public radarLeft: Color = new Color();\n    public radarRight: Color = new Color();\n    public tileData: Uint8Array = new Uint8Array(0);\n    public zData?: Uint8Array;\n    public extraData?: Uint8Array;\n    public hasZData: boolean = false;\n    public hasExtraData: boolean = false;\n    constructor(stream: DataStream, tileWidthCells: number, tileHeightCells: number) {\n        this.fromStream(stream, tileWidthCells, tileHeightCells);\n    }\n    private fromStream(stream: DataStream, tileWidthCells: number, tileHeightCells: number): void {\n        this.x = stream.readInt32();\n        this.y = stream.readInt32();\n        stream.readInt32();\n        stream.readInt32();\n        this.dataBlockSize = stream.readInt32();\n        this.extraX = stream.readInt32();\n        this.extraY = stream.readInt32();\n        this.extraWidth = stream.readInt32();\n        this.extraHeight = stream.readInt32();\n        this.flags = stream.readUint32();\n        this.height = stream.readUint8();\n        this.terrainType = stream.readUint8();\n        this.rampType = stream.readUint8();\n        this.radarLeft = this.readRadarRgbInternal(stream.readInt8(), stream.readInt8(), stream.readInt8());\n        this.radarRight = this.readRadarRgbInternal(stream.readInt8(), stream.readInt8(), stream.readInt8());\n        stream.seek(stream.position + 3);\n        const mainTileDataByteLength = (tileWidthCells * tileHeightCells) / 2;\n        this.tileData = stream.mapUint8Array(mainTileDataByteLength);\n        this.hasZData = (this.flags & TmpImageFlags.ZData) === TmpImageFlags.ZData;\n        if (this.hasZData) {\n            this.zData = stream.mapUint8Array(mainTileDataByteLength);\n        }\n        this.hasExtraData = (this.flags & TmpImageFlags.ExtraData) === TmpImageFlags.ExtraData;\n        if (this.hasExtraData) {\n            const extraDataByteLength = Math.abs(this.extraWidth * this.extraHeight);\n            this.extraData = stream.mapUint8Array(extraDataByteLength);\n            if (this.hasZData &&\n                this.hasExtraData &&\n                this.dataBlockSize > 0 &&\n                this.dataBlockSize < stream.byteLength) {\n                stream.seek(stream.position + extraDataByteLength);\n            }\n        }\n    }\n    private readRadarRgbInternal(r: number, g: number, b: number): Color {\n        return new Color(signedByteToUnsigned(r) / 255, signedByteToUnsigned(g) / 255, signedByteToUnsigned(b) / 255);\n    }\n}\n"
  },
  {
    "path": "src/data/VxlFile.ts",
    "content": "import { VirtualFile } from '@/data/vfs/VirtualFile';\nimport { Section } from '@/data/vxl/Section';\nimport { VxlHeader } from '@/data/vxl/VxlHeader';\nimport * as THREE from 'three';\nimport { DataStream } from './DataStream';\ninterface Voxel {\n    x: number;\n    y: number;\n    z: number;\n    colorIndex: number;\n    normalIndex: number;\n}\ninterface Span {\n    x: number;\n    y: number;\n    voxels: Voxel[];\n}\ninterface SectionTailer {\n    startingSpanOffset: number;\n    endingSpanOffset: number;\n    dataSpanOffset: number;\n}\ninterface PlainVxlFile {\n    sections: any[];\n    voxelCount: number;\n}\nexport class VxlFile {\n    public filename?: string;\n    public sections: Section[] = [];\n    public voxelCount: number = 0;\n    constructor(virtualFile?: VirtualFile) {\n        if (virtualFile instanceof VirtualFile) {\n            this.fromVirtualFile(virtualFile);\n        }\n    }\n    fromVirtualFile(virtualFile: VirtualFile): void {\n        this.filename = virtualFile.filename;\n        const stream: DataStream = virtualFile.stream;\n        this.sections = [];\n        if (stream.byteLength < VxlHeader.size) {\n            return;\n        }\n        const header = new VxlHeader();\n        header.read(stream);\n        if (!header.headerCount || !header.tailerCount || header.tailerCount !== header.headerCount) {\n            return;\n        }\n        for (let i = 0; i < header.headerCount; ++i) {\n            const section = new Section();\n            this.readSectionHeader(section, stream);\n            if (this.sections.find(s => s.name === section.name)) {\n                console.warn(`Duplicate section name \"${section.name}\" found in VXL \"${this.filename}\".`);\n            }\n            this.sections.push(section);\n        }\n        const bodyStartPosition = stream.position;\n        stream.seek(stream.position + header.bodySize);\n        const tailers: SectionTailer[] = [];\n        for (let i = 0; i < header.tailerCount; ++i) {\n            tailers[i] = this.readSectionTailer(this.sections[i], stream);\n        }\n        let totalVoxelCount = 0;\n        for (let i = 0; i < header.headerCount; ++i) {\n            stream.seek(bodyStartPosition);\n            totalVoxelCount += this.readSectionBodySpans(this.sections[i], tailers[i], stream);\n        }\n        this.voxelCount = totalVoxelCount;\n    }\n    private readSectionHeader(section: Section, stream: DataStream): void {\n        section.name = stream.readCString(16);\n        stream.readUint32();\n        stream.readUint32();\n        stream.readUint32();\n    }\n    private readSectionTailer(section: Section, stream: DataStream): SectionTailer {\n        const startingSpanOffset = stream.readUint32();\n        const endingSpanOffset = stream.readUint32();\n        const dataSpanOffset = stream.readUint32();\n        section.hvaMultiplier = stream.readFloat32();\n        section.transfMatrix = this.readTransfMatrix(stream);\n        section.minBounds = new THREE.Vector3(stream.readFloat32(), stream.readFloat32(), stream.readFloat32());\n        section.maxBounds = new THREE.Vector3(stream.readFloat32(), stream.readFloat32(), stream.readFloat32());\n        section.sizeX = stream.readUint8();\n        section.sizeY = stream.readUint8();\n        section.sizeZ = stream.readUint8();\n        section.normalsMode = stream.readUint8();\n        return {\n            startingSpanOffset,\n            endingSpanOffset,\n            dataSpanOffset\n        };\n    }\n    private readTransfMatrix(stream: DataStream): THREE.Matrix4 {\n        const matrix: number[] = [];\n        for (let i = 0; i < 3; ++i) {\n            matrix.push(stream.readFloat32(), stream.readFloat32(), stream.readFloat32(), stream.readFloat32());\n        }\n        matrix.push(0, 0, 0, 1);\n        return new THREE.Matrix4().fromArray(matrix).transpose();\n    }\n    private readSectionBodySpans(section: Section, tailer: SectionTailer, stream: DataStream): number {\n        stream.seek(stream.position + tailer.startingSpanOffset);\n        const { sizeX, sizeY, sizeZ } = section;\n        const startingOffsets: number[][] = new Array(sizeY);\n        for (let y = 0; y < sizeY; ++y) {\n            startingOffsets[y] = new Array(sizeX);\n            for (let x = 0; x < sizeX; ++x) {\n                startingOffsets[y][x] = stream.readInt32();\n            }\n        }\n        const endingOffsets: number[][] = new Array(sizeY);\n        for (let y = 0; y < sizeY; ++y) {\n            endingOffsets[y] = new Array(sizeX);\n            for (let x = 0; x < sizeX; ++x) {\n                endingOffsets[y][x] = stream.readInt32();\n            }\n        }\n        const spans: Span[] = section.spans = [];\n        let voxelCount = 0;\n        for (let y = 0; y < sizeY; ++y) {\n            for (let x = 0; x < sizeX; ++x) {\n                const span: Span = {\n                    x: x,\n                    y: y,\n                    voxels: this.readSpanVoxels(startingOffsets[y][x], endingOffsets[y][x], x, y, sizeZ, stream)\n                };\n                spans.push(span);\n                voxelCount += span.voxels.length;\n            }\n        }\n        return voxelCount;\n    }\n    private readSpanVoxels(startOffset: number, endOffset: number, x: number, y: number, sizeZ: number, stream: DataStream): Voxel[] {\n        if (startOffset === -1 || endOffset === -1) {\n            return [];\n        }\n        const voxels: Voxel[] = [];\n        for (let z = 0; z < sizeZ;) {\n            z += stream.readUint8();\n            const voxelCount = stream.readUint8();\n            for (let i = 0; i < voxelCount; ++i) {\n                const voxel: Voxel = {\n                    x: x,\n                    y: y,\n                    z: z++,\n                    colorIndex: stream.readUint8(),\n                    normalIndex: stream.readUint8()\n                };\n                voxels.push(voxel);\n            }\n            stream.readUint8();\n        }\n        return voxels;\n    }\n    fromPlain(plainObject: PlainVxlFile): VxlFile {\n        this.sections = plainObject.sections.map(sectionData => new Section().fromPlain(sectionData));\n        this.voxelCount = plainObject.voxelCount;\n        return this;\n    }\n    toPlain(): PlainVxlFile {\n        return {\n            sections: this.sections.map(section => section.toPlain()),\n            voxelCount: this.voxelCount\n        };\n    }\n    getSection(index: number): Section | undefined {\n        return this.sections[index];\n    }\n}\n"
  },
  {
    "path": "src/data/WavFile.ts",
    "content": "import { WaveFile } from '@ra2web/wavefile';\nimport type { VirtualFile } from './vfs/VirtualFile';\nimport type { DataStream } from './DataStream';\nexport class WavFile {\n    private rawData?: Uint8Array;\n    private decodedData?: Uint8Array;\n    constructor(source: VirtualFile | DataStream | Uint8Array) {\n        if (source instanceof Uint8Array) {\n            this.fromRawData(source);\n        }\n        else if ('stream' in source && 'getBytes' in source) {\n            this.fromVirtualFileOrDataStream(source as VirtualFile | DataStream);\n        }\n        else {\n            console.warn(\"WavFile constructor: Unknown source type\", source);\n        }\n    }\n    private fromRawData(data: Uint8Array): this {\n        this.rawData = data;\n        return this;\n    }\n    private fromVirtualFileOrDataStream(file: VirtualFile | DataStream): this {\n        if (typeof (file as any).getBytes === 'function') {\n            this.rawData = (file as VirtualFile).getBytes();\n        }\n        else if (file instanceof Uint8Array) {\n            this.rawData = file;\n        }\n        else if ((file as DataStream).buffer && (file as DataStream).byteOffset !== undefined && (file as DataStream).byteLength !== undefined) {\n            const ds = file as DataStream;\n            this.rawData = new Uint8Array(ds.buffer, ds.byteOffset, ds.byteLength);\n        }\n        else {\n            throw new Error('Cannot get Uint8Array from VirtualFile/DataStream for WavFile');\n        }\n        return this;\n    }\n    getRawData(): Uint8Array | undefined {\n        return this.rawData;\n    }\n    getData(): Uint8Array {\n        if (!this.decodedData) {\n            if (!this.rawData) {\n                throw new Error(\"WavFile: No data loaded to decode.\");\n            }\n            this.decodedData = this.decodeData(this.rawData);\n            this.rawData = undefined;\n        }\n        return this.decodedData;\n    }\n    setData(decodedData: Uint8Array): void {\n        this.rawData = undefined;\n        this.decodedData = decodedData;\n    }\n    private decodeData(data: Uint8Array): Uint8Array {\n        const wav = new WaveFile();\n        wav.fromBuffer(data as any);\n        if (wav.bitDepth === '4') {\n            wav.fromIMAADPCM();\n        }\n        return new Uint8Array(wav.toBuffer() as any);\n    }\n    isRawImaAdpcm(): boolean {\n        if (!this.rawData)\n            return false;\n        const wav = new WaveFile();\n        wav.fromBuffer(this.rawData as any);\n        return wav.bitDepth === '4';\n    }\n}\n"
  },
  {
    "path": "src/data/encoding/Blowfish.ts",
    "content": "export class Blowfish {\n    private m_p: Uint32Array;\n    private m_s: Uint32Array[];\n    private static byteSwap32(value: number): number {\n        return (((((value = (((value << 16) >>> 0) | (value >>> 16)) >>> 0) << 8) >>> 0) &\n            4278255360) |\n            ((value >>> 8) & 16711935)) >>> 0;\n    }\n    constructor(key: number[] | Uint8Array) {\n        this.m_p = new Uint32Array([\n            608135816, 2242054355, 320440878, 57701188, 2752067618, 698298832,\n            137296536, 3964562569, 1160258022, 953160567, 3193202383, 887688300,\n            3232508343, 3380367581, 1065670069, 3041331479, 2450970073, 2306472731,\n        ]);\n        this.m_s = [\n            new Uint32Array([\n                3509652390, 2564797868, 805139163, 3491422135, 3101798381, 1780907670,\n                3128725573, 4046225305, 614570311, 3012652279, 134345442, 2240740374,\n                1667834072, 1901547113, 2757295779, 4103290238, 227898511, 1921955416,\n                1904987480, 2182433518, 2069144605, 3260701109, 2620446009, 720527379,\n                3318853667, 677414384, 3393288472, 3101374703, 2390351024, 1614419982,\n                1822297739, 2954791486, 3608508353, 3174124327, 2024746970, 1432378464,\n                3864339955, 2857741204, 1464375394, 1676153920, 1439316330, 715854006,\n                3033291828, 289532110, 2706671279, 2087905683, 3018724369, 1668267050,\n                732546397, 1947742710, 3462151702, 2609353502, 2950085171, 1814351708,\n                2050118529, 680887927, 999245976, 1800124847, 3300911131, 1713906067,\n                1641548236, 4213287313, 1216130144, 1575780402, 4018429277, 3917837745,\n                3693486850, 3949271944, 596196993, 3549867205, 258830323, 2213823033,\n                772490370, 2760122372, 1774776394, 2652871518, 566650946, 4142492826,\n                1728879713, 2882767088, 1783734482, 3629395816, 2517608232, 2874225571,\n                1861159788, 326777828, 3124490320, 2130389656, 2716951837, 967770486,\n                1724537150, 2185432712, 2364442137, 1164943284, 2105845187, 998989502,\n                3765401048, 2244026483, 1075463327, 1455516326, 1322494562, 910128902,\n                469688178, 1117454909, 936433444, 3490320968, 3675253459, 1240580251,\n                122909385, 2157517691, 634681816, 4142456567, 3825094682, 3061402683,\n                2540495037, 79693498, 3249098678, 1084186820, 1583128258, 426386531,\n                1761308591, 1047286709, 322548459, 995290223, 1845252383, 2603652396,\n                3431023940, 2942221577, 3202600964, 3727903485, 1712269319, 422464435,\n                3234572375, 1170764815, 3523960633, 3117677531, 1434042557, 442511882,\n                3600875718, 1076654713, 1738483198, 4213154764, 2393238008, 3677496056,\n                1014306527, 4251020053, 793779912, 2902807211, 842905082, 4246964064,\n                1395751752, 1040244610, 2656851899, 3396308128, 445077038, 3742853595,\n                3577915638, 679411651, 2892444358, 2354009459, 1767581616, 3150600392,\n                3791627101, 3102740896, 284835224, 4246832056, 1258075500, 768725851,\n                2589189241, 3069724005, 3532540348, 1274779536, 3789419226, 2764799539,\n                1660621633, 3471099624, 4011903706, 913787905, 3497959166, 737222580,\n                2514213453, 2928710040, 3937242737, 1804850592, 3499020752, 2949064160,\n                2386320175, 2390070455, 2415321851, 4061277028, 2290661394, 2416832540,\n                1336762016, 1754252060, 3520065937, 3014181293, 791618072, 3188594551,\n                3933548030, 2332172193, 3852520463, 3043980520, 413987798, 3465142937,\n                3030929376, 4245938359, 2093235073, 3534596313, 375366246, 2157278981,\n                2479649556, 555357303, 3870105701, 2008414854, 3344188149, 4221384143,\n                3956125452, 2067696032, 3594591187, 2921233993, 2428461, 544322398,\n                577241275, 1471733935, 610547355, 4027169054, 1432588573, 1507829418,\n                2025931657, 3646575487, 545086370, 48609733, 2200306550, 1653985193,\n                298326376, 1316178497, 3007786442, 2064951626, 458293330, 2589141269,\n                3591329599, 3164325604, 727753846, 2179363840, 146436021, 1461446943,\n                4069977195, 705550613, 3059967265, 3887724982, 4281599278, 3313849956,\n                1404054877, 2845806497, 146425753, 1854211946,\n            ]),\n            new Uint32Array([\n                1266315497, 3048417604, 3681880366, 3289982499, 290971e4, 1235738493,\n                2632868024, 2414719590, 3970600049, 1771706367, 1449415276, 3266420449,\n                422970021, 1963543593, 2690192192, 3826793022, 1062508698, 1531092325,\n                1804592342, 2583117782, 2714934279, 4024971509, 1294809318, 4028980673,\n                1289560198, 2221992742, 1669523910, 35572830, 157838143, 1052438473,\n                1016535060, 1802137761, 1753167236, 1386275462, 3080475397, 2857371447,\n                1040679964, 2145300060, 2390574316, 1461121720, 2956646967, 4031777805,\n                4028374788, 33600511, 2920084762, 1018524850, 629373528, 3691585981,\n                3515945977, 2091462646, 2486323059, 586499841, 988145025, 935516892,\n                3367335476, 2599673255, 2839830854, 265290510, 3972581182, 2759138881,\n                3795373465, 1005194799, 847297441, 406762289, 1314163512, 1332590856,\n                1866599683, 4127851711, 750260880, 613907577, 1450815602, 3165620655,\n                3734664991, 3650291728, 3012275730, 3704569646, 1427272223, 778793252,\n                1343938022, 2676280711, 2052605720, 1946737175, 3164576444, 3914038668,\n                3967478842, 3682934266, 1661551462, 3294938066, 4011595847, 840292616,\n                3712170807, 616741398, 312560963, 711312465, 1351876610, 322626781,\n                1910503582, 271666773, 2175563734, 1594956187, 70604529, 3617834859,\n                1007753275, 1495573769, 4069517037, 2549218298, 2663038764, 504708206,\n                2263041392, 3941167025, 2249088522, 1514023603, 1998579484, 1312622330,\n                694541497, 2582060303, 2151582166, 1382467621, 776784248, 2618340202,\n                3323268794, 2497899128, 2784771155, 503983604, 4076293799, 907881277,\n                423175695, 432175456, 1378068232, 4145222326, 3954048622, 3938656102,\n                3820766613, 2793130115, 2977904593, 26017576, 3274890735, 3194772133,\n                1700274565, 1756076034, 4006520079, 3677328699, 720338349, 1533947780,\n                354530856, 688349552, 3973924725, 1637815568, 332179504, 3949051286,\n                53804574, 2852348879, 3044236432, 1282449977, 3583942155, 3416972820,\n                4006381244, 1617046695, 2628476075, 3002303598, 1686838959, 431878346,\n                2686675385, 1700445008, 1080580658, 1009431731, 832498133, 3223435511,\n                2605976345, 2271191193, 2516031870, 1648197032, 4164389018, 2548247927,\n                300782431, 375919233, 238389289, 3353747414, 2531188641, 2019080857,\n                1475708069, 455242339, 2609103871, 448939670, 3451063019, 1395535956,\n                2413381860, 1841049896, 1491858159, 885456874, 4264095073, 4001119347,\n                1565136089, 3898914787, 1108368660, 540939232, 1173283510, 2745871338,\n                3681308437, 4207628240, 3343053890, 4016749493, 1699691293, 1103962373,\n                3625875870, 2256883143, 3830138730, 1031889488, 3479347698, 1535977030,\n                4236805024, 3251091107, 2132092099, 1774941330, 1199868427, 1452454533,\n                157007616, 2904115357, 342012276, 595725824, 1480756522, 206960106,\n                497939518, 591360097, 863170706, 2375253569, 3596610801, 1814182875,\n                2094937945, 3421402208, 1082520231, 3463918190, 2785509508, 435703966,\n                3908032597, 1641649973, 2842273706, 3305899714, 1510255612, 2148256476,\n                2655287854, 3276092548, 4258621189, 236887753, 3681803219, 274041037,\n                1734335097, 3815195456, 3317970021, 1899903192, 1026095262, 4050517792,\n                356393447, 2410691914, 3873677099, 3682840055,\n            ]),\n            new Uint32Array([\n                3913112168, 2491498743, 4132185628, 2489919796, 1091903735, 1979897079,\n                3170134830, 3567386728, 3557303409, 857797738, 1136121015, 1342202287,\n                507115054, 2535736646, 337727348, 3213592640, 1301675037, 2528481711,\n                1895095763, 1721773893, 3216771564, 62756741, 2142006736, 835421444,\n                2531993523, 1442658625, 3659876326, 2882144922, 676362277, 1392781812,\n                170690266, 3921047035, 1759253602, 3611846912, 1745797284, 664899054,\n                1329594018, 3901205900, 3045908486, 2062866102, 2865634940, 3543621612,\n                3464012697, 1080764994, 553557557, 3656615353, 3996768171, 991055499,\n                499776247, 1265440854, 648242737, 3940784050, 980351604, 3713745714,\n                1749149687, 3396870395, 4211799374, 3640570775, 1161844396, 3125318951,\n                1431517754, 545492359, 4268468663, 3499529547, 1437099964, 2702547544,\n                3433638243, 2581715763, 2787789398, 1060185593, 1593081372, 2418618748,\n                4260947970, 69676912, 2159744348, 86519011, 2512459080, 3838209314,\n                1220612927, 3339683548, 133810670, 1090789135, 1078426020, 1569222167,\n                845107691, 3583754449, 4072456591, 1091646820, 628848692, 1613405280,\n                3757631651, 526609435, 236106946, 48312990, 2942717905, 3402727701,\n                1797494240, 859738849, 992217954, 4005476642, 2243076622, 3870952857,\n                3732016268, 765654824, 3490871365, 2511836413, 1685915746, 3888969200,\n                1414112111, 2273134842, 3281911079, 4080962846, 172450625, 2569994100,\n                980381355, 4109958455, 2819808352, 2716589560, 2568741196, 3681446669,\n                3329971472, 1835478071, 660984891, 3704678404, 4045999559, 3422617507,\n                3040415634, 1762651403, 1719377915, 3470491036, 2693910283, 3642056355,\n                3138596744, 1364962596, 2073328063, 1983633131, 926494387, 3423689081,\n                2150032023, 4096667949, 1749200295, 3328846651, 309677260, 2016342300,\n                1779581495, 3079819751, 111262694, 1274766160, 443224088, 298511866,\n                1025883608, 3806446537, 1145181785, 168956806, 3641502830, 3584813610,\n                1689216846, 3666258015, 3200248200, 1692713982, 2646376535, 4042768518,\n                1618508792, 1610833997, 3523052358, 4130873264, 2001055236, 3610705100,\n                2202168115, 4028541809, 2961195399, 1006657119, 2006996926, 3186142756,\n                1430667929, 3210227297, 1314452623, 4074634658, 4101304120, 2273951170,\n                1399257539, 3367210612, 3027628629, 1190975929, 2062231137, 2333990788,\n                2221543033, 2438960610, 1181637006, 548689776, 2362791313, 3372408396,\n                3104550113, 3145860560, 296247880, 1970579870, 3078560182, 3769228297,\n                1714227617, 3291629107, 3898220290, 166772364, 1251581989, 493813264,\n                448347421, 195405023, 2709975567, 677966185, 3703036547, 1463355134,\n                2715995803, 1338867538, 1343315457, 2802222074, 2684532164, 233230375,\n                2599980071, 2000651841, 3277868038, 1638401717, 4028070440, 3237316320,\n                6314154, 819756386, 300326615, 590932579, 1405279636, 3267499572,\n                3150704214, 2428286686, 3959192993, 3461946742, 1862657033, 1266418056,\n                963775037, 2089974820, 2263052895, 1917689273, 448879540, 3550394620,\n                3981727096, 150775221, 3627908307, 1303187396, 508620638, 2975983352,\n                2726630617, 1817252668, 1876281319, 1457606340, 908771278, 3720792119,\n                3617206836, 2455994898, 1729034894, 1080033504,\n            ]),\n            new Uint32Array([\n                976866871, 3556439503, 2881648439, 1522871579, 1555064734, 1336096578,\n                3548522304, 2579274686, 3574697629, 3205460757, 3593280638, 3338716283,\n                3079412587, 564236357, 2993598910, 1781952180, 1464380207, 3163844217,\n                3332601554, 1699332808, 1393555694, 1183702653, 3581086237, 1288719814,\n                691649499, 2847557200, 2895455976, 3193889540, 2717570544, 1781354906,\n                1676643554, 2592534050, 3230253752, 1126444790, 2770207658, 2633158820,\n                2210423226, 2615765581, 2414155088, 3127139286, 673620729, 2805611233,\n                1269405062, 4015350505, 3341807571, 4149409754, 1057255273, 2012875353,\n                2162469141, 2276492801, 2601117357, 993977747, 3918593370, 2654263191,\n                753973209, 36408145, 2530585658, 25011837, 3520020182, 2088578344,\n                530523599, 2918365339, 1524020338, 1518925132, 3760827505, 3759777254,\n                1202760957, 3985898139, 3906192525, 674977740, 4174734889, 2031300136,\n                2019492241, 3983892565, 4153806404, 3822280332, 352677332, 2297720250,\n                60907813, 90501309, 3286998549, 1016092578, 2535922412, 2839152426,\n                457141659, 509813237, 4120667899, 652014361, 1966332200, 2975202805,\n                55981186, 2327461051, 676427537, 3255491064, 2882294119, 3433927263,\n                1307055953, 942726286, 933058658, 2468411793, 3933900994, 4215176142,\n                1361170020, 2001714738, 2830558078, 3274259782, 1222529897, 1679025792,\n                2729314320, 3714953764, 1770335741, 151462246, 3013232138, 1682292957,\n                1483529935, 471910574, 1539241949, 458788160, 3436315007, 1807016891,\n                3718408830, 978976581, 1043663428, 3165965781, 1927990952, 4200891579,\n                2372276910, 3208408903, 3533431907, 1412390302, 2931980059, 4132332400,\n                1947078029, 3881505623, 4168226417, 2941484381, 1077988104, 1320477388,\n                886195818, 18198404, 3786409e3, 2509781533, 112762804, 3463356488,\n                1866414978, 891333506, 18488651, 661792760, 1628790961, 3885187036,\n                3141171499, 876946877, 2693282273, 1372485963, 791857591, 2686433993,\n                3759982718, 3167212022, 3472953795, 2716379847, 445679433, 3561995674,\n                3504004811, 3574258232, 54117162, 3331405415, 2381918588, 3769707343,\n                4154350007, 1140177722, 4074052095, 668550556, 3214352940, 367459370,\n                261225585, 2610173221, 4209349473, 3468074219, 3265815641, 314222801,\n                3066103646, 3808782860, 282218597, 3406013506, 3773591054, 379116347,\n                1285071038, 846784868, 2669647154, 3771962079, 3550491691, 2305946142,\n                453669953, 1268987020, 3317592352, 3279303384, 3744833421, 2610507566,\n                3859509063, 266596637, 3847019092, 517658769, 3462560207, 3443424879,\n                370717030, 4247526661, 2224018117, 4143653529, 4112773975, 2788324899,\n                2477274417, 1456262402, 2901442914, 1517677493, 1846949527, 2295493580,\n                3734397586, 2176403920, 1280348187, 1908823572, 3871786941, 846861322,\n                1172426758, 3287448474, 3383383037, 1655181056, 3139813346, 901632758,\n                1897031941, 2986607138, 3066810236, 3447102507, 1393639104, 373351379,\n                950779232, 625454576, 3124240540, 4148612726, 2007998917, 544563296,\n                2244738638, 2330496472, 2058025392, 1291430526, 424198748, 50039436,\n                29584100, 3605783033, 2429876329, 2791104160, 1057563949, 3255363231,\n                3075367218, 3463963227, 1469046755, 985887462,\n            ]),\n        ];\n        for (let i = 0, keyIndex = 0; i < 18; ++i) {\n            const k1 = key[keyIndex++ % key.length];\n            const k2 = key[keyIndex++ % key.length];\n            const k3 = key[keyIndex++ % key.length];\n            const k4 = key[keyIndex++ % key.length];\n            this.m_p[i] ^= (k1 << 24) | (k2 << 16) | (k3 << 8) | k4;\n        }\n        let l = 0;\n        let r = 0;\n        for (let i = 0; i < 18;) {\n            [l, r] = this._encrypt(l, r);\n            this.m_p[i++] = l;\n            this.m_p[i++] = r;\n        }\n        for (let i = 0; i < 4; ++i) {\n            for (let j = 0; j < 256;) {\n                [l, r] = this._encrypt(l, r);\n                this.m_s[i][j++] = l;\n                this.m_s[i][j++] = r;\n            }\n        }\n    }\n    encrypt(data: Uint32Array): Uint32Array {\n        return this.runCipher(data, this._encrypt.bind(this));\n    }\n    decrypt(data: Uint32Array): Uint32Array {\n        return this.runCipher(data, this._decrypt.bind(this));\n    }\n    private runCipher(data: Uint32Array, cipherFunc: (l: number, r: number) => [\n        number,\n        number\n    ]): Uint32Array {\n        const result = new Uint32Array(data.length);\n        let numBlocks = (data.length / 2) | 0;\n        let dataIndex = 0;\n        for (; 0 < numBlocks--;) {\n            let l = Blowfish.byteSwap32(data[dataIndex]);\n            let r = Blowfish.byteSwap32(data[dataIndex + 1]);\n            [l, r] = cipherFunc(l, r);\n            result[dataIndex++] = Blowfish.byteSwap32(l);\n            result[dataIndex++] = Blowfish.byteSwap32(r);\n        }\n        return result;\n    }\n    private _encrypt(l: number, r: number): [\n        number,\n        number\n    ] {\n        let currentL = l;\n        let currentR = r;\n        currentL ^= this.m_p[0];\n        let swap = false;\n        for (let i = 1; i <= 16; i++, swap = !swap) {\n            if (swap) {\n                currentL = this.round(currentL, currentR, i);\n            }\n            else {\n                currentR = this.round(currentR, currentL, i);\n            }\n        }\n        currentR ^= this.m_p[17];\n        return [currentR, currentL];\n    }\n    private _decrypt(l: number, r: number): [\n        number,\n        number\n    ] {\n        let currentL = l;\n        let currentR = r;\n        currentL ^= this.m_p[17];\n        let swap = false;\n        for (let i = 16; i >= 1; i--, swap = !swap) {\n            if (swap) {\n                currentL = this.round(currentL, currentR, i);\n            }\n            else {\n                currentR = this.round(currentR, currentL, i);\n            }\n        }\n        currentR ^= this.m_p[0];\n        return [currentR, currentL];\n    }\n    private s(val: number, boxIndex: number): number {\n        return this.m_s[boxIndex][(val >>> ((3 - boxIndex) << 3)) & 255];\n    }\n    private bf_f(val: number): number {\n        return ((((this.s(val, 0) + this.s(val, 1)) >>> 0) ^\n            this.s(val, 2)) +\n            this.s(val, 3)) >>> 0;\n    }\n    private round(l: number, r: number, pIndex: number): number {\n        return l ^ (this.bf_f(r) ^ this.m_p[pIndex]);\n    }\n}\n"
  },
  {
    "path": "src/data/encoding/BlowfishKey.ts",
    "content": "const s = \"AihRvNoIbTn85FZRYNZRcT+i6KpU+maCsEqr3Q5q+LDB5tH7Tz2qQ38V\";\nconst a = new Int8Array([\n    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n    -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,\n    55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3,\n    4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,\n    22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32,\n    33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,\n    50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n]);\nclass i {\n    key1: Uint32Array<ArrayBuffer>;\n    key2: Uint32Array<ArrayBuffer>;\n    len: number;\n    constructor() {\n        (this.key1 = new Uint32Array(64)),\n            (this.key2 = new Uint32Array(64));\n    }\n}\nexport class BlowfishKey {\n    pubkey: i;\n    glob1: Uint32Array<ArrayBuffer>;\n    glob2: Uint32Array<ArrayBuffer>;\n    glob1_hi: Uint32Array<ArrayBuffer>;\n    glob1_hi_inv: Uint32Array<ArrayBuffer>;\n    glob1_bitlen: any;\n    glob1_len_x2: number;\n    glob1_hi_bitlen: number;\n    glob1_hi_inv_lo: number;\n    glob1_hi_inv_hi: number;\n    constructor() {\n        (this.pubkey = new i()),\n            (this.glob1 = new Uint32Array(64)),\n            (this.glob2 = new Uint32Array(130)),\n            (this.glob1_hi = new Uint32Array(4)),\n            (this.glob1_hi_inv = new Uint32Array(4));\n    }\n    init_bignum(e, t, i) {\n        for (let r = 0; r < i; r++)\n            e[r] = 0;\n        e[0] = t;\n    }\n    move_key_to_big(e, t, i, r) {\n        let s;\n        s = 0 != (128 & t[0]) ? 255 : 0;\n        const a = new Uint8Array(e.buffer, e.byteOffset);\n        let n = 4 * r;\n        for (; n > i; n--)\n            a[n - 1] = s;\n        for (; 0 < n; n--)\n            a[n - 1] = t[i - n];\n    }\n    key_to_bignum(e, t, i) {\n        let r, s, a = 0;\n        if (2 === t[a]) {\n            if ((a++, 0 != (128 & t[a]))) {\n                for (r = 0, s = 0; s < (127 & t[a]); s++)\n                    r = (((r << 8) >>> 0) | t[a + s + 1]) >>> 0;\n                a += 1 + (127 & t[a]);\n            }\n            else\n                (r = t[a]), a++;\n            r <= 4 * i && this.move_key_to_big(e, t.subarray(a), r, i);\n        }\n    }\n    len_bignum(e, t) {\n        let i = t - 1;\n        for (; 0 <= i && 0 === e[i];)\n            i--;\n        return i + 1;\n    }\n    bitlen_bignum(e, t) {\n        var i;\n        let r, s;\n        if (0 === (i = this.len_bignum(e, t)))\n            return 0;\n        for (r = 32 * i, s = 2147483648; 0 == (s & e[i - 1]);)\n            (s >>>= 1), r--;\n        return r;\n    }\n    init_pubkey() {\n        let e = 0, t;\n        var i;\n        const r = new Uint8Array(256);\n        for (this.init_bignum(this.pubkey.key2, 65537, 64), t = 0; e < s.length;)\n            (i =\n                ((((((((((((a[s.charCodeAt(e++)] >>> 0) << 6) >>> 0) |\n                    (255 & a[s.charCodeAt(e++)])) >>>\n                    0) <<\n                    6) >>>\n                    0) |\n                    (255 & a[s.charCodeAt(e++)])) >>>\n                    0) <<\n                    6) >>>\n                    0) |\n                    (255 & a[s.charCodeAt(e++)])) >>>\n                    0),\n                (r[t++] = (i >> 16) & 255),\n                (r[t++] = (i >> 8) & 255),\n                (r[t++] = 255 & i);\n        this.key_to_bignum(this.pubkey.key1, r, 64),\n            (this.pubkey.len =\n                this.bitlen_bignum(this.pubkey.key1, 64) - 1);\n    }\n    len_predata() {\n        var e = ((this.pubkey.len - 1) / 8) | 0;\n        return ((1 + ((55 / e) | 0)) * (1 + e)) >>> 0;\n    }\n    cmp_bignum(e, t, i) {\n        for (; 0 < i;) {\n            if (e[--i] < t[i])\n                return -1;\n            if (e[i] > t[i])\n                return 1;\n        }\n        return 0;\n    }\n    mov_bignum(e, t, i) {\n        for (let r = 0; r < i; r++)\n            e[r] = t[r];\n    }\n    shr_bignum(e, t, i) {\n        let r;\n        var s = (t / 32) | 0;\n        if (0 < s) {\n            for (r = 0; r < i - s; r++)\n                e[r] = e[r + s];\n            for (; r < i; r++)\n                e[r] = 0;\n            t %= 32;\n        }\n        if (0 !== t) {\n            for (r = 0; r < i - 1; r++)\n                e[r] =\n                    ((e[r] >>> t) | ((e[r + 1] << (32 - t)) >>> 0)) >>> 0;\n            e[r] = e[r] >>> t;\n        }\n    }\n    shl_bignum(e, t, i) {\n        let r;\n        var s = (t / 32) | 0;\n        if (0 < s) {\n            for (r = i - 1; r > s; r--)\n                e[r] = e[r - s];\n            for (; 0 < r; r--)\n                e[r] = 0;\n            t %= 32;\n        }\n        if (0 !== t) {\n            for (r = i - 1; 0 < r; r--)\n                e[r] =\n                    (((e[r] << t) >>> 0) | (e[r - 1] >>> (32 - t))) >>> 0;\n            e[0] = (e[0] << t) >>> 0;\n        }\n    }\n    sub_bignum(e, t, i, r, s) {\n        var a, n;\n        s += s;\n        var o = new Uint16Array(t.buffer, t.byteOffset), l = new Uint16Array(i.buffer, i.byteOffset);\n        const c = new Uint16Array(e.buffer, e.byteOffset);\n        let h = 0;\n        for (; -1 != --s;)\n            (a = o[h]),\n                (n = l[h]),\n                (c[h] = (a - n - r) & 65535),\n                (r = 0 != ((a - n - r) & 65536) ? 1 : 0),\n                h++;\n        return r;\n    }\n    sub_bignum_word(e, t, i, r, s) {\n        var a, n;\n        let o = 0;\n        for (; -1 != --s;)\n            (a = t[o]),\n                (n = i[o]),\n                (e[o] = (a - n - r) & 65535),\n                (r = 0 != ((a - n - r) & 65536) ? 1 : 0),\n                o++;\n        return r;\n    }\n    inv_bignum(e, t, i) {\n        const r = new Uint32Array(64);\n        var s;\n        let a, n, o = 0;\n        for (this.init_bignum(r, 0, i),\n            this.init_bignum(e, 0, i),\n            n = this.bitlen_bignum(t, i),\n            a = (1 << n % 32) >>> 0,\n            o = (((n + 32) / 32) | 0) - 1,\n            s = (4 * (((n - 1) / 32) | 0)) >>> 0,\n            r[(s / 4) | 0] =\n                r[(s / 4) | 0] | ((1 << ((n - 1) & 31)) >>> 0); 0 < n;)\n            n--,\n                this.shl_bignum(r, 1, i),\n                -1 !== this.cmp_bignum(r, t, i) &&\n                    (this.sub_bignum(r, r, t, 0, i),\n                        (e[o] = e[o] | (a >>> 0))),\n                (a >>>= 1),\n                0 === a && (o--, (a = 2147483648));\n        this.init_bignum(r, 0, i);\n    }\n    inc_bignum(e, t) {\n        let i = 0;\n        for (; 0 == ++e[i] && 0 < --t;)\n            i++;\n    }\n    init_two_dw(e, t) {\n        this.mov_bignum(this.glob1, e, t),\n            (this.glob1_bitlen = this.bitlen_bignum(this.glob1, t)),\n            (this.glob1_len_x2 = ((this.glob1_bitlen + 15) / 16) | 0),\n            this.mov_bignum(this.glob1_hi, this.glob1.subarray(this.len_bignum(this.glob1, t) - 2), 2),\n            (this.glob1_hi_bitlen =\n                (this.bitlen_bignum(this.glob1_hi, 2) - 32) >>> 0),\n            this.shr_bignum(this.glob1_hi, this.glob1_hi_bitlen, 2),\n            this.inv_bignum(this.glob1_hi_inv, this.glob1_hi, 2),\n            this.shr_bignum(this.glob1_hi_inv, 1, 2),\n            (this.glob1_hi_bitlen =\n                (((this.glob1_hi_bitlen + 15) % 16) + 1) >>> 0),\n            this.inc_bignum(this.glob1_hi_inv, 2),\n            32 < this.bitlen_bignum(this.glob1_hi_inv, 2) &&\n                (this.shr_bignum(this.glob1_hi_inv, 1, 2),\n                    this.glob1_hi_bitlen--),\n            (this.glob1_hi_inv_lo = 65535 & this.glob1_hi_inv[0]),\n            (this.glob1_hi_inv_hi =\n                (this.glob1_hi_inv[0] >>> 16) & 65535);\n    }\n    mul_bignum_word(e, t, i, r) {\n        let s, a;\n        var n = new Uint16Array(t.buffer, t.byteOffset);\n        let o = (a = 0);\n        for (s = 0; s < r; s++)\n            (a = i * n[o] + e[o] + a),\n                (e[o] = 65535 & a),\n                o++,\n                (a >>>= 16);\n        e[o] += 65535 & a;\n    }\n    mul_bignum(e, t, i, r) {\n        let s;\n        var a = new Uint16Array(i.buffer, i.byteOffset);\n        let n = new Uint16Array(e.buffer, e.byteOffset);\n        this.init_bignum(e, 0, 2 * r);\n        let o = 0;\n        for (s = 0; s < 2 * r; s++)\n            this.mul_bignum_word(n.subarray(o), t, a[o], 2 * r), o++;\n    }\n    not_bignum(e, t) {\n        let i;\n        for (i = 0; i < t; i++)\n            e[i] = ~e[i] >>> 0;\n    }\n    neg_bignum(e, t) {\n        this.not_bignum(e, t), this.inc_bignum(e, t);\n    }\n    get_mulword(e, t) {\n        let i = (((((((((65535 & (65535 ^ e[t - 1])) * this.glob1_hi_inv_lo +\n            65536) >>>\n            1) +\n            (((65535 ^ e[t - 2]) * this.glob1_hi_inv_hi +\n                this.glob1_hi_inv_hi) >>>\n                1) +\n            1) >>>\n            16) +\n            (((65535 & (65535 ^ e[t - 1])) * this.glob1_hi_inv_hi) >>>\n                1) +\n            (((65535 ^ e[t]) * this.glob1_hi_inv_lo) >>> 1) +\n            1) >>>\n            14) +\n            this.glob1_hi_inv_hi * (65535 ^ e[t]) * 2) >>>\n            this.glob1_hi_bitlen) >>>\n            0;\n        return 65535 < i && (i = 65535), 65535 & i;\n    }\n    dec_bignum(e, t) {\n        let i = 0;\n        for (; --e[i] >>> 0 == 4294967295 && 0 < --t;)\n            i++;\n    }\n    calc_a_bignum(e, t, i, r) {\n        var s;\n        let a;\n        var n = this.glob1, o = this.glob2;\n        if ((this.mul_bignum(this.glob2, t, i, r),\n            (this.glob2[2 * r] = 0),\n            (s = 2 * this.len_bignum(this.glob2, 2 * r + 1)) >=\n                this.glob1_len_x2)) {\n            this.inc_bignum(this.glob2, 2 * r + 1),\n                this.neg_bignum(this.glob2, 2 * r + 1),\n                (a = 1 + s - this.glob1_len_x2);\n            let e = new Uint16Array(o.buffer), t = a, i = 1 + s;\n            for (; 0 !== a; a--) {\n                i--;\n                var l = this.get_mulword(e, i);\n                t--;\n                var c = e.subarray(t);\n                0 < l &&\n                    (this.mul_bignum_word(c, this.glob1, l, 2 * r),\n                        0 == (32768 & e[i]) &&\n                            0 !==\n                                this.sub_bignum_word(c, c, new Uint16Array(n.buffer), 0, 2 * r) &&\n                            e[i]--);\n            }\n            this.neg_bignum(this.glob2, r),\n                this.dec_bignum(this.glob2, r);\n        }\n        this.mov_bignum(e, this.glob2, r);\n    }\n    clear_tmp_vars(e) {\n        this.init_bignum(this.glob1, 0, e),\n            this.init_bignum(this.glob2, 0, e),\n            this.init_bignum(this.glob1_hi_inv, 0, 4),\n            this.init_bignum(this.glob1_hi, 0, 4),\n            (this.glob1_bitlen = 0),\n            (this.glob1_hi_bitlen = 0),\n            (this.glob1_len_x2 = 0),\n            (this.glob1_hi_inv_lo = 0),\n            (this.glob1_hi_inv_hi = 0);\n    }\n    calc_a_key(e, t, i, r, s) {\n        var a, n, o = new Uint32Array(64);\n        let l, c, h = 0;\n        for (this.init_bignum(e, 1, s),\n            n = this.len_bignum(r, s),\n            this.init_two_dw(r, n),\n            l = (this.bitlen_bignum(i, n) << 24) >> 24,\n            a = (((l + 31) / 32) | 0) >>> 0,\n            c = (1 << (l - 1) % 32) >>> 1,\n            h += a - 1,\n            l--,\n            this.mov_bignum(e, t, n); -1 != --l;)\n            0 === c && ((c = 2147483648), h--),\n                this.calc_a_bignum(o, e, e, n),\n                0 != (i[h] & c)\n                    ? this.calc_a_bignum(e, o, t, n)\n                    : this.mov_bignum(e, o, n),\n                (c >>>= 1);\n        this.init_bignum(o, 0, n), this.clear_tmp_vars(s);\n    }\n    memcpy(e, t, i) {\n        let r = 0;\n        for (; 0 != i--;)\n            (e[r] = t[r]), r++;\n    }\n    process_predata(e, t, i) {\n        var r = new Uint32Array(64), s = new Uint32Array(64);\n        let a = 0, n = 0;\n        for (var o = ((this.pubkey.len - 1) / 8) | 0; 1 + o <= t;)\n            this.init_bignum(r, 0, 64),\n                this.memcpy(new Uint8Array(r.buffer), e.subarray(a), 1 + o),\n                this.calc_a_key(s, r, this.pubkey.key2, this.pubkey.key1, 64),\n                this.memcpy(i.subarray(n), new Uint8Array(s.buffer), o),\n                (t -= 1 + o),\n                (a += 1 + o),\n                (n += o);\n    }\n    decryptKey(e) {\n        this.init_pubkey();\n        let t = new Uint8Array(256);\n        return (this.process_predata(e, this.len_predata(), t),\n            t.subarray(0, 56));\n    }\n}\n"
  },
  {
    "path": "src/data/encoding/Format3.ts",
    "content": "export class Format3 {\n    static decode(sourceData: Uint8Array, width: number, height: number): Uint8Array {\n        const decodedData = new Uint8Array(width * height);\n        let sourceIndex = 0;\n        let destIndex = 0;\n        for (let y = 0; y < height; y++) {\n            let lineDataLength = ((sourceData[sourceIndex + 1] << 8) | sourceData[sourceIndex]) - 2;\n            sourceIndex += 2;\n            let currentXInLine = 0;\n            while (lineDataLength > 0) {\n                const value = sourceData[sourceIndex++];\n                lineDataLength--;\n                if (value !== 0) {\n                    if (destIndex < decodedData.length && currentXInLine < width) {\n                        decodedData[destIndex++] = value;\n                    }\n                    currentXInLine++;\n                }\n                else {\n                    let runLength = sourceData[sourceIndex++];\n                    lineDataLength--;\n                    if (currentXInLine + runLength > width) {\n                        runLength = (width - currentXInLine) & 255;\n                    }\n                    for (let k = 0; k < runLength; k++) {\n                        if (destIndex < decodedData.length && currentXInLine < width) {\n                            decodedData[destIndex++] = 0;\n                        }\n                        currentXInLine++;\n                    }\n                }\n            }\n            while (currentXInLine < width && destIndex < (y + 1) * width && destIndex < decodedData.length) {\n                decodedData[destIndex++] = 0;\n                currentXInLine++;\n            }\n            destIndex = (y + 1) * width;\n        }\n        return decodedData;\n    }\n}\n"
  },
  {
    "path": "src/data/encoding/Format5.ts",
    "content": "import { Format80 } from './Format80';\nimport { MiniLzo } from './MiniLzo';\nexport class Format5 {\n    static decode(input: Uint8Array, outputSize: number, format: number = 5): Uint8Array {\n        const output = new Uint8Array(outputSize);\n        this.decodeInto(input, output, format);\n        return output;\n    }\n    static decodeInto(input: Uint8Array, output: Uint8Array, format: number = 5): void {\n        const outputLength = output.length;\n        let inputPos = 0;\n        let outputPos = 0;\n        while (outputPos < outputLength) {\n            const compressedSize = (input[inputPos + 1] << 8) | input[inputPos];\n            inputPos += 2;\n            const decompressedSize = (input[inputPos + 1] << 8) | input[inputPos];\n            inputPos += 2;\n            if (!compressedSize || !decompressedSize)\n                break;\n            let decompressed: Uint8Array;\n            if (format === 80) {\n                decompressed = Format80.decode(input.subarray(inputPos, inputPos + compressedSize), decompressedSize);\n            }\n            else {\n                decompressed = MiniLzo.decompress(input.subarray(inputPos, inputPos + compressedSize), decompressedSize);\n            }\n            for (let i = 0; i < decompressedSize; ++i) {\n                output[outputPos + i] = decompressed[i];\n            }\n            inputPos += compressedSize;\n            outputPos += decompressedSize;\n        }\n    }\n}\n"
  },
  {
    "path": "src/data/encoding/Format80.ts",
    "content": "import { DataStream } from '../DataStream';\nexport class Format80 {\n    static decode(input: Uint8Array, outputSize: number): Uint8Array {\n        const output = new Uint8Array(outputSize);\n        this.decodeInto(input, output);\n        return output;\n    }\n    static decodeInto(input: Uint8Array, output: Uint8Array): number {\n        const stream = new DataStream(new DataView(input.buffer, input.byteOffset, input.byteLength));\n        let outputPos = 0;\n        while (true) {\n            const cmd = stream.readUint8();\n            if ((cmd & 128) === 0) {\n                const byte = stream.readUint8();\n                const count = 3 + ((cmd & 112) >> 4);\n                this.replicatePrevious(output, outputPos, outputPos - (((cmd & 15) << 8) + byte), count);\n                outputPos += count;\n            }\n            else if ((cmd & 64) === 0) {\n                const count = cmd & 63;\n                if (count === 0)\n                    return outputPos;\n                output.set(stream.readUint8Array(count), outputPos);\n                outputPos += count;\n            }\n            else {\n                const count = cmd & 63;\n                if (count === 62) {\n                    const length = stream.readInt16();\n                    const value = stream.readUint8();\n                    const end = outputPos + length;\n                    while (outputPos < end) {\n                        output[outputPos++] = value;\n                    }\n                }\n                else if (count === 63) {\n                    const length = stream.readInt16();\n                    let srcIndex = stream.readInt16();\n                    if (srcIndex >= outputPos) {\n                        throw new Error(`srcIndex >= destIndex ${srcIndex} ${outputPos}`);\n                    }\n                    const end = outputPos + length;\n                    while (outputPos < end) {\n                        output[outputPos++] = output[srcIndex++];\n                    }\n                }\n                else {\n                    const count2 = 3 + count;\n                    let srcIndex = stream.readInt16();\n                    if (srcIndex >= outputPos) {\n                        throw new Error(`srcIndex >= destIndex ${srcIndex} ${outputPos}`);\n                    }\n                    const end = outputPos + count2;\n                    while (outputPos < end) {\n                        output[outputPos++] = output[srcIndex++];\n                    }\n                }\n            }\n        }\n    }\n    private static replicatePrevious(output: Uint8Array, destIndex: number, srcIndex: number, count: number): void {\n        if (destIndex < srcIndex) {\n            throw new Error(`srcIndex > destIndex ${srcIndex} ${destIndex}`);\n        }\n        if (destIndex - srcIndex === 1) {\n            for (let i = 0; i < count; i++) {\n                output[destIndex + i] = output[destIndex - 1];\n            }\n        }\n        else {\n            for (let i = 0; i < count; i++) {\n                output[destIndex + i] = output[srcIndex + i];\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/data/encoding/MiniLzo.ts",
    "content": "import { lzo1x } from './lzo1x';\nexport class MiniLzo {\n    static decompress(input: Uint8Array, outputSize: number): Uint8Array {\n        const buffer = { inputBuffer: input, outputBuffer: null };\n        const result = lzo1x.decompress(buffer, { outputSize });\n        if (result !== 0) {\n            throw new Error(`MiniLzo decode failed with code ${result}`);\n        }\n        return buffer.outputBuffer;\n    }\n}\n"
  },
  {
    "path": "src/data/encoding/lzo1x.ts",
    "content": "interface LzoState {\n    inputBuffer: Uint8Array;\n    outputBuffer: Uint8Array | null;\n}\n\ninterface LzoConfig {\n    outputSize?: number;\n    blockSize?: number;\n}\n\nclass Lzo1xImpl {\n    blockSize = 128 * 1024;\n    minNewSize = this.blockSize;\n    maxSize = 0;\n    OK = 0;\n    INPUT_OVERRUN = -4;\n    OUTPUT_OVERRUN = -5;\n    LOOKBEHIND_OVERRUN = -6;\n    EOF_FOUND = -999;\n    ret = 0;\n    buf: Uint8Array | null = null;\n    buf32: Uint32Array | null = null;\n    out = new Uint8Array(256 * 1024);\n    cbl = 0;\n    ip_end = 0;\n    op_end = 0;\n    t = 0;\n    ip = 0;\n    op = 0;\n    m_pos = 0;\n    m_len = 0;\n    m_off = 0;\n    dv_hi = 0;\n    dv_lo = 0;\n    dindex = 0;\n    ii = 0;\n    jj = 0;\n    tt = 0;\n    v = 0;\n    dict = new Uint32Array(16384);\n    emptyDict = new Uint32Array(16384);\n    skipToFirstLiteralFun = false;\n    returnNewBuffers = true;\n    state: LzoState = { inputBuffer: new Uint8Array(), outputBuffer: null };\n\n    setBlockSize(blockSize: number) {\n        if (typeof blockSize === 'number' && !isNaN(blockSize) && parseInt(String(blockSize), 10) > 0) {\n            this.blockSize = parseInt(String(blockSize), 10);\n            return true;\n        }\n        return false;\n    }\n\n    setOutputSize(outputSize: number) {\n        if (typeof outputSize === 'number' && !isNaN(outputSize) && parseInt(String(outputSize), 10) > 0) {\n            this.out = new Uint8Array(parseInt(String(outputSize), 10));\n            return true;\n        }\n        return false;\n    }\n\n    setReturnNewBuffers(value: boolean) {\n        this.returnNewBuffers = !!value;\n    }\n\n    applyConfig(cfg?: LzoConfig) {\n        if (cfg?.outputSize !== undefined) {\n            this.setOutputSize(cfg.outputSize);\n        }\n        if (cfg?.blockSize !== undefined) {\n            this.setBlockSize(cfg.blockSize);\n        }\n    }\n\n    extendBuffer() {\n        const newBuffer = new Uint8Array(this.minNewSize + (this.blockSize - this.minNewSize % this.blockSize));\n        newBuffer.set(this.out);\n        this.out = newBuffer;\n        this.cbl = this.out.length;\n    }\n\n    match_next() {\n        this.minNewSize = this.op + 3;\n        if (this.minNewSize > this.cbl) {\n            this.extendBuffer();\n        }\n        this.out[this.op++] = this.buf![this.ip++];\n        if (this.t > 1) {\n            this.out[this.op++] = this.buf![this.ip++];\n            if (this.t > 2) {\n                this.out[this.op++] = this.buf![this.ip++];\n            }\n        }\n        this.t = this.buf![this.ip++];\n    }\n\n    match_done() {\n        this.t = this.buf![this.ip - 2] & 3;\n        return this.t;\n    }\n\n    copy_match() {\n        this.t += 2;\n        this.minNewSize = this.op + this.t;\n        if (this.minNewSize > this.cbl) {\n            this.extendBuffer();\n        }\n        do {\n            this.out[this.op++] = this.out[this.m_pos++];\n        } while (--this.t > 0);\n    }\n\n    copy_from_buf() {\n        this.minNewSize = this.op + this.t;\n        if (this.minNewSize > this.cbl) {\n            this.extendBuffer();\n        }\n        do {\n            this.out[this.op++] = this.buf![this.ip++];\n        } while (--this.t > 0);\n    }\n\n    match() {\n        for (;;) {\n            if (this.t >= 64) {\n                this.m_pos = (this.op - 1) - ((this.t >> 2) & 7) - (this.buf![this.ip++] << 3);\n                this.t = (this.t >> 5) - 1;\n                this.copy_match();\n            }\n            else if (this.t >= 32) {\n                this.t &= 31;\n                if (this.t === 0) {\n                    while (this.buf![this.ip] === 0) {\n                        this.t += 255;\n                        this.ip++;\n                    }\n                    this.t += 31 + this.buf![this.ip++];\n                }\n                this.m_pos = (this.op - 1) - (this.buf![this.ip] >> 2) - (this.buf![this.ip + 1] << 6);\n                this.ip += 2;\n                this.copy_match();\n            }\n            else if (this.t >= 16) {\n                this.m_pos = this.op - ((this.t & 8) << 11);\n                this.t &= 7;\n                if (this.t === 0) {\n                    while (this.buf![this.ip] === 0) {\n                        this.t += 255;\n                        this.ip++;\n                    }\n                    this.t += 7 + this.buf![this.ip++];\n                }\n                this.m_pos -= (this.buf![this.ip] >> 2) + (this.buf![this.ip + 1] << 6);\n                this.ip += 2;\n                if (this.m_pos === this.op) {\n                    this.state.outputBuffer = this.returnNewBuffers\n                        ? new Uint8Array(this.out.subarray(0, this.op))\n                        : this.out.subarray(0, this.op);\n                    return this.EOF_FOUND;\n                }\n                this.m_pos -= 0x4000;\n                this.copy_match();\n            }\n            else {\n                this.m_pos = (this.op - 1) - (this.t >> 2) - (this.buf![this.ip++] << 2);\n                this.minNewSize = this.op + 2;\n                if (this.minNewSize > this.cbl) {\n                    this.extendBuffer();\n                }\n                this.out[this.op++] = this.out[this.m_pos++];\n                this.out[this.op++] = this.out[this.m_pos];\n            }\n            if (this.match_done() === 0) {\n                return this.OK;\n            }\n            this.match_next();\n        }\n    }\n\n    decompress(state: LzoState) {\n        this.state = state;\n        this.buf = this.state.inputBuffer;\n        this.cbl = this.out.length;\n        this.ip_end = this.buf.length;\n        this.t = 0;\n        this.ip = 0;\n        this.op = 0;\n        this.m_pos = 0;\n        this.skipToFirstLiteralFun = false;\n        if (this.buf[this.ip] > 17) {\n            this.t = this.buf[this.ip++] - 17;\n            if (this.t < 4) {\n                this.match_next();\n                this.ret = this.match();\n                if (this.ret !== this.OK) {\n                    return this.ret === this.EOF_FOUND ? this.OK : this.ret;\n                }\n            }\n            else {\n                this.copy_from_buf();\n                this.skipToFirstLiteralFun = true;\n            }\n        }\n        for (;;) {\n            if (!this.skipToFirstLiteralFun) {\n                this.t = this.buf[this.ip++];\n                if (this.t >= 16) {\n                    this.ret = this.match();\n                    if (this.ret !== this.OK) {\n                        return this.ret === this.EOF_FOUND ? this.OK : this.ret;\n                    }\n                    continue;\n                }\n                else if (this.t === 0) {\n                    while (this.buf[this.ip] === 0) {\n                        this.t += 255;\n                        this.ip++;\n                    }\n                    this.t += 15 + this.buf[this.ip++];\n                }\n                this.t += 3;\n                this.copy_from_buf();\n            }\n            else {\n                this.skipToFirstLiteralFun = false;\n            }\n            this.t = this.buf[this.ip++];\n            if (this.t < 16) {\n                this.m_pos = this.op - (1 + 0x0800);\n                this.m_pos -= this.t >> 2;\n                this.m_pos -= this.buf[this.ip++] << 2;\n                this.minNewSize = this.op + 3;\n                if (this.minNewSize > this.cbl) {\n                    this.extendBuffer();\n                }\n                this.out[this.op++] = this.out[this.m_pos++];\n                this.out[this.op++] = this.out[this.m_pos++];\n                this.out[this.op++] = this.out[this.m_pos];\n                if (this.match_done() === 0) {\n                    continue;\n                }\n                this.match_next();\n            }\n            this.ret = this.match();\n            if (this.ret !== this.OK) {\n                return this.ret === this.EOF_FOUND ? this.OK : this.ret;\n            }\n        }\n    }\n\n    compress(_state: LzoState) {\n        throw new Error('MiniLzo compression is not implemented in the ESM migration');\n    }\n}\n\nconst instance = new Lzo1xImpl();\n\nexport const lzo1x = {\n    setBlockSize(blockSize: number) {\n        return instance.setBlockSize(blockSize);\n    },\n    setOutputEstimate(outputSize: number) {\n        return instance.setOutputSize(outputSize);\n    },\n    setReturnNewBuffers(value: boolean) {\n        instance.setReturnNewBuffers(value);\n    },\n    compress(state: LzoState, cfg?: LzoConfig) {\n        if (cfg !== undefined) {\n            instance.applyConfig(cfg);\n        }\n        return instance.compress(state);\n    },\n    decompress(state: LzoState, cfg?: LzoConfig) {\n        if (cfg !== undefined) {\n            instance.applyConfig(cfg);\n        }\n        return instance.decompress(state);\n    },\n};\n\nexport type { LzoState, LzoConfig };\n"
  },
  {
    "path": "src/data/hva/Section.ts",
    "content": "import type { Matrix4 } from 'three';\nexport class Section {\n    public name: string = \"\";\n    public matrices: Matrix4[] = [];\n    constructor() {\n    }\n    public getMatrix(index: number): Matrix4 {\n        return this.matrices[index];\n    }\n}\n"
  },
  {
    "path": "src/data/map/MapLighting.ts",
    "content": "export class MapLighting {\n    level: number;\n    ambient: number;\n    red: number;\n    green: number;\n    blue: number;\n    ground: number;\n    forceTint: boolean;\n    constructor() {\n        this.level = 0;\n        this.ambient = 1;\n        this.red = 1;\n        this.green = 1;\n        this.blue = 1;\n        this.ground = 0;\n        this.forceTint = false;\n    }\n    read(reader: any, prefix: string = \"\"): MapLighting {\n        this.level = reader.getNumber(prefix + \"Level\", 0.032);\n        this.ambient = reader.getNumber(prefix + \"Ambient\", 1);\n        this.red = reader.getNumber(prefix + \"Red\", 1);\n        this.green = reader.getNumber(prefix + \"Green\", 1);\n        this.blue = reader.getNumber(prefix + \"Blue\", 1);\n        this.ground = reader.getNumber(prefix + \"Ground\", 0);\n        return this;\n    }\n    copy(source: MapLighting): MapLighting {\n        this.level = source.level;\n        this.ambient = source.ambient;\n        this.red = source.red;\n        this.green = source.green;\n        this.blue = source.blue;\n        this.ground = source.ground;\n        this.forceTint = source.forceTint;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/data/map/MapObjects.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nexport class MapObject {\n    constructor(public type: ObjectType) { }\n    isStructure(): boolean {\n        return this.type === ObjectType.Building;\n    }\n    isVehicle(): boolean {\n        return this.type === ObjectType.Vehicle;\n    }\n    isInfantry(): boolean {\n        return this.type === ObjectType.Infantry;\n    }\n    isAircraft(): boolean {\n        return this.type === ObjectType.Aircraft;\n    }\n    isTerrain(): boolean {\n        return this.type === ObjectType.Terrain;\n    }\n    isSmudge(): boolean {\n        return this.type === ObjectType.Smudge;\n    }\n    isOverlay(): boolean {\n        return this.type === ObjectType.Overlay;\n    }\n    isNamed(): boolean {\n        return \"name\" in this;\n    }\n    isTechno(): boolean {\n        return \"health\" in this;\n    }\n}\nexport class Structure extends MapObject {\n    constructor() {\n        super(ObjectType.Building);\n    }\n}\nexport class Vehicle extends MapObject {\n    constructor() {\n        super(ObjectType.Vehicle);\n    }\n}\nexport class Infantry extends MapObject {\n    constructor() {\n        super(ObjectType.Infantry);\n    }\n}\nexport class Aircraft extends MapObject {\n    constructor() {\n        super(ObjectType.Aircraft);\n    }\n}\nexport class Terrain extends MapObject {\n    constructor() {\n        super(ObjectType.Terrain);\n    }\n}\nexport class Smudge extends MapObject {\n    constructor() {\n        super(ObjectType.Smudge);\n    }\n}\nexport class Overlay extends MapObject {\n    constructor() {\n        super(ObjectType.Overlay);\n    }\n}\n"
  },
  {
    "path": "src/data/map/SpecialFlags.ts",
    "content": "export class SpecialFlags {\n    initialVeteran: boolean;\n    read(data: {\n        getBool: (key: string) => boolean;\n    }): SpecialFlags {\n        this.initialVeteran = data.getBool(\"InitialVeteran\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/data/map/Variable.ts",
    "content": "export class Variable {\n    name: string;\n    value: any;\n    constructor(name: string, value: any) {\n        this.name = name;\n        this.value = value;\n    }\n    clone(): Variable {\n        return new Variable(this.name, this.value);\n    }\n}\n"
  },
  {
    "path": "src/data/map/tag/CellTag.ts",
    "content": "export class CellTag {\n}\n"
  },
  {
    "path": "src/data/map/tag/CellTagsReader.ts",
    "content": "import { IniSection } from '@/data/IniSection';\nexport class CellTagsReader {\n    read(section: IniSection, version: number): Array<{\n        tagId: number;\n        coords: {\n            x: number;\n            y: number;\n        };\n    }> {\n        const result: Array<{\n            tagId: number;\n            coords: {\n                x: number;\n                y: number;\n            };\n        }> = [];\n        for (const [key, rawValue] of section.entries) {\n            const tagId = typeof rawValue === 'string' ? Number(rawValue) : Number(rawValue as any);\n            const coords = this.readCoords(Number(key), version);\n            result.push({ tagId, coords });\n        }\n        return result;\n    }\n    readCoords(key: number, version: number): {\n        x: number;\n        y: number;\n    } {\n        const divisor = version < 4 ? 128 : 1000;\n        return {\n            x: key % divisor,\n            y: Math.floor(key / divisor)\n        };\n    }\n}\n"
  },
  {
    "path": "src/data/map/tag/Tag.ts",
    "content": "export class Tag {\n}\n"
  },
  {
    "path": "src/data/map/tag/TagRepeatType.ts",
    "content": "export enum TagRepeatType {\n    OnceAny = 0,\n    OnceAll = 1,\n    Repeat = 2\n}\n"
  },
  {
    "path": "src/data/map/tag/TagsReader.ts",
    "content": "import { TagRepeatType } from './TagRepeatType';\nimport { IniSection } from '@/data/IniSection';\nexport class TagsReader {\n    read(section: IniSection): Array<{\n        id: string;\n        repeatType: number;\n        name: string;\n        triggerId: string;\n    }> {\n        const result: Array<{\n            id: string;\n            repeatType: number;\n            name: string;\n            triggerId: string;\n        }> = [];\n        for (const [id, rawValue] of section.entries) {\n            if (typeof rawValue !== 'string') {\n                continue;\n            }\n            const parts = rawValue.split(',');\n            if (parts.length < 3) {\n                console.warn(`Invalid tag ${id}=${rawValue}. Skipping.`);\n                continue;\n            }\n            const repeatType = Number(parts[0]);\n            if (TagRepeatType[repeatType] === undefined) {\n                console.warn(`Invalid repeat value ${repeatType} for tag id ${id}. Skipping.`);\n                continue;\n            }\n            result.push({\n                id,\n                repeatType,\n                name: parts[1],\n                triggerId: parts[2]\n            });\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/data/map/trigger/Trigger.ts",
    "content": "export class Trigger {\n    [key: string]: any;\n}\n"
  },
  {
    "path": "src/data/map/trigger/TriggerAction.ts",
    "content": "export class TriggerAction {\n    constructor() {\n    }\n    execute(): void {\n    }\n}\n"
  },
  {
    "path": "src/data/map/trigger/TriggerActionType.ts",
    "content": "export enum TriggerActionType {\n    NoAction = 0,\n    FireSale = 9,\n    TextTrigger = 11,\n    DestroyTrigger = 12,\n    ChangeHouse = 14,\n    RevealMap = 16,\n    RevealAroundWaypoint = 17,\n    PlaySoundFx = 19,\n    PlaySpeech = 21,\n    ForceTrigger = 22,\n    TimerStart = 23,\n    TimerStop = 24,\n    TimerExtend = 25,\n    TimerShorten = 26,\n    TimerSet = 27,\n    GlobalSet = 28,\n    GlobalClear = 29,\n    DestroyObject = 32,\n    AddOneTimeSuperWeapon = 33,\n    AddRepeatingSuperWeapon = 34,\n    AllChangeHouse = 36,\n    ResizePlayerView = 40,\n    PlayAnimAt = 41,\n    DetonateWarhead = 42,\n    ReshroudMap = 51,\n    EnableTrigger = 53,\n    DisableTrigger = 54,\n    CreateRadarEvent = 55,\n    LocalSet = 56,\n    LocalClear = 57,\n    SellBuilding = 60,\n    TurnOffBuilding = 61,\n    TurnOnBuilding = 62,\n    ApplyOneHundredDamage = 63,\n    ForceEnd = 69,\n    DestroyTag = 70,\n    SetAmbientStep = 71,\n    SetAmbientRate = 72,\n    SetAmbientLight = 73,\n    NukeStrike = 95,\n    PlaySoundFxAt = 99,\n    UnrevealAroundWaypoint = 101,\n    LightningStrike = 102,\n    TimerText = 103,\n    CreateCrate = 108,\n    IronCurtainAt = 109,\n    EvictOccupiers = 111,\n    Cheer = 113,\n    StopSoundsAt = 116\n}\n"
  },
  {
    "path": "src/data/map/trigger/TriggerEvent.ts",
    "content": "export class TriggerEvent {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/data/map/trigger/TriggerEventType.ts",
    "content": "export enum TriggerEventType {\n    NoEvent = 0,\n    EnteredBy = 1,\n    SpiedBy = 2,\n    AttackedByAny = 6,\n    DestroyedByAny = 7,\n    AnyEvent = 8,\n    DestroyedAllUnits = 9,\n    DestroyedAllBuildings = 10,\n    DestroyedAll = 11,\n    CreditsExceed = 12,\n    ElapsedTime = 13,\n    MissionTimerExpired = 14,\n    DestroyedBuildings = 15,\n    DestroyedUnits = 16,\n    NoFactoriesLeft = 17,\n    BuildBuilding = 19,\n    BuildUnit = 20,\n    BuildInfantry = 21,\n    BuildAircraft = 22,\n    CrossesHorizontalLine = 25,\n    CrossesVerticalLine = 26,\n    GlobalIsSet = 27,\n    GlobalIsCleared = 28,\n    DestroyedOrCaptured = 29,\n    LowPower = 30,\n    DestroyedBridge = 31,\n    BuildingExists = 32,\n    ComesNearWaypoint = 34,\n    LocalIsSet = 36,\n    LocalIsCleared = 37,\n    FirstDamagedCombat = 38,\n    HalfHealthCombat = 39,\n    QuarterHealthCombat = 40,\n    FirstDamagedAny = 41,\n    HalfHealthAny = 42,\n    QuarterHealthAny = 43,\n    AttackedByHouse = 44,\n    AmbientLightBelow = 45,\n    AmbientLightAbove = 46,\n    ElapsedScenarioTime = 47,\n    DestroyedOrCapturedOrInfiltrated = 48,\n    PickupCrate = 49,\n    PickupCrateAny = 50,\n    RandomDelay = 51,\n    CreditsBelow = 52,\n    SpyEnteringAsHouse = 53,\n    SpyEnteringAsInfantry = 54,\n    DestroyedAllUnitsNaval = 55,\n    DestroyedAllUnitsLand = 56,\n    BuildingNotExists = 57\n}\n"
  },
  {
    "path": "src/data/map/trigger/TriggerReader.ts",
    "content": "import { TriggerEventType } from './TriggerEventType';\nimport { TriggerActionType } from './TriggerActionType';\nimport { IniSection } from '@/data/IniSection';\nexport class TriggerReader {\n    read(triggers: IniSection, events: IniSection, actions: IniSection, tags: Array<any>) {\n        const triggerList = this.readTriggers(triggers);\n        const { events: eventMap, unknownEventTypes } = this.readEvents(events);\n        const { actions: actionMap, unknownActionTypes } = this.readActions(actions);\n        const tagList = [...tags.values?.() ?? tags];\n        const rootTriggers = new Set(triggerList);\n        for (const trigger of triggerList.values()) {\n            const triggerEvents = eventMap.get(trigger.id);\n            if (triggerEvents) {\n                trigger.events.push(...triggerEvents);\n            }\n            const triggerActions = actionMap.get(trigger.id);\n            if (triggerActions) {\n                trigger.actions.push(...triggerActions);\n            }\n            if (trigger.attachedTriggerId) {\n                const attachedTrigger = triggerList.find(t => t.id === trigger.attachedTriggerId);\n                if (attachedTrigger) {\n                    trigger.attachedTrigger = attachedTrigger;\n                    rootTriggers.delete(attachedTrigger);\n                }\n            }\n        }\n        for (const rootTrigger of rootTriggers) {\n            const tag = tagList.find(t => t.triggerId === rootTrigger.id);\n            if (tag) {\n                let currentTrigger = rootTrigger;\n                while (currentTrigger) {\n                    currentTrigger.tag = tag;\n                    currentTrigger = currentTrigger.attachedTrigger;\n                }\n            }\n            else {\n                let currentTrigger = rootTrigger;\n                while (currentTrigger) {\n                    console.warn(`Trigger ${currentTrigger.id} has no associated tag or valid root trigger. Skipping.`);\n                    const index = triggerList.indexOf(currentTrigger);\n                    if (index !== -1) {\n                        triggerList.splice(index, 1);\n                    }\n                    currentTrigger = currentTrigger.attachedTrigger;\n                }\n            }\n        }\n        return {\n            triggers: triggerList,\n            unknownEventTypes,\n            unknownActionTypes,\n        };\n    }\n    private readTriggers(triggers: IniSection) {\n        const result: any[] = [];\n        for (const [id, raw] of triggers.entries) {\n            if (typeof raw !== 'string')\n                continue;\n            const parts = raw.split(',');\n            if (parts.length < 8) {\n                console.warn(`Invalid trigger ${id}=${raw}. Skipping.`);\n            }\n            else {\n                const trigger = {\n                    id,\n                    houseName: parts[0],\n                    attachedTriggerId: parts[1] !== '<none>' ? parts[1] : undefined,\n                    attachedTrigger: undefined,\n                    name: parts[2],\n                    disabled: Boolean(Number(parts[3])),\n                    difficulties: {\n                        easy: Boolean(Number(parts[4])),\n                        medium: Boolean(Number(parts[5])),\n                        hard: Boolean(Number(parts[6])),\n                    },\n                    events: [],\n                    actions: [],\n                    tag: undefined,\n                };\n                result.push(trigger);\n            }\n        }\n        return result;\n    }\n    private readEvents(events: IniSection) {\n        const eventMap = new Map();\n        const unknownTypes = new Set();\n        for (const [triggerId, raw] of events.entries) {\n            if (typeof raw !== 'string')\n                continue;\n            const parts = raw.split(',');\n            if (parts.length < 4) {\n                console.warn(`Invalid event ${triggerId}=${raw}. Skipping.`);\n            }\n            else {\n                const eventCount = Number(parts.shift());\n                const eventList = [];\n                for (let i = 0; i < eventCount; i++) {\n                    const type = Number(parts.shift());\n                    const paramCount = Number(parts.shift());\n                    const params = parts.splice(0, paramCount === 2 ? 2 : 1);\n                    if (TriggerEventType[type] !== undefined) {\n                        const event = {\n                            triggerId,\n                            eventIndex: i,\n                            type,\n                            params: [paramCount, ...params.map(p => p || '0')],\n                        };\n                        eventList.push(event);\n                    }\n                    else {\n                        unknownTypes.add(type);\n                        console.warn(`Unknown event type ${type} for trigger id ${triggerId}. Skipping.`);\n                    }\n                }\n                eventMap.set(triggerId, eventList);\n            }\n        }\n        return { events: eventMap, unknownEventTypes: unknownTypes };\n    }\n    private readActions(actions: IniSection) {\n        const actionMap = new Map();\n        const unknownTypes = new Set();\n        for (const [triggerId, raw] of actions.entries) {\n            if (typeof raw !== 'string')\n                continue;\n            const parts = raw.split(',');\n            if (parts.length < 9) {\n                console.warn(`Invalid action ${triggerId}=${raw}. Skipping.`);\n            }\n            else {\n                const actionCount = Number(parts.shift());\n                if (parts.length < 8 * actionCount) {\n                    console.warn(`Invalid action ${triggerId}=${raw}. Skipping.`);\n                }\n                else {\n                    const actionList = [];\n                    for (let i = 0; i < actionCount; i++) {\n                        const type = Number(parts.shift());\n                        const params = parts.splice(0, 7);\n                        if (TriggerActionType[type] !== undefined) {\n                            const action = {\n                                triggerId,\n                                index: i,\n                                type,\n                                params: [\n                                    Number(params[0] || '0'),\n                                    params[1] || '0',\n                                    params[2] || '0',\n                                    params[3] || '0',\n                                    params[4] || '0',\n                                    params[5] || '0',\n                                    params[6] ? this.readAZActionParam(params[6]) : 0,\n                                ],\n                            };\n                            actionList.push(action);\n                        }\n                        else {\n                            unknownTypes.add(type);\n                            console.warn(`Unknown action type ${type} for trigger id \"${triggerId}\". Skipping.`);\n                        }\n                    }\n                    actionMap.set(triggerId, actionList);\n                }\n            }\n        }\n        return { actions: actionMap, unknownActionTypes: unknownTypes };\n    }\n    private readAZActionParam(param: string): number {\n        const zCode = 'Z'.charCodeAt(0);\n        const aCode = 'A'.charCodeAt(0);\n        const base = zCode - aCode + 1;\n        return param.length > 1\n            ? param.charCodeAt(1) - aCode + (param.charCodeAt(0) - aCode + 1) * base\n            : param.charCodeAt(0) - aCode;\n    }\n}\n"
  },
  {
    "path": "src/data/vfs/Archive.ts",
    "content": "export class Archive {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/data/vfs/FileNotFoundError.ts",
    "content": "export class FileNotFoundError extends Error {\n    public cause?: Error;\n    constructor(message?: string, cause?: Error) {\n        super(message);\n        this.name = \"FileNotFoundError\";\n        if (cause) {\n            this.cause = cause;\n        }\n        Object.setPrototypeOf(this, FileNotFoundError.prototype);\n    }\n}\n"
  },
  {
    "path": "src/data/vfs/FileSystem.ts",
    "content": "/**\n * FileSystem interface for virtual file system access.\n */\nexport interface FileSystem {\n    [key: string]: any;\n}\n"
  },
  {
    "path": "src/data/vfs/IOError.ts",
    "content": "export class IOError extends Error {\n    public cause?: Error;\n    constructor(message: string, cause?: Error) {\n        super(message);\n        this.name = \"IOError\";\n        if (cause) {\n            this.cause = cause;\n        }\n        Object.setPrototypeOf(this, IOError.prototype);\n    }\n}\n"
  },
  {
    "path": "src/data/vfs/MemArchive.ts",
    "content": "import type { VirtualFile } from \"./VirtualFile\";\nexport class MemArchive {\n    private entries: Map<string, VirtualFile>;\n    constructor() {\n        this.entries = new Map<string, VirtualFile>();\n    }\n    addFile(file: VirtualFile): void {\n        this.entries.set(file.filename, file);\n    }\n    containsFile(filename: string): boolean {\n        return this.entries.has(filename);\n    }\n    openFile(filename: string): VirtualFile {\n        if (!this.containsFile(filename)) {\n            throw new Error(`File \"${filename}\" not found in MemArchive`);\n        }\n        return this.entries.get(filename)!;\n    }\n    listFiles(): string[] {\n        return [...this.entries.keys()];\n    }\n    getAllFiles(): VirtualFile[] {\n        return [...this.entries.values()];\n    }\n}\n"
  },
  {
    "path": "src/data/vfs/NameNotAllowedError.ts",
    "content": "import { IOError } from \"./IOError\";\nexport class NameNotAllowedError extends IOError {\n    constructor(message: string = \"File name is not allowed\", cause?: Error) {\n        super(message);\n        this.name = \"NameNotAllowedError\";\n        if (cause && this instanceof Error) {\n            (this as any).cause = cause;\n        }\n        Object.setPrototypeOf(this, NameNotAllowedError.prototype);\n    }\n}\n"
  },
  {
    "path": "src/data/vfs/RealFileSystem.ts",
    "content": "import { FileNotFoundError } from \"./FileNotFoundError\";\nimport { RealFileSystemDir } from \"./RealFileSystemDir\";\nimport type { VirtualFile } from \"./VirtualFile\";\nexport interface RFSConstructorOptions {\n}\nexport class RealFileSystem {\n    private directories: RealFileSystemDir[];\n    private rootDirectory: RealFileSystemDir | undefined;\n    private rootDirectoryHandle: FileSystemDirectoryHandle | undefined;\n    constructor(options?: RFSConstructorOptions) {\n        this.directories = [];\n    }\n    addRootDirectoryHandle(handle: FileSystemDirectoryHandle): RealFileSystemDir {\n        this.rootDirectoryHandle = handle;\n        const newDir = new RealFileSystemDir(handle);\n        this.directories.push(newDir);\n        this.rootDirectory = newDir;\n        return newDir;\n    }\n    getRootDirectoryHandle(): FileSystemDirectoryHandle | undefined {\n        return this.rootDirectoryHandle;\n    }\n    addDirectoryHandle(handle: FileSystemDirectoryHandle): RealFileSystemDir {\n        const newDir = new RealFileSystemDir(handle);\n        this.directories.push(newDir);\n        return newDir;\n    }\n    addDirectory(dir: RealFileSystemDir): void {\n        if (!this.directories.includes(dir)) {\n            this.directories.push(dir);\n        }\n    }\n    async getDirectory(path: string): Promise<RealFileSystemDir> {\n        for (const dir of this.directories) {\n            if (dir.name === path)\n                return dir;\n            try {\n                return await dir.getDirectory(path);\n            }\n            catch (e) {\n                if (!(e instanceof FileNotFoundError)) {\n                }\n            }\n        }\n        throw new Error(`Directory \"${path}\" not found in real file system`);\n    }\n    async findDirectory(directoryName: string): Promise<RealFileSystemDir | undefined> {\n        for (const dir of this.directories) {\n            if (await dir.containsEntry(directoryName)) {\n                try {\n                    return await dir.getDirectory(directoryName);\n                }\n                catch (e) {\n                    continue;\n                }\n            }\n        }\n        return undefined;\n    }\n    getRootDirectory(): RealFileSystemDir | undefined {\n        return this.rootDirectory;\n    }\n    async containsEntry(entryName: string): Promise<boolean> {\n        for (const dir of this.directories) {\n            if (await dir.containsEntry(entryName)) {\n                return true;\n            }\n        }\n        return false;\n    }\n    async openFile(filename: string, skipCaseFix: boolean = false): Promise<VirtualFile> {\n        for (const dir of this.directories) {\n            try {\n                return await dir.openFile(filename, skipCaseFix);\n            }\n            catch (e) {\n                if (!(e instanceof FileNotFoundError)) {\n                    throw e;\n                }\n            }\n        }\n        throw new FileNotFoundError(`File \"${filename}\" not found in any registered real file system directories.`);\n    }\n    async getRawFile(filename: string): Promise<File> {\n        for (const dir of this.directories) {\n            try {\n                return await dir.getRawFile(filename);\n            }\n            catch (e) {\n                if (!(e instanceof FileNotFoundError))\n                    throw e;\n            }\n        }\n        throw new FileNotFoundError(`File \"${filename}\" not found in real file system (getRawFile)`);\n    }\n    async *getEntries(): AsyncGenerator<string, void, undefined> {\n        for (const dir of this.directories) {\n            for await (const entryName of dir.getEntries()) {\n                yield entryName;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/data/vfs/RealFileSystemDir.ts",
    "content": "import { StorageQuotaError } from \"./StorageQuotaError\";\nimport { equalsIgnoreCase } from \"../../util/string\";\nimport { FileNotFoundError } from \"./FileNotFoundError\";\nimport { IOError } from \"./IOError\";\nimport { NameNotAllowedError } from \"./NameNotAllowedError\";\nimport { VirtualFile } from \"./VirtualFile\";\nexport class RealFileSystemDir {\n    private handle: FileSystemDirectoryHandle;\n    public caseSensitive: boolean;\n    constructor(handle: FileSystemDirectoryHandle, caseSensitive: boolean = false) {\n        this.handle = handle;\n        this.caseSensitive = caseSensitive;\n    }\n    getNativeHandle(): FileSystemDirectoryHandle {\n        return this.handle;\n    }\n    get name(): string {\n        return this.handle.name;\n    }\n    async *getEntries(): AsyncGenerator<string, void, undefined> {\n        try {\n            for await (const [key, _handle] of this.handle.entries()) {\n                yield key;\n            }\n        }\n        catch (e: any) {\n            if (e.name === \"NotFoundError\") {\n                throw new FileNotFoundError(`Directory \\\"${this.handle.name}\\\" not found`, e);\n            }\n            if (e instanceof DOMException) {\n                throw new IOError(`Directory \\\"${this.handle.name}\\\" could not be read (${e.name})`, e);\n            }\n            throw e;\n        }\n    }\n    async listEntries(): Promise<string[]> {\n        const entries: string[] = [];\n        for await (const entry of this.getEntries()) {\n            entries.push(entry);\n        }\n        return entries;\n    }\n    async *getFileHandles(): AsyncGenerator<FileSystemFileHandle, void, undefined> {\n        try {\n            for await (const entryHandle of this.handle.values()) {\n                if (entryHandle.kind === \"file\") {\n                    yield entryHandle as FileSystemFileHandle;\n                }\n            }\n        }\n        catch (e: any) {\n            if (e.name === \"NotFoundError\") {\n                throw new FileNotFoundError(`Directory \\\"${this.handle.name}\\\" not found`, e);\n            }\n            if (e instanceof DOMException) {\n                throw new IOError(`Directory \\\"${this.handle.name}\\\" could not be read (${e.name})`, e);\n            }\n            throw e;\n        }\n    }\n    async *getRawFiles(): AsyncGenerator<File, void, undefined> {\n        for await (const fileHandle of this.getFileHandles()) {\n            yield await fileHandle.getFile();\n        }\n    }\n    async containsEntry(entryName: string): Promise<boolean> {\n        return (await this.resolveEntryName(entryName)) !== undefined;\n    }\n    async resolveEntryName(entryName: string): Promise<string | undefined> {\n        if (this.caseSensitive) {\n            try {\n                const fileHandle = await this.handle.getFileHandle(entryName).catch(() => null);\n                if (fileHandle)\n                    return fileHandle.name;\n                const dirHandle = await this.handle.getDirectoryHandle(entryName).catch(() => null);\n                if (dirHandle)\n                    return dirHandle.name;\n                return undefined;\n            }\n            catch {\n                return undefined;\n            }\n        }\n        else {\n            for await (const key of this.getEntries()) {\n                if (equalsIgnoreCase(key, entryName)) {\n                    return key;\n                }\n            }\n        }\n        return undefined;\n    }\n    async fixEntryCase(entryName: string): Promise<string> {\n        if (!this.caseSensitive) {\n            for await (const key of this.getEntries()) {\n                if (equalsIgnoreCase(key, entryName)) {\n                    return key;\n                }\n            }\n        }\n        return entryName;\n    }\n    async getRawFile(filename: string, skipCaseFix: boolean = false, type?: string): Promise<File> {\n        let fileHandle: FileSystemFileHandle;\n        try {\n            const resolvedName = skipCaseFix ? filename : await this.fixEntryCase(filename);\n            fileHandle = await this.handle.getFileHandle(resolvedName);\n        }\n        catch (e: any) {\n            if (e.name === \"NotFoundError\") {\n                throw new FileNotFoundError(`File \\\"${filename}\\\" not found in directory \\\"${this.handle.name}\\\"`, e);\n            }\n            if (e instanceof TypeError && e.message.includes(\"not allowed\")) {\n                throw new NameNotAllowedError(`File name \\\"${filename}\\\" is not allowed`, e);\n            }\n            if (e instanceof DOMException) {\n                throw new IOError(`File \\\"${filename}\\\" could not be read (${e.name})`, e);\n            }\n            throw e;\n        }\n        const file = await fileHandle.getFile();\n        if (type) {\n            return new File([await file.arrayBuffer()], file.name, { type });\n        }\n        return file;\n    }\n    async openFile(filename: string, skipCaseFix: boolean = false): Promise<VirtualFile> {\n        const rawFile = await this.getRawFile(filename, skipCaseFix);\n        return VirtualFile.fromRealFile(rawFile);\n    }\n    async writeFile(virtualFile: VirtualFile, filenameOverride?: string): Promise<void> {\n        const resolvedFilename = filenameOverride ?? virtualFile.filename;\n        try {\n            const finalFilename = await this.fixEntryCase(resolvedFilename);\n            try {\n                await this.deleteFile(finalFilename, true);\n            }\n            catch (delError: any) {\n                if (!(delError instanceof FileNotFoundError)) {\n                }\n            }\n            const fileHandle = await this.handle.getFileHandle(finalFilename, { create: true });\n            const writable = await fileHandle.createWritable();\n            try {\n                await writable.write(virtualFile.getBytes() as any);\n                await writable.close();\n            }\n            catch (writeError) {\n                await writable.abort();\n                throw writeError;\n            }\n        }\n        catch (e: any) {\n            if (e.name === \"QuotaExceededError\" || (e instanceof DOMException && e.message.toLowerCase().includes(\"quota\"))) {\n                throw new StorageQuotaError(undefined, e);\n            }\n            if (e.name === \"NotFoundError\") {\n                throw new FileNotFoundError(`Directory \\\"${this.handle.name}\\\" not found during writeFile operation for \\\"${resolvedFilename}\\\"`, e);\n            }\n            if (e instanceof TypeError && e.message.includes(\"not allowed\")) {\n                throw new NameNotAllowedError(`File name \\\"${resolvedFilename}\\\" is not allowed`, e);\n            }\n            if (e instanceof DOMException) {\n                throw new IOError(`File \\\"${resolvedFilename}\\\" could not be written (${e.name})`, e);\n            }\n            throw e;\n        }\n    }\n    async deleteFile(filename: string, skipCaseFix: boolean = false): Promise<void> {\n        const resolvedName = skipCaseFix ? filename : await this.resolveEntryName(filename);\n        if (resolvedName) {\n            try {\n                await this.handle.removeEntry(resolvedName);\n            }\n            catch (e: any) {\n                if (skipCaseFix && e.name === \"NotFoundError\") {\n                    return;\n                }\n                if (e.name === \"QuotaExceededError\" || (e instanceof DOMException && e.message.toLowerCase().includes(\"quota\"))) {\n                    throw new StorageQuotaError(undefined, e);\n                }\n                if (e instanceof TypeError && e.message.includes(\"not allowed\")) {\n                    throw new NameNotAllowedError(`File name \\\"${resolvedName}\\\" is not allowed for deletion`, e);\n                }\n                if (e instanceof DOMException) {\n                    throw new IOError(`File \\\"${resolvedName}\\\" could not be deleted (${e.name})`, e);\n                }\n                throw e;\n            }\n        }\n    }\n    async getDirectory(dirName: string, forceCaseSensitive: boolean = this.caseSensitive): Promise<RealFileSystemDir> {\n        const resolvedName = forceCaseSensitive ? dirName : await this.fixEntryCase(dirName);\n        let dirHandle: FileSystemDirectoryHandle;\n        try {\n            dirHandle = await this.handle.getDirectoryHandle(resolvedName);\n        }\n        catch (e: any) {\n            if (e.name === \"NotFoundError\") {\n                throw new FileNotFoundError(`Directory \\\"${dirName}\\\" not found or parent directory \\\"${this.handle.name}\\\" is gone`, e);\n            }\n            if (e instanceof TypeError && e.message.includes(\"not allowed\")) {\n                throw new NameNotAllowedError(`Directory name \\\"${dirName}\\\" is not allowed`, e);\n            }\n            if (e instanceof DOMException) {\n                throw new IOError(`Directory \\\"${dirName}\\\" could not be read (${e.name})`, e);\n            }\n            throw e;\n        }\n        return new RealFileSystemDir(dirHandle, forceCaseSensitive);\n    }\n    async getOrCreateDirectory(dirName: string, forceCaseSensitive: boolean = this.caseSensitive): Promise<RealFileSystemDir> {\n        const resolvedName = forceCaseSensitive ? dirName : await this.fixEntryCase(dirName);\n        try {\n            const dirHandle = await this.handle.getDirectoryHandle(resolvedName, { create: true });\n            return new RealFileSystemDir(dirHandle, forceCaseSensitive);\n        }\n        catch (e: any) {\n            if (e.name === \"QuotaExceededError\" || (e instanceof DOMException && e.message.toLowerCase().includes(\"quota\"))) {\n                throw new StorageQuotaError(undefined, e);\n            }\n            if (e.name === \"NotFoundError\") {\n                throw new FileNotFoundError(`Directory \\\"${this.handle.name}\\\" not found while trying to create/get \\\"${dirName}\\\"`, e);\n            }\n            if (e instanceof TypeError && e.message.includes(\"not allowed\")) {\n                throw new NameNotAllowedError(`Directory name \\\"${dirName}\\\" is not allowed`, e);\n            }\n            if (e instanceof DOMException) {\n                throw new IOError(`Directory \\\"${dirName}\\\" could not be created/accessed (${e.name})`, e);\n            }\n            throw e;\n        }\n    }\n    async getOrCreateDirectoryHandle(dirName: string, isPrivate?: boolean): Promise<FileSystemDirectoryHandle> {\n        const rfsDir = await this.getOrCreateDirectory(dirName, isPrivate);\n        return rfsDir.getNativeHandle();\n    }\n    async deleteDirectory(dirName: string, recursive: boolean = false): Promise<void> {\n        const resolvedName = await this.fixEntryCase(dirName);\n        if (resolvedName) {\n            try {\n                await this.handle.removeEntry(resolvedName, { recursive });\n            }\n            catch (e: any) {\n                if (e.name === \"QuotaExceededError\" || (e instanceof DOMException && e.message.toLowerCase().includes(\"quota\"))) {\n                    throw new StorageQuotaError(undefined, e);\n                }\n                if (e.name === \"InvalidModificationError\" && !recursive) {\n                    throw new IOError(\"Can't delete non-empty directory when recursive = false\", e);\n                }\n                if (e.name === \"NotFoundError\") {\n                    throw new FileNotFoundError(`Directory \\\"${resolvedName}\\\" not found for deletion.`, e);\n                }\n                if (e instanceof TypeError && e.message.includes(\"not allowed\")) {\n                    throw new NameNotAllowedError(`Directory name \\\"${resolvedName}\\\" is not allowed for deletion`, e);\n                }\n                if (e instanceof DOMException) {\n                    throw new IOError(`Directory \\\"${resolvedName}\\\" could not be deleted (${e.name})`, e);\n                }\n                throw e;\n            }\n        }\n        else {\n            throw new FileNotFoundError(`Directory \\\"${dirName}\\\" not found for deletion (case-insensitive check failed).`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/data/vfs/StorageQuotaError.ts",
    "content": "export class StorageQuotaError extends Error {\n    public cause?: Error;\n    constructor(message: string = \"Storage quota exceeded\", cause?: Error) {\n        super(message);\n        this.name = \"StorageQuotaError\";\n        if (cause) {\n            this.cause = cause;\n        }\n        Object.setPrototypeOf(this, StorageQuotaError.prototype);\n    }\n}\n"
  },
  {
    "path": "src/data/vfs/VirtualFile.ts",
    "content": "import { DataStream } from '../DataStream';\nimport { IOError } from './IOError';\nexport class VirtualFile {\n    public stream: DataStream;\n    public filename: string;\n    public static async fromRealFile(realFile: File): Promise<VirtualFile> {\n        try {\n            const arrayBuffer = await realFile.arrayBuffer();\n            const dataStream = new DataStream(arrayBuffer);\n            return new VirtualFile(dataStream, realFile.name);\n        }\n        catch (error) {\n            if (error instanceof DOMException) {\n                throw new IOError(`File \"${realFile.name}\" could not be read (${error.name})`);\n            }\n            throw error;\n        }\n    }\n    public static fromBytes(bytes: ArrayBuffer | ArrayBufferView, filename: string): VirtualFile {\n        const view = bytes instanceof ArrayBuffer\n            ? new DataView(bytes)\n            : new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n        const dataStream = new DataStream(view);\n        return new VirtualFile(dataStream, filename);\n    }\n    public static factory(buffer: ArrayBuffer | ArrayBufferView, filename: string, byteOffset: number = 0, byteLength?: number): VirtualFile {\n        let view: DataView;\n        if (buffer instanceof ArrayBuffer) {\n            view = new DataView(buffer, byteOffset, byteLength);\n        }\n        else {\n            view = new DataView(buffer.buffer, buffer.byteOffset + byteOffset, byteLength ?? buffer.byteLength - byteOffset);\n        }\n        const dataStream = new DataStream(view);\n        return new VirtualFile(dataStream, filename);\n    }\n    constructor(stream: DataStream, filename: string) {\n        this.stream = stream;\n        this.filename = filename;\n    }\n    readAsString(encoding?: string): string {\n        this.stream.seek(0);\n        return this.stream.readString(this.stream.byteLength, encoding);\n    }\n    getBytes(): Uint8Array {\n        return new Uint8Array(this.stream.buffer, this.stream.byteOffset, this.stream.byteLength);\n    }\n    getSize(): number {\n        return this.stream.byteLength;\n    }\n    asFile(mimeType?: string): File {\n        return new File([this.getBytes() as any], this.filename, { type: mimeType });\n    }\n}\n"
  },
  {
    "path": "src/data/vfs/VirtualFileSystem.ts",
    "content": "import { AudioBagFile } from \"../AudioBagFile\";\nimport { IdxFile } from \"../IdxFile\";\nimport { MixFile } from \"../MixFile\";\nimport { EngineType } from \"../../engine/EngineType\";\nimport { pad } from \"../../util/string\";\nimport { FileNotFoundError } from \"./FileNotFoundError\";\nimport { MemArchive } from \"./MemArchive\";\nimport type { VirtualFile } from \"./VirtualFile\";\nimport type { RealFileSystem } from \"./RealFileSystem\";\ninterface VfsLogger {\n    info(message: string, ...args: unknown[]): void;\n    warn(message: string, ...args: unknown[]): void;\n    error(message: string, ...args: unknown[]): void;\n}\ninterface Archive {\n    containsFile(filename: string): boolean;\n    openFile(filename: string): VirtualFile;\n}\nexport class VirtualFileSystem {\n    private rfs: RealFileSystem;\n    private logger: VfsLogger;\n    private allArchives: Map<string, Archive>;\n    private archivesByPriority: Archive[];\n    constructor(rfs: RealFileSystem, logger: VfsLogger) {\n        this.rfs = rfs;\n        this.logger = logger;\n        this.allArchives = new Map<string, Archive>();\n        this.archivesByPriority = [];\n    }\n    fileExists(filename: string): boolean {\n        for (const archive of this.archivesByPriority) {\n            if (archive.containsFile(filename)) {\n                return true;\n            }\n        }\n        return false;\n    }\n    openFile(filename: string): VirtualFile {\n        for (const archive of this.archivesByPriority) {\n            if (archive.containsFile(filename)) {\n                return archive.openFile(filename);\n            }\n        }\n        throw new FileNotFoundError(`File \"${filename}\" not found in VFS`);\n    }\n    addArchive(archive: Archive, name: string): void {\n        if (!this.allArchives.has(name)) {\n            this.allArchives.set(name, archive);\n            this.archivesByPriority.push(archive);\n            this.logger.info(`Added archive \"${name}\" to VFS`);\n        }\n    }\n    hasArchive(name: string): boolean {\n        return this.allArchives.has(name);\n    }\n    removeArchive(name: string): void {\n        const archive = this.allArchives.get(name);\n        if (archive) {\n            this.allArchives.delete(name);\n            const index = this.archivesByPriority.indexOf(archive);\n            if (index > -1) {\n                this.archivesByPriority.splice(index, 1);\n            }\n            this.logger.info(`Removed archive \"${name}\" from VFS`);\n        }\n    }\n    listArchives(): string[] {\n        return [...this.allArchives.keys()];\n    }\n    debugListFileOwners(filename: string): string[] {\n        const owners: string[] = [];\n        this.allArchives.forEach((archive, name) => {\n            try {\n                if (archive.containsFile(filename))\n                    owners.push(name);\n            }\n            catch {\n            }\n        });\n        return owners;\n    }\n    private async openFileWithRfs(filename: string): Promise<VirtualFile | undefined> {\n        let file: VirtualFile | undefined;\n        try {\n            file = await this.rfs.openFile(filename);\n        }\n        catch (e) {\n            if (!(e instanceof FileNotFoundError)) {\n                throw e;\n            }\n        }\n        if (!file) {\n            if (!this.fileExists(filename)) {\n                this.logger.warn(`File \"${filename}\" not found in VFS, returning undefined`);\n                return undefined;\n            }\n            file = this.openFile(filename);\n        }\n        return file;\n    }\n    private async addArchiveByFilename(filename: string, createArchive: (file: VirtualFile) => Archive | Promise<Archive>): Promise<void> {\n        if (this.allArchives.has(filename)) {\n            this.logger.info(`Archive \"${filename}\" already loaded, skipping.`);\n            return;\n        }\n        const virtualFile = await this.openFileWithRfs(filename);\n        if (virtualFile) {\n            try {\n                const archive = await createArchive(virtualFile);\n                this.addArchive(archive, filename);\n            }\n            catch (error) {\n                this.logger.error(`Failed to create archive from \"${filename}\":`, error);\n            }\n        }\n        else {\n            this.logger.warn(`Could not open \"${filename}\" via RFS to add as archive.`);\n        }\n    }\n    async addMixFile(filename: string): Promise<void> {\n        await this.addArchiveByFilename(filename, async (fileStreamHolder) => {\n            if (filename === \"ra2.mix\") {\n                this.logger.info(`Testing original MixFile implementation for ${filename}...`);\n                try {\n                    this.logger.info(`Original MixFile created successfully for ${filename}`);\n                }\n                catch (error) {\n                    this.logger.error(`Original MixFile failed for ${filename}:`, error);\n                }\n                fileStreamHolder.stream.seek(0);\n            }\n            return new MixFile(fileStreamHolder.stream);\n        });\n    }\n    async addBagFile(filename: string): Promise<void> {\n        const idxFilename = filename.replace(/\\.bag$/i, \".idx\");\n        try {\n            const idxFile = await this.openFileWithRfs(idxFilename);\n            if (!idxFile) {\n                this.logger.error(`IDX file \"${idxFilename}\" not found for BAG file \"${filename}\".`);\n                return;\n            }\n            await this.addArchiveByFilename(filename, async (bagVirtualFile) => {\n                const idxData = new IdxFile(idxFile.stream);\n                const audioBag = new AudioBagFile();\n                await audioBag.fromVirtualFile(bagVirtualFile, idxData);\n                return audioBag;\n            });\n        }\n        catch (error) {\n            this.logger.error(`Failed to add BAG file \"${filename}\":`, error);\n        }\n    }\n    async loadImplicitMixFiles(engineType: EngineType): Promise<void> {\n        this.logger.info(\"Initializing implicit mix files...\");\n        const YR = engineType === EngineType.YurisRevenge;\n        if (YR)\n            await this.addMixFile(\"langmd.mix\");\n        await this.addMixFile(\"language.mix\");\n        if (YR)\n            await this.addMixFile(\"ra2md.mix\");\n        await this.addMixFile(\"ra2.mix\");\n        if (YR)\n            await this.addMixFile(\"cachemd.mix\");\n        await this.addMixFile(\"cache.mix\");\n        if (YR)\n            await this.addMixFile(\"loadmd.mix\");\n        await this.addMixFile(\"load.mix\");\n        if (YR)\n            await this.addMixFile(\"localmd.mix\");\n        await this.addMixFile(\"local.mix\");\n        if (YR)\n            await this.addMixFile(\"ntrlmd.mix\");\n        await this.addMixFile(\"neutral.mix\");\n        if (YR)\n            await this.addMixFile(\"audiomd.mix\");\n        await this.addMixFile(\"audio.mix\");\n        await this.addBagFile(\"audio.bag\");\n        await this.addMixFile(\"conquer.mix\");\n        if (YR) {\n            await this.addMixFile(\"conqmd.mix\");\n            await this.addMixFile(\"genermd.mix\");\n        }\n        await this.addMixFile(\"generic.mix\");\n        if (YR)\n            await this.addMixFile(\"isogenmd.mix\");\n        await this.addMixFile(\"isogen.mix\");\n        if (YR)\n            await this.addMixFile(\"cameomd.mix\");\n        await this.addMixFile(\"cameo.mix\");\n        await this.addMixFile(\"cameocd.mix\");\n        if (YR)\n            await this.addMixFile(\"multimd.mix\");\n        await this.addMixFile(\"multi.mix\");\n        this.logger.info(\"Finished initializing implicit mix files.\");\n    }\n    async loadExtraMixFiles(engineType: EngineType): Promise<void> {\n        this.logger.info(\"Loading extra mix files...\");\n        const rfsEntries = new Set<string>();\n        for await (const entry of this.rfs.getEntries()) {\n            rfsEntries.add(entry.toLowerCase());\n        }\n        const prefixes = [\"ecache\", \"expand\", \"elocal\"];\n        for (const prefix of prefixes) {\n            for (let i = 99; i >= 0; i--) {\n                const numStr = pad(i, \"00\");\n                const baseFilename = `${prefix}${numStr}.mix`;\n                const mdFilename = `${prefix}md${numStr}.mix`;\n                const filesToTry: string[] = [];\n                if (engineType === EngineType.YurisRevenge) {\n                    filesToTry.push(mdFilename);\n                }\n                filesToTry.push(baseFilename);\n                for (const fileToTry of filesToTry) {\n                    if (rfsEntries.has(fileToTry)) {\n                        if (!this.hasArchive(fileToTry)) {\n                            await this.addMixFile(fileToTry);\n                        }\n                    }\n                }\n            }\n        }\n        const mapExtensions = [\".mmx\"];\n        if (engineType === EngineType.YurisRevenge) {\n            mapExtensions.push(\".yro\");\n        }\n        for (const ext of mapExtensions) {\n            for (const rfsFile of rfsEntries) {\n                if (rfsFile.endsWith(ext)) {\n                    if (!this.hasArchive(rfsFile)) {\n                        const fileData = await this.rfs.openFile(rfsFile);\n                        if (fileData) {\n                            this.addArchive(new MixFile(fileData.stream), rfsFile);\n                        }\n                        else {\n                            this.logger.warn(`Could not open RFS file ${rfsFile} for map archive loading.`);\n                        }\n                    }\n                }\n            }\n        }\n        this.logger.info(\"Finished loading extra mix files.\");\n    }\n    async loadStandaloneFiles(options?: {\n        exclude?: string[];\n    }): Promise<void> {\n        this.logger.info(\"Loading standalone files into mem.archive...\");\n        const extensionsToLoad = [\"ini\", \"csf\"];\n        const excludeSet = new Set<string>((options?.exclude || []).map(f => f.toLowerCase()));\n        const filesForMemArchive: VirtualFile[] = [];\n        for await (const entryName of this.rfs.getEntries()) {\n            const lowerEntryName = entryName.toLowerCase();\n            if (extensionsToLoad.some((ext) => lowerEntryName.endsWith(\".\" + ext)) &&\n                !excludeSet.has(lowerEntryName)) {\n                try {\n                    const file = await this.rfs.openFile(entryName);\n                    if (file) {\n                        filesForMemArchive.push(file);\n                    }\n                }\n                catch (e) {\n                    if (e instanceof FileNotFoundError) {\n                        this.logger.warn(`Standalone file ${entryName} not found during VFS loadStandaloneFiles.`);\n                    }\n                    else {\n                        throw e;\n                    }\n                }\n            }\n        }\n        if (filesForMemArchive.length > 0) {\n            const memArchive = new MemArchive();\n            for (const vf of filesForMemArchive) {\n                memArchive.addFile(vf);\n            }\n            this.addArchive(memArchive, \"mem.archive\");\n            this.logger.info(`Added ${filesForMemArchive.length} standalone files to mem.archive`);\n        }\n        else {\n            this.logger.info(\"No standalone files found or added to mem.archive.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/data/vxl/Section.ts",
    "content": "import * as Normals from \"./normals\";\nimport { VoxelField } from \"./VoxelField\";\nimport type { Voxel } from \"./Voxel\";\nimport type { Span } from \"./Span\";\nimport { Matrix4, Vector3 } from 'three';\nexport interface PlainSection {\n    name: string;\n    normalsMode: number;\n    minBounds: number[];\n    maxBounds: number[];\n    sizeX: number;\n    sizeY: number;\n    sizeZ: number;\n    hvaMultiplier: number;\n    transfMatrix: number[];\n    spans: Span[];\n}\nexport class Section {\n    public name: string = \"\";\n    public normalsMode: number = 1;\n    public minBounds: Vector3 = new Vector3();\n    public maxBounds: Vector3 = new Vector3();\n    public sizeX: number = 0;\n    public sizeY: number = 0;\n    public sizeZ: number = 0;\n    public hvaMultiplier: number = 1.0;\n    public transfMatrix: Matrix4 = new Matrix4();\n    public spans: Span[] = [];\n    get spanX(): number {\n        return this.maxBounds.x - this.minBounds.x;\n    }\n    get spanY(): number {\n        return this.maxBounds.y - this.minBounds.y;\n    }\n    get spanZ(): number {\n        return this.maxBounds.z - this.minBounds.z;\n    }\n    get scaleX(): number {\n        return this.sizeX === 0 ? 1 : this.spanX / this.sizeX;\n    }\n    get scaleY(): number {\n        return this.sizeY === 0 ? 1 : this.spanY / this.sizeY;\n    }\n    get scaleZ(): number {\n        return this.sizeZ === 0 ? 1 : this.spanZ / this.sizeZ;\n    }\n    get scale(): Vector3 {\n        return new Vector3(this.scaleX, this.scaleY, this.scaleZ);\n    }\n    getAllVoxels(): {\n        voxels: Voxel[];\n        voxelField: VoxelField;\n    } {\n        const allVoxels: Voxel[] = [];\n        const field = new VoxelField(this.sizeX + 1, this.sizeY + 1, this.sizeZ + 1);\n        for (const span of this.spans) {\n            if (span.voxels) {\n                for (const voxel of span.voxels) {\n                    allVoxels.push(voxel);\n                    field.add(voxel);\n                }\n            }\n        }\n        return { voxels: allVoxels, voxelField: field };\n    }\n    getNormals(): Vector3[] {\n        switch (this.normalsMode) {\n            case 1: return Normals.normals1;\n            case 2: return Normals.normals2;\n            case 3: return Normals.normals3;\n            case 4: return Normals.normals4;\n            default:\n                console.warn(`Invalid normalsMode ${this.normalsMode}, defaulting to normals1.`);\n                return Normals.normals1;\n        }\n    }\n    scaleHvaMatrix(matrix: Matrix4): Matrix4 {\n        const newMatrix = matrix.clone ? matrix.clone() : new Matrix4().fromArray!(matrix.elements);\n        if (newMatrix.elements.length >= 15) {\n            newMatrix.elements[12] *= this.hvaMultiplier;\n            newMatrix.elements[13] *= this.hvaMultiplier;\n            newMatrix.elements[14] *= this.hvaMultiplier;\n        }\n        return newMatrix;\n    }\n    toPlain(): PlainSection {\n        return {\n            name: this.name,\n            normalsMode: this.normalsMode,\n            minBounds: this.minBounds.toArray ? this.minBounds.toArray() : [this.minBounds.x, this.minBounds.y, this.minBounds.z],\n            maxBounds: this.maxBounds.toArray ? this.maxBounds.toArray() : [this.maxBounds.x, this.maxBounds.y, this.maxBounds.z],\n            sizeX: this.sizeX,\n            sizeY: this.sizeY,\n            sizeZ: this.sizeZ,\n            hvaMultiplier: this.hvaMultiplier,\n            transfMatrix: this.transfMatrix.toArray ? this.transfMatrix.toArray() : [...this.transfMatrix.elements],\n            spans: this.spans,\n        };\n    }\n    fromPlain(plain: PlainSection): this {\n        this.name = plain.name;\n        this.normalsMode = plain.normalsMode;\n        this.minBounds = new Vector3().fromArray!(plain.minBounds);\n        this.maxBounds = new Vector3().fromArray!(plain.maxBounds);\n        this.sizeX = plain.sizeX;\n        this.sizeY = plain.sizeY;\n        this.sizeZ = plain.sizeZ;\n        this.hvaMultiplier = plain.hvaMultiplier;\n        this.transfMatrix = new Matrix4().fromArray!(plain.transfMatrix);\n        this.spans = plain.spans;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/data/vxl/Span.ts",
    "content": "import type { Voxel } from './Voxel';\nexport interface Span {\n    voxels: Voxel[];\n    startIndex: number;\n    endIndex: number;\n}\n"
  },
  {
    "path": "src/data/vxl/SpanOffsets.ts",
    "content": "export class SpanOffsets {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/data/vxl/Voxel.ts",
    "content": "export interface Voxel {\n    x: number;\n    y: number;\n    z: number;\n    colorIndex: number;\n    normalIndex?: number;\n}\n"
  },
  {
    "path": "src/data/vxl/VoxelField.ts",
    "content": "import type { Voxel } from './Voxel';\nexport class VoxelField {\n    public sizeX: number;\n    public sizeY: number;\n    public sizeZ: number;\n    private arr: (Voxel | undefined)[];\n    constructor(sizeX: number, sizeY: number, sizeZ: number) {\n        this.sizeX = sizeX;\n        this.sizeY = sizeY;\n        this.sizeZ = sizeZ;\n        this.arr = new Array(sizeX * sizeY * sizeZ).fill(undefined);\n    }\n    add(voxel: Voxel): void {\n        if (voxel.x >= 0 && voxel.x < this.sizeX &&\n            voxel.y >= 0 && voxel.y < this.sizeY &&\n            voxel.z >= 0 && voxel.z < this.sizeZ) {\n            this.arr[voxel.x + voxel.y * this.sizeX + voxel.z * this.sizeX * this.sizeY] =\n                voxel;\n        }\n        else {\n            console.warn(\"VoxelField.add: Voxel coordinates out of bounds.\", voxel);\n        }\n    }\n    get(x: number, y: number, z: number): Voxel | undefined {\n        if (x < 0 || x >= this.sizeX || y < 0 || y >= this.sizeY || z < 0 || z >= this.sizeZ) {\n            return undefined;\n        }\n        return this.arr[x + y * this.sizeX + z * this.sizeX * this.sizeY];\n    }\n    forEachVoxel(callback: (voxel: Voxel, x: number, y: number, z: number) => void): void {\n        for (let z = 0; z < this.sizeZ; z++) {\n            for (let y = 0; y < this.sizeY; y++) {\n                for (let x = 0; x < this.sizeX; x++) {\n                    const voxel = this.get(x, y, z);\n                    if (voxel) {\n                        callback(voxel, x, y, z);\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/data/vxl/VxlHeader.ts",
    "content": "import type { DataStream } from \"../DataStream\";\nexport class VxlHeader {\n    public static readonly size = 32;\n    public fileName: string = \"\";\n    public paletteCount: number = 0;\n    public headerCount: number = 0;\n    public tailerCount: number = 0;\n    public bodySize: number = 0;\n    public paletteRemapStart: number = 0;\n    public paletteRemapEnd: number = 0;\n    read(stream: DataStream): void {\n        this.fileName = stream.readCString(16);\n        this.paletteCount = stream.readUint32();\n        this.headerCount = stream.readUint32();\n        this.tailerCount = stream.readUint32();\n        this.bodySize = stream.readUint32();\n        this.paletteRemapStart = stream.readUint8();\n        this.paletteRemapEnd = stream.readUint8();\n        stream.seek(stream.position + 768);\n    }\n}\n"
  },
  {
    "path": "src/data/vxl/normals.ts",
    "content": "import { Vector3 } from 'three';\nexport const normals1: Vector3[] = [\n    new Vector3(0.54946297, -183e-6, -0.835518),\n    new Vector3(0.00014400001, 0.54940403, -0.83555698),\n    new Vector3(-0.54940403, -68000001e-12, -0.83555698),\n    new Vector3(106e-6, -0.54946297, -0.835518),\n    new Vector3(0.94900799, 0.00031599999, -0.31525001),\n    new Vector3(-186e-6, 0.94899702, -0.31528401),\n    new Vector3(-0.94899702, 0.00031800001, -0.31528401),\n    new Vector3(-447e-6, -0.94900799, -0.31525001),\n    new Vector3(0.95084399, -279e-6, 0.30967101),\n    new Vector3(202e-6, 0.95084798, 0.30965701),\n    new Vector3(-0.95084798, -70000002e-12, 0.30965701),\n    new Vector3(147e-6, -0.95084399, 0.30967101),\n    new Vector3(0.55237001, -11e-6, 0.83359897),\n    new Vector3(19999999e-12, 0.55238003, 0.833592),\n    new Vector3(-0.55238003, 57000001e-12, 0.83359301),\n    new Vector3(-66000001e-12, -0.55237001, 0.83359897),\n];\nexport const normals2: Vector3[] = [\n    new Vector3(0.67121398, 0.19849201, -0.714194),\n    new Vector3(0.26964301, 0.58439398, -0.76536),\n    new Vector3(-0.040546, 0.096988, -0.99445897),\n    new Vector3(-0.57242799, -0.091913998, -0.81478697),\n    new Vector3(-0.17140099, -0.57270998, -0.80163902),\n    new Vector3(0.36255699, -0.30299899, -0.88133103),\n    new Vector3(0.81034702, -0.34897199, -0.470698),\n    new Vector3(0.103962, 0.93867201, -0.328767),\n    new Vector3(-0.324047, 0.58766901, -0.74137598),\n    new Vector3(-0.80086499, 0.34046099, -0.49264699),\n    new Vector3(-0.66549802, -0.59014702, -0.45698899),\n    new Vector3(0.314767, -0.803002, -0.506073),\n    new Vector3(0.97262901, 0.151076, -0.17655),\n    new Vector3(0.680291, 0.68423599, -0.26272699),\n    new Vector3(-0.52007902, 0.82777703, -0.210483),\n    new Vector3(-0.96164399, -0.179001, -0.207847),\n    new Vector3(-0.262714, -0.937451, -0.22840101),\n    new Vector3(0.219707, -0.97130102, 0.091124997),\n    new Vector3(0.92380798, -0.229975, 0.30608699),\n    new Vector3(-0.082488999, 0.97065997, 0.225866),\n    new Vector3(-0.59179801, 0.69678998, 0.40528899),\n    new Vector3(-0.92529601, 0.36660099, 0.097111002),\n    new Vector3(-0.705051, -0.68777502, 0.172828),\n    new Vector3(0.7324, -0.68036699, -0.026304999),\n    new Vector3(0.85516202, 0.37458199, 0.358311),\n    new Vector3(0.47300601, 0.83648002, 0.276705),\n    new Vector3(-0.097617, 0.65411198, 0.750072),\n    new Vector3(-0.90412402, -0.153725, 0.39865801),\n    new Vector3(-0.211916, -0.85808998, 0.46773201),\n    new Vector3(0.50022697, -0.67440802, 0.543091),\n    new Vector3(0.584539, -0.110249, 0.80384099),\n    new Vector3(0.43737301, 0.45464399, 0.77588898),\n    new Vector3(-0.042440999, 0.083318003, 0.995619),\n    new Vector3(-0.59625101, 0.22013199, 0.77202803),\n    new Vector3(-0.506455, -0.39697701, 0.76544899),\n    new Vector3(0.070569001, -0.47847399, 0.87526202),\n];\nexport const normals3: Vector3[] = [\n    new Vector3(0.45651099, -0.073968001, -0.88663799),\n    new Vector3(0.50769401, 0.38511699, -0.77067),\n    new Vector3(0.095431998, 0.22666401, -0.96928602),\n    new Vector3(-0.35876599, 0.54318798, -0.75910097),\n    new Vector3(-0.361276, 0.13299499, -0.92292601),\n    new Vector3(-0.48311701, -0.32406601, -0.813375),\n    new Vector3(-0.018073, -0.197559, -0.980124),\n    new Vector3(0.3211, -0.501477, -0.80337799),\n    new Vector3(0.79949099, 0.069615997, -0.59662998),\n    new Vector3(0.390971, 0.77130598, -0.50222403),\n    new Vector3(0.080782004, 0.61448997, -0.784778),\n    new Vector3(-0.73275, 0.41143101, -0.54203498),\n    new Vector3(-0.73525399, 0.0091019999, -0.67773098),\n    new Vector3(-0.80249399, -0.39490801, -0.44727099),\n    new Vector3(-0.13413, -0.58915502, -0.79680902),\n    new Vector3(0.71955299, -0.37622699, -0.58369303),\n    new Vector3(0.96687502, 0.173593, -0.187132),\n    new Vector3(0.760831, 0.51910597, -0.38944301),\n    new Vector3(-0.114642, 0.87551898, -0.46938601),\n    new Vector3(-0.53236699, 0.76885903, -0.354177),\n    new Vector3(-0.96226698, 0.024977, -0.27095801),\n    new Vector3(-0.46738699, -0.721986, -0.51018202),\n    new Vector3(0.058449998, -0.85235399, -0.51968902),\n    new Vector3(0.49823299, -0.74374002, -0.44566301),\n    new Vector3(0.93915099, -0.27024499, -0.212044),\n    new Vector3(0.58393198, 0.80944198, -0.061857),\n    new Vector3(0.183797, 0.97322798, -0.138007),\n    new Vector3(-0.88435501, 0.45221901, -0.115822),\n    new Vector3(-0.943178, -0.33206701, 0.012138),\n    new Vector3(-0.69844002, -0.70656699, -0.113772),\n    new Vector3(-0.228411, -0.95470601, -0.190694),\n    new Vector3(0.73156399, -0.675861, -0.089588001),\n    new Vector3(0.96925098, 0.046804, 0.24158201),\n    new Vector3(0.85564703, 0.50347698, 0.119916),\n    new Vector3(-0.25115299, 0.96794701, -80999998e-12),\n    new Vector3(-0.64779502, 0.75674897, 0.087711997),\n    new Vector3(-0.96916401, 0.14519399, 0.1991),\n    new Vector3(-0.41479301, -0.88896698, 0.194126),\n    new Vector3(0.25077501, -0.961178, -0.115109),\n    new Vector3(0.47862899, -0.84259301, 0.246883),\n    new Vector3(0.89004397, -0.39614201, 0.225595),\n    new Vector3(0.52405101, 0.76235998, 0.37970701),\n    new Vector3(0.11962, 0.94548202, 0.30291),\n    new Vector3(-0.76085001, 0.49007499, 0.42536199),\n    new Vector3(-0.86978501, -0.20215, 0.450122),\n    new Vector3(-0.70946699, -0.60242403, 0.36570701),\n    new Vector3(0.019308999, -0.95887101, 0.28318599),\n    new Vector3(0.626113, -0.564677, 0.53770101),\n    new Vector3(0.769943, -0.126663, 0.62541503),\n    new Vector3(0.76419097, 0.35070199, 0.54131401),\n    new Vector3(-0.001878, 0.74136698, 0.67109799),\n    new Vector3(-0.37088001, 0.81836802, 0.43900099),\n    new Vector3(-0.71390897, 0.12865201, 0.68831801),\n    new Vector3(-0.295165, -0.73866397, 0.60601401),\n    new Vector3(0.186195, -0.73836899, 0.648184),\n    new Vector3(0.387523, -0.35878301, 0.84917599),\n    new Vector3(0.481022, 0.124846, 0.86777401),\n    new Vector3(0.391808, 0.54505599, 0.741216),\n    new Vector3(-0.0035359999, 0.36559799, 0.93076599),\n    new Vector3(-0.42049801, 0.484961, 0.76680797),\n    new Vector3(-0.35490301, 0.019470001, 0.93470001),\n    new Vector3(-0.54783702, -0.35920799, 0.75554299),\n    new Vector3(-0.106662, -0.445115, 0.88909799),\n    new Vector3(0.086796001, -0.059307002, 0.99445897),\n];\nexport const normals4: Vector3[] = [\n    new Vector3(0.52657801, -0.35962099, -0.77031702),\n    new Vector3(0.150482, 0.43598399, 0.88728398),\n    new Vector3(0.414195, 0.73825502, -0.53237402),\n    new Vector3(0.075152002, 0.91624898, -0.393498),\n    new Vector3(-0.316149, 0.93073601, -0.18379299),\n    new Vector3(-0.77381903, 0.62333399, -0.11251),\n    new Vector3(-0.90084201, 0.42853701, -0.069568001),\n    new Vector3(-0.99894202, -0.010971, 0.044665001),\n    new Vector3(-0.979761, -0.15767001, -0.123324),\n    new Vector3(-0.91127402, -0.362371, -0.19562),\n    new Vector3(-0.62406898, -0.72094101, -0.301301),\n    new Vector3(-0.310173, -0.80934501, -0.498752),\n    new Vector3(0.146613, -0.81581903, -0.55941403),\n    new Vector3(-0.71651602, -0.69435602, -0.066887997),\n    new Vector3(0.50397199, -0.114202, -0.85613698),\n    new Vector3(0.45549101, 0.87262702, -0.176211),\n    new Vector3(-0.00501, -0.114373, -0.99342501),\n    new Vector3(-0.104675, -0.327701, -0.93896502),\n    new Vector3(0.56041199, 0.75258899, -0.34575599),\n    new Vector3(-0.060575999, 0.82162797, -0.566796),\n    new Vector3(-0.30234101, 0.79700702, -0.522847),\n    new Vector3(-0.671543, 0.67074001, -0.314863),\n    new Vector3(-0.77840102, -0.12835699, 0.61450499),\n    new Vector3(-0.92404997, 0.278382, -0.261985),\n    new Vector3(-0.69977301, -0.55049098, -0.45527801),\n    new Vector3(-0.56824797, -0.51718903, -0.64000797),\n    new Vector3(0.054097999, -0.93286401, -0.356143),\n    new Vector3(0.75838202, 0.57289302, -0.31088799),\n    new Vector3(0.0036200001, 0.30502599, -0.95233703),\n    new Vector3(-0.060849998, -0.98688602, -0.14951099),\n    new Vector3(0.63523, 0.045478001, -0.77098298),\n    new Vector3(0.52170497, 0.241309, -0.81828701),\n    new Vector3(0.26940399, 0.63542497, -0.72364098),\n    new Vector3(0.045676, 0.67275399, -0.738455),\n    new Vector3(-0.180511, 0.67465699, -0.71571898),\n    new Vector3(-0.397131, 0.63664001, -0.66104198),\n    new Vector3(-0.55200398, 0.47251499, -0.687038),\n    new Vector3(-0.77217001, 0.08309, -0.62996),\n    new Vector3(-0.669819, -0.119533, -0.73284),\n    new Vector3(-0.54045498, -0.31844401, -0.77878201),\n    new Vector3(-0.38613501, -0.522789, -0.75999397),\n    new Vector3(-0.261466, -0.68856698, -0.676395),\n    new Vector3(-0.019412, -0.69610298, -0.71767998),\n    new Vector3(0.30356899, -0.48184401, -0.82199299),\n    new Vector3(0.68193901, -0.19512901, -0.70490003),\n    new Vector3(-0.24488901, -0.116562, -0.96251899),\n    new Vector3(0.80075902, -0.022979001, -0.59854603),\n    new Vector3(-0.37027499, 0.095583998, -0.92399102),\n    new Vector3(-0.33067101, -0.32657799, -0.88543999),\n    new Vector3(-0.16322, -0.52757901, -0.83367902),\n    new Vector3(0.12639, -0.313146, -0.941257),\n    new Vector3(0.34954801, -0.27222601, -0.89649802),\n    new Vector3(0.23991799, -0.085825004, -0.96699202),\n    new Vector3(0.390845, 0.081537001, -0.91683799),\n    new Vector3(0.25526699, 0.26869699, -0.92878503),\n    new Vector3(0.146245, 0.48043799, -0.86474901),\n    new Vector3(-0.32601601, 0.47845599, -0.81534898),\n    new Vector3(-0.46968201, -0.112519, -0.87563598),\n    new Vector3(0.81844002, -0.25852001, -0.51315099),\n    new Vector3(-0.474318, 0.292238, -0.83043301),\n    new Vector3(0.778943, 0.39584199, -0.48637101),\n    new Vector3(0.62409401, 0.39377299, -0.67487001),\n    new Vector3(0.74088597, 0.203834, -0.63995302),\n    new Vector3(0.48021701, 0.565768, -0.67029703),\n    new Vector3(0.38093001, 0.42453501, -0.82137799),\n    new Vector3(-0.093422003, 0.50112402, -0.86031801),\n    new Vector3(-0.236485, 0.29619801, -0.92538702),\n    new Vector3(-0.131531, 0.093959004, -0.98684901),\n    new Vector3(-0.82356203, 0.29577699, -0.48400599),\n    new Vector3(0.61106598, -0.624304, -0.486664),\n    new Vector3(0.069495998, -0.52033001, -0.85113299),\n    new Vector3(0.226522, -0.66487902, -0.711775),\n    new Vector3(0.47130799, -0.56890398, -0.67395699),\n    new Vector3(0.38842499, -0.74262398, -0.54556),\n    new Vector3(0.78367501, -0.48072901, -0.39338499),\n    new Vector3(0.962394, 0.135676, -0.235349),\n    new Vector3(0.876607, 0.172034, -0.449406),\n    new Vector3(0.63340503, 0.58979303, -0.50094098),\n    new Vector3(0.182276, 0.80065799, -0.57072097),\n    new Vector3(0.177003, 0.76413399, 0.62029701),\n    new Vector3(-0.544016, 0.675515, -0.49772099),\n    new Vector3(-0.67929697, 0.28646699, -0.67564201),\n    new Vector3(-0.59039098, 0.091369003, -0.801929),\n    new Vector3(-0.82436001, -0.13312399, -0.55018902),\n    new Vector3(-0.71579403, -0.33454201, -0.61296099),\n    new Vector3(0.17428599, -0.89248401, 0.416049),\n    new Vector3(-0.082528003, -0.83712298, -0.54075301),\n    new Vector3(0.28333101, -0.88087398, -0.37918901),\n    new Vector3(0.675134, -0.42662701, -0.60181701),\n    new Vector3(0.84372002, -0.512335, -0.160156),\n    new Vector3(0.97730398, -0.098555997, -0.18752),\n    new Vector3(0.846295, 0.522672, -0.102947),\n    new Vector3(0.67714101, 0.72132498, -0.145501),\n    new Vector3(0.32096499, 0.87089199, -0.37219399),\n    new Vector3(-0.178978, 0.911533, -0.37023601),\n    new Vector3(-0.44716901, 0.82670099, -0.341474),\n    new Vector3(-0.70320302, 0.496328, -0.50908101),\n    new Vector3(-0.97718102, 0.063562997, -0.202674),\n    new Vector3(-0.87817001, -0.412938, 0.241455),\n    new Vector3(-0.83583099, -0.35855001, -0.415728),\n    new Vector3(-0.499174, -0.69343299, -0.51959199),\n    new Vector3(-0.188789, -0.92375302, -0.33322501),\n    new Vector3(0.19225401, -0.96936101, -0.152896),\n    new Vector3(0.51594001, -0.783907, -0.34539199),\n    new Vector3(0.90592498, -0.30095199, -0.29787099),\n    new Vector3(0.99111199, -0.127746, 0.037106998),\n    new Vector3(0.99513501, 0.098424003, -0.0043830001),\n    new Vector3(0.76012301, 0.64627701, 0.067367002),\n    new Vector3(0.205221, 0.95958, -0.192591),\n    new Vector3(-0.042750001, 0.97951299, -0.19679099),\n    new Vector3(-0.43801701, 0.89892697, 0.0084920004),\n    new Vector3(-0.82199401, 0.48078501, -0.30523899),\n    new Vector3(-0.89991701, 0.081710003, -0.42833701),\n    new Vector3(-0.92661202, -0.144618, -0.347096),\n    new Vector3(-0.79365999, -0.55779201, -0.24283899),\n    new Vector3(-0.43134999, -0.84777898, -0.30855799),\n    new Vector3(-0.0054919999, -0.96499997, 0.26219299),\n    new Vector3(0.58790499, -0.80402601, -0.088940002),\n    new Vector3(0.69949299, -0.66768599, -0.254765),\n    new Vector3(0.88930303, 0.359795, -0.282291),\n    new Vector3(0.780972, 0.197037, 0.59267199),\n    new Vector3(0.52012098, 0.50669599, 0.68755698),\n    new Vector3(0.40389499, 0.69396102, 0.59605998),\n    new Vector3(-0.154983, 0.89923602, 0.40909001),\n    new Vector3(-0.65733802, 0.53716803, 0.528543),\n    new Vector3(-0.74619502, 0.33409101, 0.575827),\n    new Vector3(-0.62495202, -0.049144, 0.77911502),\n    new Vector3(0.31814101, -0.254715, 0.913185),\n    new Vector3(-0.555897, 0.405294, 0.725752),\n    new Vector3(-0.79443401, 0.099405997, 0.59916002),\n    new Vector3(-0.64036101, -0.68946302, 0.33849499),\n    new Vector3(-0.12671299, -0.73409498, 0.66711998),\n    new Vector3(0.105457, -0.78081697, 0.61579502),\n    new Vector3(0.40799299, -0.48091599, 0.77605498),\n    new Vector3(0.69513601, -0.54512, 0.468647),\n    new Vector3(0.97319102, -0.0064889998, 0.229908),\n    new Vector3(0.94689399, 0.317509, -0.050799001),\n    new Vector3(0.56358302, 0.82561201, 0.027183),\n    new Vector3(0.325773, 0.94542301, 0.0069490001),\n    new Vector3(-0.171821, 0.98509699, -0.0078149997),\n    new Vector3(-0.67044097, 0.73993897, 0.054768998),\n    new Vector3(-0.822981, 0.55496198, 0.121322),\n    new Vector3(-0.96619302, 0.117857, 0.229307),\n    new Vector3(-0.95376903, -0.29470399, 0.058945),\n    new Vector3(-0.86438698, -0.50272799, -0.010015),\n    new Vector3(-0.53060901, -0.84200603, -0.097365998),\n    new Vector3(-0.162618, -0.98407501, 0.071772002),\n    new Vector3(0.081446998, -0.99601102, 0.036439002),\n    new Vector3(0.74598402, -0.66596299, 0.00076199998),\n    new Vector3(0.94205701, -0.32926899, -0.064106002),\n    new Vector3(0.93970197, -0.28108999, 0.194803),\n    new Vector3(0.77121401, 0.55067003, 0.319363),\n    new Vector3(0.641348, 0.73069, 0.23402099),\n    new Vector3(0.080682002, 0.99669099, 0.0098789996),\n    new Vector3(-0.046725001, 0.97664303, 0.20972501),\n    new Vector3(-0.53107601, 0.82100099, 0.209562),\n    new Vector3(-0.69581503, 0.65599, 0.29243499),\n    new Vector3(-0.97612202, 0.216709, -0.014913),\n    new Vector3(-0.96166098, -0.14412899, 0.23331399),\n    new Vector3(-0.772084, -0.61364698, 0.165299),\n    new Vector3(-0.44960001, -0.83605999, 0.314426),\n    new Vector3(-0.39269999, -0.91461599, 0.096247002),\n    new Vector3(0.390589, -0.91947001, 0.044890001),\n    new Vector3(0.58252901, -0.79919797, 0.148127),\n    new Vector3(0.866431, -0.48981199, 0.096864),\n    new Vector3(0.90458697, 0.111498, 0.41145),\n    new Vector3(0.95353699, 0.23232999, 0.191806),\n    new Vector3(0.497311, 0.77080297, 0.398177),\n    new Vector3(0.194066, 0.95631999, 0.218611),\n    new Vector3(0.422876, 0.882276, 0.206797),\n    new Vector3(-0.373797, 0.84956598, 0.37217399),\n    new Vector3(-0.53449702, 0.71402299, 0.4522),\n    new Vector3(-0.881827, 0.23716, 0.40759799),\n    new Vector3(-0.904948, -0.014069, 0.42528901),\n    new Vector3(-0.751827, -0.51281703, 0.41445801),\n    new Vector3(-0.50101501, -0.69791698, 0.51175803),\n    new Vector3(-0.23519, -0.92592299, 0.295555),\n    new Vector3(0.228983, -0.95393997, 0.193819),\n    new Vector3(0.734025, -0.63489801, 0.241062),\n    new Vector3(0.91375297, -0.063253, -0.40131599),\n    new Vector3(0.90573502, -0.161487, 0.391875),\n    new Vector3(0.85892999, 0.342446, 0.38074899),\n    new Vector3(0.62448603, 0.60758102, 0.49077699),\n    new Vector3(0.28926399, 0.85747898, 0.42550799),\n    new Vector3(0.069968, 0.90216899, 0.42567101),\n    new Vector3(-0.28617999, 0.94069999, 0.182165),\n    new Vector3(-0.57401299, 0.80511898, -0.14930899),\n    new Vector3(0.111258, 0.099717997, -0.98877603),\n    new Vector3(-0.30539301, -0.94422799, -0.12316),\n    new Vector3(-0.60116601, -0.78957599, 0.123163),\n    new Vector3(-0.290645, -0.81213999, 0.50591898),\n    new Vector3(-0.064920001, -0.87716299, 0.47578499),\n    new Vector3(0.408301, -0.862216, 0.29978901),\n    new Vector3(0.56609702, -0.72556603, 0.39126399),\n    new Vector3(0.83936399, -0.427387, 0.33586901),\n    new Vector3(0.81889999, -0.041305002, 0.57244802),\n    new Vector3(0.71978402, 0.41499701, 0.55649698),\n    new Vector3(0.88174403, 0.45027, 0.140659),\n    new Vector3(0.40182301, -0.89822, -0.17815199),\n    new Vector3(-0.054019999, 0.79134399, 0.60898),\n    new Vector3(-0.29377401, 0.76399398, 0.57446498),\n    new Vector3(-0.450798, 0.61034697, 0.65135098),\n    new Vector3(-0.63822103, 0.186694, 0.74687302),\n    new Vector3(-0.87287003, -0.25712699, 0.41470799),\n    new Vector3(-0.58725703, -0.52170998, 0.618828),\n    new Vector3(-0.35365799, -0.64197397, 0.680291),\n    new Vector3(0.041648999, -0.61127299, 0.79032302),\n    new Vector3(0.348342, -0.77918297, 0.52108699),\n    new Vector3(0.499167, -0.62244099, 0.602826),\n    new Vector3(0.79001898, -0.30383101, 0.53250003),\n    new Vector3(0.66011798, 0.060733002, 0.74870199),\n    new Vector3(0.60492098, 0.29416099, 0.73996001),\n    new Vector3(0.38569701, 0.37934601, 0.84103203),\n    new Vector3(0.239693, 0.207876, 0.94833201),\n    new Vector3(0.012623, 0.25853199, 0.96591997),\n    new Vector3(-0.100557, 0.457147, 0.88368797),\n    new Vector3(0.046967, 0.62858802, 0.77631903),\n    new Vector3(-0.43039101, -0.44540501, 0.785097),\n    new Vector3(-0.43429101, -0.196228, 0.87913901),\n    new Vector3(-0.25663701, -0.336867, 0.90590203),\n    new Vector3(-0.131372, -0.15891001, 0.97851402),\n    new Vector3(0.102379, -0.208767, 0.972592),\n    new Vector3(0.195687, -0.450129, 0.87125802),\n    new Vector3(0.62731898, -0.42314801, 0.65377098),\n    new Vector3(0.68743902, -0.171583, 0.70568198),\n    new Vector3(0.27592, -0.021255, 0.96094602),\n    new Vector3(0.45936701, 0.15746599, 0.87417799),\n    new Vector3(0.285395, 0.583184, 0.76055598),\n    new Vector3(-0.81217402, 0.46030301, 0.35846099),\n    new Vector3(-0.189068, 0.64122301, 0.743698),\n    new Vector3(-0.338875, 0.47648001, 0.811252),\n    new Vector3(-0.92099398, 0.347186, 0.176727),\n    new Vector3(0.040638998, 0.024465, 0.99887401),\n    new Vector3(-0.73913199, -0.35374701, 0.57318997),\n    new Vector3(-0.60351199, -0.28661501, 0.74405998),\n    new Vector3(-0.188676, -0.547059, 0.81555402),\n    new Vector3(-0.026045, -0.39782, 0.91709399),\n    new Vector3(0.26789701, -0.649041, 0.71202302),\n    new Vector3(0.518246, -0.28489101, 0.80638599),\n    new Vector3(0.493451, -0.066532999, 0.86722499),\n    new Vector3(-0.328188, 0.140251, 0.93414301),\n    new Vector3(0.328188, 0.140251, 0.93414301),\n    new Vector3(-0.328188, 0.140251, 0.93414301),\n    new Vector3(-0.328188, 0.140251, 0.93414301),\n    new Vector3(-0.328188, 0.140251, 0.93414301),\n];\n"
  },
  {
    "path": "src/data/zip/Zip.ts",
    "content": "import { Crc32 } from '../Crc32';\nimport { ZipUtils } from './ZipUtils';\ninterface FileRecord {\n    name: string;\n    sizeBig: bigint;\n    crc: Crc32;\n    done: boolean;\n    date: Date;\n    headerOffsetBig: bigint;\n}\ninterface ByteArrayData {\n    data: number | bigint | Uint8Array;\n    size?: number;\n}\nexport class Zip {\n    private zip64: boolean;\n    private fileRecord: FileRecord[];\n    private finished: boolean;\n    private byteCounterBig: bigint;\n    private outputStream: ReadableStream<Uint8Array>;\n    private outputController: ReadableStreamDefaultController<Uint8Array>;\n    constructor(zip64: boolean = false) {\n        this.zip64 = zip64;\n        console.info(\"Started zip with zip64: \" + this.zip64);\n        this.fileRecord = [];\n        this.finished = false;\n        this.byteCounterBig = BigInt(0);\n        this.outputStream = new ReadableStream<Uint8Array>({\n            start: (controller) => {\n                console.info(\"OutputStream has started!\");\n                this.outputController = controller;\n            },\n            cancel: () => {\n                console.info(\"OutputStream has been canceled!\");\n            },\n        });\n    }\n    private enqueue(data: Uint8Array): void {\n        this.outputController.enqueue(data);\n    }\n    private close(): void {\n        this.outputController.close();\n    }\n    private getZip64ExtraField(sizeBig: bigint, offsetBig: bigint): Uint8Array {\n        return ZipUtils.createByteArray([\n            { data: 1, size: 2 },\n            { data: 28, size: 2 },\n            { data: sizeBig, size: 8 },\n            { data: sizeBig, size: 8 },\n            { data: offsetBig, size: 8 },\n            { data: 0, size: 4 },\n        ]);\n    }\n    private isWritingFile(): boolean {\n        return (0 < this.fileRecord.length &&\n            false === this.fileRecord[this.fileRecord.length - 1].done);\n    }\n    public startFile(fileName: string, fileDate: Date): void {\n        if (this.isWritingFile() || this.finished) {\n            throw new Error(\"Tried adding file while adding other file or while zip has finished\");\n        }\n        console.info(\"Start file: \" + fileName);\n        const date = new Date(fileDate);\n        this.fileRecord = [\n            ...this.fileRecord,\n            {\n                name: fileName,\n                sizeBig: BigInt(0),\n                crc: new Crc32(),\n                done: false,\n                date: date,\n                headerOffsetBig: this.byteCounterBig,\n            },\n        ];\n        const encodedFileName = new TextEncoder().encode(fileName);\n        const headerData = ZipUtils.createByteArray([\n            { data: 67324752, size: 4 },\n            { data: 45, size: 2 },\n            { data: 2056, size: 2 },\n            { data: 0, size: 2 },\n            { data: ZipUtils.getTimeStruct(date), size: 2 },\n            { data: ZipUtils.getDateStruct(date), size: 2 },\n            { data: 0, size: 4 },\n            { data: this.zip64 ? 4294967295 : 0, size: 4 },\n            { data: this.zip64 ? 4294967295 : 0, size: 4 },\n            { data: encodedFileName.length, size: 2 },\n            { data: this.zip64 ? 32 : 0, size: 2 },\n            { data: encodedFileName },\n            {\n                data: this.zip64\n                    ? this.getZip64ExtraField(BigInt(0), this.byteCounterBig)\n                    : new Uint8Array(0),\n            },\n        ]);\n        this.enqueue(headerData);\n        this.byteCounterBig += BigInt(headerData.length);\n    }\n    public appendData(data: Uint8Array): void {\n        if (!this.isWritingFile() || this.finished) {\n            throw new Error(\"Tried to append file data, but there is no open file!\");\n        }\n        this.enqueue(data);\n        this.byteCounterBig += BigInt(data.length);\n        this.fileRecord[this.fileRecord.length - 1].crc.append(data);\n        this.fileRecord[this.fileRecord.length - 1].sizeBig += BigInt(data.length);\n    }\n    public endFile(): void {\n        if (!this.isWritingFile() || this.finished) {\n            throw new Error(\"Tried to end file, but there is no open file!\");\n        }\n        const currentFile = this.fileRecord[this.fileRecord.length - 1];\n        console.info(\"End file: \" + currentFile.name);\n        const dataDescriptor = ZipUtils.createByteArray([\n            { data: currentFile.crc.get(), size: 4 },\n            { data: currentFile.sizeBig, size: this.zip64 ? 8 : 4 },\n            { data: currentFile.sizeBig, size: this.zip64 ? 8 : 4 },\n        ]);\n        this.enqueue(dataDescriptor);\n        this.byteCounterBig += BigInt(dataDescriptor.length);\n        this.fileRecord[this.fileRecord.length - 1].done = true;\n    }\n    public finish(): void {\n        if (this.isWritingFile() || this.finished) {\n            throw new Error(\"Empty zip, or there is still a file open\");\n        }\n        console.info(\"Finishing zip\");\n        let centralDirectorySize = BigInt(0);\n        const centralDirectoryOffset = this.byteCounterBig;\n        this.fileRecord.forEach((fileRecord) => {\n            const { date, crc, sizeBig, name, headerOffsetBig, } = fileRecord;\n            const encodedFileName = new TextEncoder().encode(name);\n            const centralDirectoryRecord = ZipUtils.createByteArray([\n                { data: 33639248, size: 4 },\n                { data: 45, size: 2 },\n                { data: 45, size: 2 },\n                { data: 2056, size: 2 },\n                { data: 0, size: 2 },\n                { data: ZipUtils.getTimeStruct(date), size: 2 },\n                { data: ZipUtils.getDateStruct(date), size: 2 },\n                { data: crc.get(), size: 4 },\n                { data: this.zip64 ? 4294967295 : sizeBig, size: 4 },\n                { data: this.zip64 ? 4294967295 : sizeBig, size: 4 },\n                { data: encodedFileName.length, size: 2 },\n                { data: this.zip64 ? 32 : 0, size: 2 },\n                { data: 0, size: 2 },\n                { data: 0, size: 2 },\n                { data: 0, size: 2 },\n                { data: 0, size: 4 },\n                { data: this.zip64 ? 4294967295 : headerOffsetBig, size: 4 },\n                { data: encodedFileName },\n                {\n                    data: this.zip64 ? this.getZip64ExtraField(sizeBig, headerOffsetBig) : new Uint8Array(0),\n                },\n            ]);\n            this.enqueue(centralDirectoryRecord);\n            this.byteCounterBig += BigInt(centralDirectoryRecord.length);\n            centralDirectorySize += BigInt(centralDirectoryRecord.length);\n        });\n        if (this.zip64) {\n            const zip64EndOfCentralDirectoryOffset = this.byteCounterBig;\n            const zip64EndOfCentralDirectoryRecord = ZipUtils.createByteArray([\n                { data: 101075792, size: 4 },\n                { data: 44, size: 8 },\n                { data: 45, size: 2 },\n                { data: 45, size: 2 },\n                { data: 0, size: 4 },\n                { data: 0, size: 4 },\n                { data: this.fileRecord.length, size: 8 },\n                { data: this.fileRecord.length, size: 8 },\n                { data: centralDirectorySize, size: 8 },\n                { data: centralDirectoryOffset, size: 8 },\n            ]);\n            this.enqueue(zip64EndOfCentralDirectoryRecord);\n            this.byteCounterBig += BigInt(zip64EndOfCentralDirectoryRecord.length);\n            const zip64EndOfCentralDirectoryLocator = ZipUtils.createByteArray([\n                { data: 117853008, size: 4 },\n                { data: 0, size: 4 },\n                { data: zip64EndOfCentralDirectoryOffset, size: 8 },\n                { data: 1, size: 4 },\n            ]);\n            this.enqueue(zip64EndOfCentralDirectoryLocator);\n            this.byteCounterBig += BigInt(zip64EndOfCentralDirectoryLocator.length);\n        }\n        const endOfCentralDirectoryRecord = ZipUtils.createByteArray([\n            { data: 101010256, size: 4 },\n            { data: 0, size: 2 },\n            { data: 0, size: 2 },\n            {\n                data: this.zip64 ? 65535 : this.fileRecord.length,\n                size: 2,\n            },\n            {\n                data: this.zip64 ? 65535 : this.fileRecord.length,\n                size: 2,\n            },\n            { data: this.zip64 ? 4294967295 : centralDirectorySize, size: 4 },\n            { data: this.zip64 ? 4294967295 : centralDirectoryOffset, size: 4 },\n            { data: 0, size: 2 },\n        ]);\n        this.enqueue(endOfCentralDirectoryRecord);\n        this.close();\n        this.byteCounterBig += BigInt(endOfCentralDirectoryRecord.length);\n        this.finished = true;\n        console.info(\"Done writing zip file. \" +\n            `Wrote ${this.fileRecord.length} files and a total of ${this.byteCounterBig} bytes.`);\n    }\n    public getOutputStream(): ReadableStream<Uint8Array> {\n        return this.outputStream;\n    }\n}\n"
  },
  {
    "path": "src/data/zip/ZipUtils.ts",
    "content": "export class ZipUtils {\n    static createByteArray(entries: {\n        size?: number;\n        data: Uint8Array | number | string | bigint;\n    }[]): Uint8Array {\n        const totalSize = entries.reduce((sum, entry) => sum + (entry.size || (entry.data as any).length), 0);\n        const result = new Uint8Array(totalSize);\n        const view = new DataView(result.buffer);\n        let offset = 0;\n        entries.forEach((entry) => {\n            if (entry.data instanceof Uint8Array) {\n                result.set(entry.data, offset);\n                offset += entry.data.length;\n            }\n            else {\n                switch (entry.size) {\n                    case 1:\n                        view.setInt8(offset, parseInt(entry.data.toString()));\n                        break;\n                    case 2:\n                        view.setInt16(offset, parseInt(entry.data.toString()), true);\n                        break;\n                    case 4:\n                        view.setInt32(offset, parseInt(entry.data.toString()), true);\n                        break;\n                    case 8:\n                        view.setBigInt64(offset, BigInt(entry.data.toString()), true);\n                        break;\n                    default:\n                        throw new Error(`createByteArray: No handler defined for data size ${entry.size} of entry data ${JSON.stringify(entry.data)}`);\n                }\n                offset += entry.size!;\n            }\n        });\n        return result;\n    }\n    static getTimeStruct(date: Date): number {\n        return (((date.getHours() << 6) | date.getMinutes()) << 5) | (date.getSeconds() / 2);\n    }\n    static getDateStruct(date: Date): number {\n        return ((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) << 5) | date.getDate();\n    }\n}\n"
  },
  {
    "path": "src/engine/AnimProps.ts",
    "content": "import { IniSection } from '../data/IniSection';\nimport { ShpFile } from '../data/ShpFile';\nimport { GameSpeed } from '../game/GameSpeed';\nimport { getRandomInt } from '../util/math';\nexport class AnimProps {\n    static defaultRate = GameSpeed.BASE_TICKS_PER_SECOND;\n    public shadow: boolean;\n    public reverse: boolean;\n    public frameCount: number;\n    public end: number;\n    public rate: number;\n    public start: number;\n    public loopStart: number;\n    public loopEnd: number;\n    public loopCount: number;\n    public randomLoopDelay?: [\n        number,\n        number\n    ];\n    constructor(public art: IniSection, frameCountOrShpFile: number | ShpFile) {\n        this.init(frameCountOrShpFile);\n    }\n    private init(frameCountOrShpFile: number | ShpFile): void {\n        this.shadow = this.art.getBool(\"Shadow\");\n        this.reverse = this.art.getBool(\"Reverse\");\n        this.frameCount = typeof frameCountOrShpFile === \"number\"\n            ? frameCountOrShpFile\n            : this.shadow\n                ? frameCountOrShpFile.numImages / 2\n                : frameCountOrShpFile.numImages;\n        this.end = this.art.getNumber(\"End\", this.frameCount - 1);\n        const randomRateArray = this.art.getNumberArray(\"RandomRate\").sort();\n        if (randomRateArray.length === 2) {\n            this.rate = getRandomInt(randomRateArray[0], randomRateArray[1]) / 60;\n        }\n        else {\n            this.rate = this.art.getNumber(\"Rate\", 60 * AnimProps.defaultRate) / 60;\n        }\n        this.start = this.art.getNumber(\"Start\", 0);\n        this.loopStart = this.art.getNumber(\"LoopStart\", 0);\n        this.loopEnd = Math.max(this.loopStart, this.art.getNumber(\"LoopEnd\", this.end + 1) - 1);\n        this.loopCount = this.art.getNumber(\"LoopCount\", 1);\n        const randomLoopDelayArray = this.art.getNumberArray(\"RandomLoopDelay\").sort();\n        this.randomLoopDelay = randomLoopDelayArray.length === 2\n            ? [randomLoopDelayArray[0], randomLoopDelayArray[1]]\n            : undefined;\n    }\n    getArt(): IniSection {\n        return this.art;\n    }\n    setArt(art: IniSection): void {\n        this.art = art;\n        this.init(this.frameCount);\n    }\n}\n"
  },
  {
    "path": "src/engine/Animation.ts",
    "content": "import { AnimProps } from './AnimProps';\nimport { BoxedVar } from '../util/BoxedVar';\nimport { getRandomInt } from '../util/math';\nexport enum AnimationState {\n    NOT_STARTED = 0,\n    RUNNING = 1,\n    STOPPED = 2,\n    DELAYED = 3,\n    PAUSED = 4\n}\nexport class Animation {\n    private state: AnimationState = AnimationState.NOT_STARTED;\n    private frameNo: number = 0;\n    private time: number = 0;\n    private loopNo: number = 0;\n    private delayFrames: number = 0;\n    private endLoopFlag: boolean = false;\n    private playToEndFlag: boolean = false;\n    constructor(public props: AnimProps, public speed: BoxedVar<number>) { }\n    getState(): AnimationState {\n        return this.state;\n    }\n    start(time: number, delayFrames: number = 0): void {\n        this.time = time;\n        this.frameNo = this.props.reverse ? this.props.end : this.props.start;\n        this.loopNo = 0;\n        this.delayFrames = delayFrames;\n        this.state = delayFrames ? AnimationState.DELAYED : AnimationState.RUNNING;\n    }\n    pause(): void {\n        if (this.state === AnimationState.RUNNING) {\n            this.state = AnimationState.PAUSED;\n        }\n    }\n    unpause(): void {\n        if (this.state === AnimationState.PAUSED) {\n            this.state = AnimationState.RUNNING;\n        }\n    }\n    reset(): void {\n        this.state = AnimationState.NOT_STARTED;\n    }\n    stop(): void {\n        this.state = AnimationState.STOPPED;\n    }\n    update(time: number): void {\n        const deltaTime = (time - this.time) / 1000;\n        const rate = this.props.rate * this.speed.value;\n        const framesToAdvance = Math.floor(deltaTime * rate);\n        if (framesToAdvance < 1)\n            return;\n        this.time = time;\n        if (this.state === AnimationState.PAUSED)\n            return;\n        if (this.delayFrames > 0) {\n            this.delayFrames = Math.max(0, this.delayFrames - framesToAdvance);\n            if (this.delayFrames > 0) {\n                this.state = AnimationState.DELAYED;\n                return;\n            }\n            this.state = AnimationState.RUNNING;\n        }\n        if (this.computeNextFrame(framesToAdvance)) {\n            this.state = AnimationState.STOPPED;\n        }\n    }\n    endLoop(): void {\n        this.endLoopFlag = true;\n    }\n    endLoopAndPlayToEnd(): void {\n        this.endLoopFlag = true;\n        this.playToEndFlag = true;\n    }\n    rewind(): void {\n        if (this.props.reverse) {\n            this.frameNo = this.loopNo ? this.props.loopEnd : this.props.end;\n        }\n        else {\n            this.frameNo = this.loopNo ? this.props.loopStart : this.props.start;\n        }\n    }\n    getCurrentFrame(): number {\n        return this.frameNo;\n    }\n    private computeNextFrame(framesToAdvance: number): boolean {\n        let currentFrame = this.frameNo;\n        while (framesToAdvance > 0) {\n            const targetFrame = this.endLoopFlag && this.playToEndFlag\n                ? (this.props.reverse ? this.props.start : this.props.end)\n                : (this.props.reverse ? this.props.loopStart : this.props.loopEnd);\n            if ((!this.props.reverse && currentFrame + framesToAdvance <= targetFrame) ||\n                (this.props.reverse && currentFrame - framesToAdvance >= targetFrame)) {\n                currentFrame += this.props.reverse ? -framesToAdvance : framesToAdvance;\n                break;\n            }\n            if (this.props.loopCount !== -1 && this.loopNo >= this.props.loopCount - 1) {\n                this.frameNo = targetFrame;\n                return true;\n            }\n            if (this.endLoopFlag) {\n                this.frameNo = targetFrame;\n                this.endLoopFlag = false;\n                this.playToEndFlag = false;\n                return true;\n            }\n            framesToAdvance -= 1 + (this.props.reverse ? currentFrame - targetFrame : targetFrame - currentFrame);\n            currentFrame = this.props.reverse ? this.props.loopEnd : this.props.loopStart;\n            this.loopNo++;\n            if (this.props.randomLoopDelay) {\n                this.state = AnimationState.DELAYED;\n                this.delayFrames = getRandomInt(this.props.randomLoopDelay[0], this.props.randomLoopDelay[1]);\n            }\n        }\n        this.frameNo = currentFrame;\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/engine/AsyncResourceCollection.ts",
    "content": "export class AsyncResourceCollection {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/engine/Engine.ts",
    "content": "import { IniFile } from '../data/IniFile';\nimport { ShpFile } from '../data/ShpFile';\nimport { VxlFile } from '../data/VxlFile';\nimport { TmpFile } from '../data/TmpFile';\nimport { Palette } from '../data/Palette';\nimport { Theater } from './Theater';\nimport { TheaterType } from './TheaterType';\nimport { version as appVersion } from '../version';\nimport { VirtualFileSystem } from '../data/vfs/VirtualFileSystem';\nimport { RealFileSystem } from '../data/vfs/RealFileSystem';\nimport { LazyResourceCollection } from './LazyResourceCollection';\nimport { WavFile } from '../data/WavFile';\nimport { LazyAsyncResourceCollection } from './LazyAsyncResourceCollection';\nimport { Mp3File } from '../data/Mp3File';\nimport { mixDatabase } from './mixDatabase';\nimport { GameResSource } from './gameRes/GameResSource';\nimport { Crc32 } from '../data/Crc32';\nimport { GameModes } from '../game/ini/GameModes';\nimport * as stringUtils from '../util/string';\nimport { MapList } from './MapList';\nimport { HvaFile } from '../data/HvaFile';\nimport { MixinRulesType } from '../game/ini/MixinRulesType';\nimport { AppLogger } from '../util/logger';\ntype AppLoggerType = typeof AppLogger;\ninterface TheaterSettings {\n    type: TheaterType;\n    theaterIni: string;\n    mixes: string[];\n    extension: string;\n    newTheaterChar: string;\n    isoPaletteName: string;\n    unitPaletteName: string;\n    overlayPaletteName: string;\n    libPaletteName: string;\n}\ninterface VfsLogger {\n    info(message: string, ...args: unknown[]): void;\n    warn(message: string, ...args: unknown[]): void;\n    error(message: string, ...args: unknown[]): void;\n}\nexport enum EngineType {\n    AutoDetect = 0,\n    TiberianSun = 1,\n    Firestorm = 2,\n    RedAlert2 = 3,\n    YurisRevenge = 4\n}\nexport class Engine {\n    public static readonly UI_ANIM_SPEED = 2;\n    public static rfsSettings = {\n        menuVideoFileName: \"ra2ts_l.webm\",\n        splashImgFileName: \"glsl.png\",\n        mapDir: \"maps\",\n        modDir: \"mods\",\n        musicDir: \"music\",\n        tauntsDir: \"Taunts\",\n        cacheDir: \"cache\",\n        replayDir: \"replays\",\n    };\n    public static supportedMapTypes = [\"mpr\", \"map\"];\n    public static images = new LazyResourceCollection((file) => new ShpFile(file));\n    public static voxels = new LazyResourceCollection((file) => new VxlFile(file));\n    public static voxelAnims = new LazyResourceCollection((file) => new HvaFile(file));\n    public static sounds = new LazyResourceCollection((file) => new WavFile(file));\n    public static themes = new LazyAsyncResourceCollection((file) => new Mp3File(file), false);\n    public static taunts = new LazyAsyncResourceCollection(async (file) => new WavFile(file instanceof File\n        ? new Uint8Array(await file.arrayBuffer())\n        : file.getBytes()));\n    public static iniFiles = new LazyResourceCollection((file) => new IniFile(file));\n    public static tileData = new LazyResourceCollection((file) => new TmpFile(file));\n    public static palettes = new LazyResourceCollection((file) => new Palette(file));\n    public static theaters = new Map<TheaterType, Theater>();\n    public static theaterSettings = new Map<EngineType, TheaterSettings[]>()\n        .set(EngineType.RedAlert2, [\n        {\n            type: TheaterType.Temperate,\n            theaterIni: \"temperat.ini\",\n            mixes: [\"isotemp.mix\", \"temperat.mix\", \"tem.mix\"],\n            extension: \".tem\",\n            newTheaterChar: \"T\",\n            isoPaletteName: \"isotem.pal\",\n            unitPaletteName: \"unittem.pal\",\n            overlayPaletteName: \"temperat.pal\",\n            libPaletteName: \"libtem.pal\",\n        },\n        {\n            type: TheaterType.Snow,\n            theaterIni: \"snow.ini\",\n            mixes: [\"isosnow.mix\", \"snow.mix\", \"sno.mix\"],\n            extension: \".sno\",\n            newTheaterChar: \"A\",\n            isoPaletteName: \"isosno.pal\",\n            unitPaletteName: \"unitsno.pal\",\n            overlayPaletteName: \"snow.pal\",\n            libPaletteName: \"libsno.pal\",\n        },\n        {\n            type: TheaterType.Urban,\n            theaterIni: \"urban.ini\",\n            mixes: [\"isourb.mix\", \"urb.mix\", \"urban.mix\"],\n            extension: \".urb\",\n            newTheaterChar: \"U\",\n            isoPaletteName: \"isourb.pal\",\n            unitPaletteName: \"uniturb.pal\",\n            overlayPaletteName: \"urban.pal\",\n            libPaletteName: \"liburb.pal\",\n        },\n    ])\n        .set(EngineType.YurisRevenge, [\n        {\n            type: TheaterType.Temperate,\n            theaterIni: \"temperatmd.ini\",\n            mixes: [\n                \"isotemp.mix\",\n                \"isotemmd.mix\",\n                \"temperat.mix\",\n                \"tem.mix\",\n            ],\n            extension: \".tem\",\n            newTheaterChar: \"T\",\n            isoPaletteName: \"isotem.pal\",\n            unitPaletteName: \"unittem.pal\",\n            overlayPaletteName: \"temperat.pal\",\n            libPaletteName: \"libtem.pal\",\n        },\n        {\n            type: TheaterType.Snow,\n            theaterIni: \"snowmd.ini\",\n            mixes: [\n                \"isosnomd.mix\",\n                \"snowmd.mix\",\n                \"isosnow.mix\",\n                \"snow.mix\",\n                \"sno.mix\",\n            ],\n            extension: \".sno\",\n            newTheaterChar: \"A\",\n            isoPaletteName: \"isosno.pal\",\n            unitPaletteName: \"unitsno.pal\",\n            overlayPaletteName: \"snow.pal\",\n            libPaletteName: \"libsno.pal\",\n        },\n        {\n            type: TheaterType.Urban,\n            theaterIni: \"urbanmd.ini\",\n            mixes: [\"isourbmd.mix\", \"isourb.mix\", \"urb.mix\", \"urban.mix\"],\n            extension: \".urb\",\n            newTheaterChar: \"U\",\n            isoPaletteName: \"isourb.pal\",\n            unitPaletteName: \"uniturb.pal\",\n            overlayPaletteName: \"urban.pal\",\n            libPaletteName: \"liburb.pal\",\n        },\n        {\n            type: TheaterType.NewUrban,\n            theaterIni: \"urbannmd.ini\",\n            mixes: [\n                \"isoubnmd.mix\",\n                \"isoubn.mix\",\n                \"ubn.mix\",\n                \"urbann.mix\",\n            ],\n            extension: \".ubn\",\n            newTheaterChar: \"N\",\n            isoPaletteName: \"isoubn.pal\",\n            unitPaletteName: \"unitubn.pal\",\n            overlayPaletteName: \"urbann.pal\",\n            libPaletteName: \"libubn.pal\",\n        },\n        {\n            type: TheaterType.Desert,\n            theaterIni: \"desertmd.ini\",\n            mixes: [\n                \"isodesmd.mix\",\n                \"desert.mix\",\n                \"des.mix\",\n                \"isodes.mix\",\n            ],\n            extension: \".des\",\n            newTheaterChar: \"D\",\n            isoPaletteName: \"isodes.pal\",\n            unitPaletteName: \"unitdes.pal\",\n            overlayPaletteName: \"desert.pal\",\n            libPaletteName: \"libdes.pal\",\n        },\n        {\n            type: TheaterType.Lunar,\n            theaterIni: \"lunarmd.ini\",\n            mixes: [\"isolunmd.mix\", \"isolun.mix\", \"lun.mix\", \"lunar.mix\"],\n            extension: \".lun\",\n            newTheaterChar: \"L\",\n            isoPaletteName: \"isolun.pal\",\n            unitPaletteName: \"unitlun.pal\",\n            overlayPaletteName: \"lunar.pal\",\n            libPaletteName: \"liblun.pal\",\n        },\n    ]);\n    public static customRulesFileName = \"rulescd.ini\";\n    public static customArtFileName = \"artcd.ini\";\n    public static customMpModesFileName = \"mpmodescd.ini\";\n    public static shroudFileName = \"shroud.shp\";\n    public static mixinRulesFileNames = new Map<MixinRulesType, string>().set(MixinRulesType.NoDogEngiKills, \"nodogengikills.ini\");\n    private static activeMod?: string;\n    private static modHash?: number;\n    private static gameResSource?: GameResSource;\n    public static rfs?: RealFileSystem;\n    public static vfs?: VirtualFileSystem;\n    public static art?: IniFile;\n    public static rules?: IniFile;\n    public static ai?: IniFile;\n    public static activeTheater?: Theater;\n    private static mapList?: MapList;\n    static getVersion(): string {\n        return appVersion.split(\".\").slice(0, 2).join(\".\");\n    }\n    static getModHash(): number {\n        if (!this.modHash) {\n            throw new Error(\"Rules must be loaded first\");\n        }\n        return this.modHash;\n    }\n    static getActiveMod(): string | undefined {\n        return this.activeMod;\n    }\n    static setActiveMod(modName: string | undefined): void {\n        this.activeMod = modName;\n    }\n    static initGameResSource(source: GameResSource): void {\n        this.gameResSource = source;\n    }\n    static async initRfs(rootHandle: FileSystemDirectoryHandle): Promise<RealFileSystem> {\n        const rfsInstance = (this.rfs = new RealFileSystem());\n        rfsInstance.addRootDirectoryHandle(rootHandle);\n        return rfsInstance;\n    }\n    static async initVfs(rfsInstance: RealFileSystem | undefined, logger: VfsLogger): Promise<VirtualFileSystem> {\n        this.vfs = new VirtualFileSystem(rfsInstance, logger);\n        this.iniFiles.setVfs(this.vfs);\n        this.palettes.setVfs(this.vfs);\n        this.images.setVfs(this.vfs);\n        this.voxels.setVfs(this.vfs);\n        this.voxelAnims.setVfs(this.vfs);\n        this.tileData.setVfs(this.vfs);\n        this.sounds.setVfs(this.vfs);\n        const musicDirPath = Engine.rfsSettings.musicDir;\n        if (Engine.rfs && (await Engine.rfs.containsEntry(musicDirPath))) {\n            const musicDir = await Engine.rfs.getDirectory(musicDirPath);\n            console.log('[Engine] Setting themes directory for music files');\n            try {\n                const handle = musicDir.getNativeHandle();\n                if (handle) {\n                    Engine.themes.setDir(handle);\n                    console.log('[Engine] Themes directory set successfully');\n                }\n                else {\n                    console.warn('[Engine] Failed to get native handle for music directory');\n                }\n            }\n            catch (error) {\n                console.error('[Engine] Failed to set themes directory:', error);\n            }\n        }\n        else {\n            console.warn('[Engine] Music directory not found in RFS');\n        }\n        const tauntsDir = await this.rfs?.findDirectory(this.rfsSettings.tauntsDir);\n        this.taunts.setDir(tauntsDir?.getNativeHandle());\n        return this.vfs;\n    }\n    static supportsTheater(theaterType: TheaterType): boolean {\n        const currentEngine = this.getActiveEngine();\n        return (this.theaterSettings.get(currentEngine)?.some((setting) => setting.type === theaterType) || false);\n    }\n    static getTheaterSettings(engineType: EngineType, theaterType: TheaterType): TheaterSettings {\n        const settingsForEngine = this.theaterSettings.get(engineType);\n        if (!settingsForEngine) {\n            throw new Error(`Unknown engineType \"${EngineType[engineType]}\"`);\n        }\n        const specificSetting = settingsForEngine.find((setting) => setting.type === theaterType);\n        if (!specificSetting) {\n            throw new Error(`Unsupported theater \"${TheaterType[theaterType]}\" for engine \"${EngineType[engineType]}\"`);\n        }\n        return specificSetting;\n    }\n    static async loadTheater(theaterType: TheaterType): Promise<Theater> {\n        if (!this.rules || !this.art) {\n            throw new Error(\"Rules and art should be loaded first\");\n        }\n        if (this.gameResSource === undefined) {\n            throw new Error(\"No gameResSource is set\");\n        }\n        const currentEngine = this.getActiveEngine();\n        let theaterInstance: Theater | undefined;\n        const settings = this.getTheaterSettings(currentEngine, theaterType);\n        if (this.gameResSource !== GameResSource.Cdn && this.vfs) {\n            for (const mixName of settings.mixes) {\n                await this.vfs.addMixFile(mixName);\n            }\n        }\n        if (this.theaters.has(theaterType)) {\n            theaterInstance = this.theaters.get(theaterType)!;\n        }\n        else {\n            const theaterIniFile = this.getTheaterIni(currentEngine, theaterType);\n            const tileDataCollection = this.getTileData();\n            theaterInstance = Theater.factory(theaterType, theaterIniFile, settings, tileDataCollection, this.palettes);\n            this.theaters.set(theaterType, theaterInstance);\n        }\n        this.activeTheater = theaterInstance;\n        return theaterInstance;\n    }\n    static unloadTheater(theaterType: TheaterType): void {\n        if (this.vfs) {\n            const currentEngine = this.getActiveEngine();\n            const settings = this.getTheaterSettings(currentEngine, theaterType);\n            for (const mixName of settings.mixes) {\n                this.vfs.removeArchive(mixName);\n            }\n        }\n    }\n    static unloadSideMixData(): void {\n        for (const mixFileName of [\"sidec01.mix\", \"sidec01cd.mix\"]) {\n            const mixInfo = mixDatabase.get(mixFileName);\n            if (!mixInfo) {\n                console.warn(`Mix \"${mixFileName}\" not found in mix database`);\n                return;\n            }\n            for (const entryName of mixInfo) {\n                const extension = entryName.split('.').pop()?.toLowerCase();\n                (extension === \"pal\" ? this.palettes : this.images).clear(entryName);\n            }\n        }\n    }\n    static getTheaterIni(engineType: EngineType, theaterType: TheaterType): IniFile {\n        const iniFileName = this.getTheaterSettings(engineType, theaterType).theaterIni;\n        return this.getIni(iniFileName);\n    }\n    static loadRules(): void {\n        const rulesFileName = this.getFileNameVariant(\"rules.ini\");\n        const artFileName = this.getFileNameVariant(\"art.ini\");\n        const aiFileName = this.getFileNameVariant(\"ai.ini\");\n        const rulesBase = this.iniFiles.get(rulesFileName);\n        console.log('current rulesBase', rulesBase);\n        const artBase = this.iniFiles.get(artFileName);\n        const aiBase = this.iniFiles.get(aiFileName);\n        if (!rulesBase)\n            throw new Error(`Rules \"${rulesFileName}\" not found`);\n        if (!artBase)\n            throw new Error(`Art \"${artFileName}\" not found`);\n        if (!aiBase)\n            throw new Error(`AI \"${aiFileName}\" not found`);\n        const rulesCustom = this.iniFiles.get(this.customRulesFileName);\n        const artCustom = this.iniFiles.get(this.customArtFileName);\n        if (!rulesCustom)\n            throw new Error(`Rules \"${this.customRulesFileName}\" not found`);\n        if (!artCustom)\n            throw new Error(`Art \"${this.customArtFileName}\" not found`);\n        this.art = artBase.clone().mergeWith(artCustom);\n        this.rules = rulesBase.clone().mergeWith(rulesCustom);\n        console.log('current custom rules', rulesCustom);\n        this.ai = aiBase;\n        this.modHash = this.computeModHash();\n    }\n    static computeModHash(): number {\n        if (!this.vfs)\n            throw new Error(\"VFS not initialized\");\n        const filesToHash: string[] = [\n            this.customRulesFileName,\n            this.customArtFileName,\n            this.customMpModesFileName,\n            this.shroudFileName,\n            this.getFileNameVariant(\"rules.ini\"),\n            this.getFileNameVariant(\"art.ini\"),\n            this.getFileNameVariant(\"ai.ini\"),\n            ...Array.from(this.mixinRulesFileNames.values()),\n        ];\n        const currentEngine = this.getActiveEngine();\n        const theaterSettingsForEngine = this.theaterSettings.get(currentEngine);\n        if (!theaterSettingsForEngine) {\n            throw new Error(`Unsupported engineType \"${EngineType[currentEngine]}\"`);\n        }\n        for (const setting of theaterSettingsForEngine) {\n            filesToHash.push(this.getFileNameVariant(setting.theaterIni));\n        }\n        const mpModes = this.getMpModes();\n        for (const mode of mpModes.getAll()) {\n            if (mode.rulesOverride) {\n                filesToHash.push(mode.rulesOverride);\n            }\n        }\n        const crc = new Crc32();\n        for (const fileName of filesToHash) {\n            if (!this.vfs.fileExists(fileName)) {\n                throw new Error(`File ${fileName} not found for hashing`);\n            }\n            crc.append(this.vfs.openFile(fileName).getBytes());\n        }\n        crc.append(stringUtils.binaryStringToUint8Array(this.getVersion()));\n        return crc.get();\n    }\n    static getRules(): IniFile {\n        if (!this.rules)\n            throw new Error(\"Rules must be loaded first\");\n        console.log('current rules', this.rules);\n        return this.rules;\n    }\n    static getArt(): IniFile {\n        if (!this.art)\n            throw new Error(\"Art must be loaded first\");\n        return this.art;\n    }\n    static getAi(): IniFile {\n        if (!this.ai)\n            throw new Error(\"AI must be loaded first\");\n        return this.ai;\n    }\n    static getFileNameVariant(baseFileName: string): string {\n        const currentEngine = this.getActiveEngine();\n        let suffix = \"\";\n        if (currentEngine === EngineType.YurisRevenge) {\n            suffix = \"md\";\n        }\n        else if (currentEngine !== EngineType.RedAlert2) {\n            throw new Error(\"Unsupported engine type \" + EngineType[currentEngine]);\n        }\n        return suffix ? baseFileName.replace(/\\.([^.]+)$/, `${suffix}.$1`) : baseFileName;\n    }\n    static getMpModes(): GameModes {\n        return new GameModes(this.getIni(this.customMpModesFileName), (fileName: string) => this.getIni(fileName));\n    }\n    static getUiIni(): IniFile {\n        const uiIniFileName = this.getFileNameVariant(\"ui.ini\");\n        return this.getIni(uiIniFileName);\n    }\n    static getIni(fileName: string): IniFile {\n        const iniFile = this.iniFiles.get(fileName);\n        if (!iniFile) {\n            console.warn(`INI file \"${fileName}\" not found, returning empty INI file`);\n            return new IniFile();\n        }\n        return iniFile;\n    }\n    static async loadMapList(): Promise<MapList> {\n        if (!this.vfs)\n            throw new Error(\"File system not initialized\");\n        const gameModes = this.getMpModes();\n        const combinedMapList = new MapList(gameModes);\n        const missionsPktFileName = this.getFileNameVariant(\"missions.pkt\");\n        if (this.iniFiles.has(missionsPktFileName)) {\n            combinedMapList.addFromIni(this.getIni(missionsPktFileName));\n        }\n        else {\n            console.warn(`Map list file \"${missionsPktFileName}\" not found, skipping`);\n        }\n        for (const archiveName of this.vfs.listArchives()) {\n            const pktFileName = archiveName.toLowerCase().replace(/\\.[^.]+$/, \"\") + \".pkt\";\n            if (this.vfs.fileExists(pktFileName)) {\n                combinedMapList.addFromIni(new IniFile(this.vfs.openFile(pktFileName)));\n            }\n        }\n        const localMapList = new MapList(gameModes);\n        if (this.rfs) {\n            const rootDir = this.rfs.getRootDirectory();\n            if (rootDir) {\n                const entries = await rootDir.listEntries();\n                for (const entryName of entries) {\n                    const lowerEntryName = entryName.toLowerCase();\n                    try {\n                        if (lowerEntryName.endsWith(\".pkt\")) {\n                            const fileData = await this.rfs.openFile(entryName, true);\n                            if (fileData) {\n                                localMapList.addFromIni(new IniFile(fileData));\n                            }\n                        }\n                        else if (this.supportedMapTypes.some((type) => lowerEntryName.endsWith(\".\" + type))) {\n                            const fileData = await this.rfs.openFile(entryName, true);\n                            if (fileData) {\n                                localMapList.addFromMapFile(fileData);\n                            }\n                        }\n                    }\n                    catch (e) {\n                        console.warn(`Couldn't read file \"${entryName}\" from RFS`, e);\n                    }\n                }\n            }\n        }\n        localMapList.sortByName();\n        combinedMapList.mergeWith(localMapList);\n        this.mapList = combinedMapList;\n        return combinedMapList;\n    }\n    static getTileData(): LazyResourceCollection<TmpFile> {\n        return this.tileData;\n    }\n    static getImages(): LazyResourceCollection<ShpFile> {\n        return this.images;\n    }\n    static getVoxels(): LazyResourceCollection<VxlFile> {\n        return this.voxels;\n    }\n    static getVoxelAnims(): LazyResourceCollection<HvaFile> {\n        return this.voxelAnims;\n    }\n    static getPalettes(): LazyResourceCollection<Palette> {\n        return this.palettes;\n    }\n    static getSounds(): LazyResourceCollection<WavFile> {\n        return this.sounds;\n    }\n    static getThemes(): LazyAsyncResourceCollection<Mp3File> {\n        return this.themes;\n    }\n    static getTaunts(): LazyAsyncResourceCollection<WavFile> {\n        return this.taunts;\n    }\n    static getActiveEngine(): EngineType {\n        return EngineType.RedAlert2;\n    }\n    static getLastTheaterType(): TheaterType | undefined {\n        return this.activeTheater?.type;\n    }\n    static async getCacheDir(): Promise<FileSystemDirectoryHandle | undefined> {\n        try {\n            return await this.getOrCreateDir(this.rfsSettings.cacheDir, true);\n        }\n        catch (e) {\n            console.error(\"Couldn't get cache directory\", e);\n            return undefined;\n        }\n    }\n    static async getReplayDir(): Promise<FileSystemDirectoryHandle | undefined> {\n        const currentMod = this.getActiveMod();\n        if (currentMod) {\n            const modDirRoot = await this.getModDir();\n            const modSpecificDir = await modDirRoot?.getDirectoryHandle(currentMod, {\n                create: true,\n            });\n            return await modSpecificDir?.getDirectoryHandle(this.rfsSettings.replayDir, { create: true });\n        }\n        return await this.getOrCreateDir(this.rfsSettings.replayDir);\n    }\n    static async getModDir(): Promise<FileSystemDirectoryHandle | undefined> {\n        return await this.getOrCreateDir(this.rfsSettings.modDir);\n    }\n    static async getMapDir(): Promise<FileSystemDirectoryHandle | undefined> {\n        return await this.getOrCreateDir(this.rfsSettings.mapDir);\n    }\n    static async getOrCreateDir(dirName: string, isPrivate: boolean = false): Promise<FileSystemDirectoryHandle | undefined> {\n        const rootDir = this.rfs?.getRootDirectory();\n        if (rootDir) {\n            const nativeRootDirHandle = rootDir.getNativeHandle();\n            if (nativeRootDirHandle) {\n                return await nativeRootDirHandle.getDirectoryHandle(dirName, { create: true });\n            }\n            else {\n                return await rootDir.getOrCreateDirectoryHandle(dirName, isPrivate);\n            }\n        }\n        return undefined;\n    }\n    static getMapList(): MapList | undefined {\n        return this.mapList;\n    }\n    static destroy(): void {\n        this.activeTheater = undefined;\n        this.activeMod = undefined;\n        this.modHash = undefined;\n        this.mapList = undefined;\n        this.rfs = undefined;\n        this.vfs = undefined;\n        this.art = undefined;\n        this.iniFiles.clearAll();\n        this.images.clearAll();\n        this.palettes.clearAll();\n        this.rules = undefined;\n        this.ai = undefined;\n        this.theaters.clear();\n        this.tileData.clearAll();\n        this.voxels.clearAll();\n        this.voxelAnims.clearAll();\n        this.sounds.clearAll();\n        this.themes.clearAll();\n        this.taunts.clearAll();\n    }\n}\n"
  },
  {
    "path": "src/engine/EngineType.ts",
    "content": "export enum EngineType {\n    AutoDetect = 0,\n    TiberianSun = 1,\n    Firestorm = 2,\n    RedAlert2 = 3,\n    YurisRevenge = 4\n}\n"
  },
  {
    "path": "src/engine/GameAnimationLoop.ts",
    "content": "import { IrcConnection } from \"@/network/IrcConnection\";\nimport { recordGamePerformanceFrame } from \"@/performance/PerformanceRuntime\";\ninterface LocalPlayer {\n    isObserver: boolean;\n}\ninterface Renderer {\n    getStats(): {\n        begin(): void;\n        end(): void;\n    } | null;\n    update(timestamp: number, interpolation: number): void;\n    render(): void;\n    flush(): void;\n}\ninterface Sound {\n    audioSystem: {\n        setMuted(muted: boolean): void;\n    };\n}\ninterface GameTurnManager {\n    getTurnMillis(): number;\n    doGameTurn(timestamp: number): boolean;\n    setErrorState(): void;\n    setPassiveMode?(passive: boolean): void;\n}\ninterface GameAnimationLoopOptions {\n    skipFrames?: boolean;\n    skipBudgetMillis?: number;\n    onError?(error: Error, isRenderError?: boolean): void;\n}\nexport class GameAnimationLoop {\n    private localPlayer: LocalPlayer;\n    private renderer: Renderer;\n    private sound: Sound;\n    private gameTurnMgr: GameTurnManager;\n    private options: GameAnimationLoopOptions;\n    private isStarted: boolean = false;\n    private paused: boolean = false;\n    private rendererErrorState: boolean = false;\n    private turnMgrIsWaiting: boolean = false;\n    private startTime: number | undefined;\n    private lastGameFrame: number = 0;\n    private lastGameTurnMillis: number | undefined;\n    private rafId: number | undefined;\n    private backgroundIntervalId: number | undefined;\n    constructor(localPlayer: LocalPlayer, renderer: Renderer, sound: Sound, gameTurnMgr: GameTurnManager, options: GameAnimationLoopOptions = {}) {\n        this.localPlayer = localPlayer;\n        this.renderer = renderer;\n        this.sound = sound;\n        this.gameTurnMgr = gameTurnMgr;\n        this.options = options;\n    }\n    private doBackgroundFrame = (timestamp: number): void => {\n        if (this.isStarted && this.paused) {\n            let deltaFrames = this.updateDeltaGameFrames(timestamp);\n            if (this.turnMgrIsWaiting) {\n                deltaFrames = 1;\n            }\n            while (deltaFrames > 0) {\n                this.turnMgrIsWaiting = !this.tickGame(timestamp);\n                deltaFrames--;\n            }\n        }\n    };\n    private doFrame = (timestamp: number): void => {\n        if (this.isStarted && !this.paused) {\n            recordGamePerformanceFrame(timestamp);\n            let deltaFrames = this.updateDeltaGameFrames(timestamp);\n            if (this.turnMgrIsWaiting || (!this.options.skipFrames && deltaFrames > 1)) {\n                deltaFrames = 1;\n            }\n            const stats = this.renderer.getStats();\n            if (stats) {\n                stats.begin();\n            }\n            if (this.options.skipBudgetMillis) {\n                let budget = this.options.skipBudgetMillis;\n                while (deltaFrames > 0) {\n                    const startTime = performance.now();\n                    this.turnMgrIsWaiting = !this.tickGame(timestamp);\n                    deltaFrames--;\n                    const elapsed = performance.now() - startTime;\n                    budget = Math.max(0, budget - elapsed);\n                    if (budget <= 0) {\n                        break;\n                    }\n                }\n            }\n            else {\n                while (deltaFrames > 0) {\n                    this.turnMgrIsWaiting = !this.tickGame(timestamp);\n                    deltaFrames--;\n                }\n            }\n            const turnMillis = this.gameTurnMgr.getTurnMillis();\n            const interpolation = Math.max(0, (timestamp - (this.startTime! + this.lastGameFrame * turnMillis)) / turnMillis);\n            this.updateRenderer(timestamp, interpolation);\n            if (this.render()) {\n                if (stats) {\n                    stats.end();\n                }\n                this.rafId = requestAnimationFrame(this.doFrame);\n            }\n        }\n    };\n    private handleVisibilityChange = (): void => {\n        const isHidden = document.hidden;\n        if (this.paused !== isHidden) {\n            if (this.localPlayer &&\n                !this.localPlayer.isObserver &&\n                this.paused) {\n                this.doBackgroundFrame(performance.now());\n            }\n            this.paused = isHidden;\n            if (!this.paused) {\n                this.startTime = undefined;\n                this.lastGameFrame = 0;\n            }\n            if (this.localPlayer && !this.localPlayer.isObserver) {\n                try {\n                    this.gameTurnMgr.setPassiveMode?.(this.paused);\n                }\n                catch (error) {\n                    if (!(error instanceof IrcConnection.SocketError)) {\n                        throw error;\n                    }\n                }\n            }\n            if (this.paused) {\n                if (this.rafId) {\n                    cancelAnimationFrame(this.rafId);\n                    this.rafId = undefined;\n                }\n                this.backgroundIntervalId = setInterval(() => {\n                    const timestamp = performance.now();\n                    this.doBackgroundFrame(timestamp);\n                }, 1000);\n            }\n            else {\n                if (this.backgroundIntervalId) {\n                    clearInterval(this.backgroundIntervalId);\n                    this.backgroundIntervalId = undefined;\n                }\n                this.rafId = requestAnimationFrame(this.doFrame);\n            }\n            this.sound.audioSystem.setMuted(this.paused);\n        }\n    };\n    start(): void {\n        if (!this.isStarted) {\n            this.isStarted = true;\n            this.paused = false;\n            this.startTime = undefined;\n            this.lastGameFrame = 0;\n            if (document.hidden) {\n                this.handleVisibilityChange();\n            }\n            else {\n                this.rafId = requestAnimationFrame(this.doFrame);\n            }\n            document.addEventListener(\"visibilitychange\", this.handleVisibilityChange);\n        }\n    }\n    private updateDeltaGameFrames(timestamp: number): number {\n        const turnMillis = this.gameTurnMgr.getTurnMillis();\n        const turnMillisChanged = turnMillis !== this.lastGameTurnMillis;\n        this.lastGameTurnMillis = turnMillis;\n        if (turnMillisChanged) {\n            this.lastGameFrame = 0;\n            this.startTime = timestamp;\n        }\n        let deltaFrames = 0;\n        if (this.startTime) {\n            const elapsed = timestamp - this.startTime;\n            const currentFrame = Math.round(elapsed / turnMillis);\n            deltaFrames = currentFrame - this.lastGameFrame;\n            this.lastGameFrame = currentFrame;\n        }\n        else {\n            this.startTime = timestamp;\n        }\n        return deltaFrames;\n    }\n    private tickGame(timestamp: number): boolean {\n        if (!this.options.onError) {\n            return this.gameTurnMgr.doGameTurn(timestamp);\n        }\n        try {\n            return this.gameTurnMgr.doGameTurn(timestamp);\n        }\n        catch (error) {\n            this.gameTurnMgr.setErrorState();\n            this.options.onError(error as Error);\n            return false;\n        }\n    }\n    private updateRenderer(timestamp: number, interpolation: number): void {\n        if (this.options.onError) {\n            if (!this.rendererErrorState) {\n                try {\n                    this.renderer.update(timestamp, interpolation);\n                }\n                catch (error) {\n                    this.gameTurnMgr.setErrorState();\n                    this.rendererErrorState = true;\n                    this.options.onError(error as Error);\n                    return;\n                }\n            }\n        }\n        else {\n            this.renderer.update(timestamp, interpolation);\n        }\n    }\n    private render(): boolean {\n        if (this.options.onError) {\n            try {\n                this.renderer.render();\n            }\n            catch (error) {\n                this.gameTurnMgr.setErrorState();\n                this.rendererErrorState = true;\n                this.options.onError(error as Error, true);\n                return false;\n            }\n        }\n        else {\n            this.renderer.render();\n        }\n        return true;\n    }\n    stop(): void {\n        if (this.isStarted) {\n            this.isStarted = false;\n            if (this.rafId) {\n                cancelAnimationFrame(this.rafId);\n                this.rafId = undefined;\n            }\n            if (this.backgroundIntervalId) {\n                clearInterval(this.backgroundIntervalId);\n                this.backgroundIntervalId = undefined;\n            }\n            document.removeEventListener(\"visibilitychange\", this.handleVisibilityChange);\n        }\n    }\n    destroy(): void {\n        this.stop();\n        this.renderer.flush();\n    }\n}\n"
  },
  {
    "path": "src/engine/ImageFinder.ts",
    "content": "export class MissingImageError extends Error {\n}\nexport class ImageFinder {\n    static MissingImageError = MissingImageError;\n    private images: Map<string, any>;\n    private theater: any;\n    constructor(images: Map<string, any>, theater: any) {\n        this.images = images;\n        this.theater = theater;\n    }\n    findByObjectArt(objectArt: {\n        imageName: string;\n        useTheaterExtension: boolean;\n    }) {\n        return this.find(objectArt.imageName, objectArt.useTheaterExtension);\n    }\n    find(artName: string, useTheaterExtension: boolean) {\n        const filename = this.getFilename(artName, useTheaterExtension);\n        const image = this.images.get(filename);\n        if (!image) {\n            throw new MissingImageError(`No image file found for artName=\"${artName}\" (file=${filename})`);\n        }\n        return image;\n    }\n    tryFind(artName: string, useTheaterExtension: boolean) {\n        let image;\n        try {\n            image = this.find(artName, useTheaterExtension);\n        }\n        catch (error) {\n            if (!(error instanceof MissingImageError))\n                throw error;\n        }\n        return image;\n    }\n    getFilename(artName: string, useTheaterExtension: boolean) {\n        let filename = artName.toLowerCase();\n        filename += useTheaterExtension ? this.theater.settings.extension : \".shp\";\n        filename = this.applyNewTheaterIfNeeded(artName, filename);\n        return filename;\n    }\n    applyNewTheaterIfNeeded(artName: string, filename: string) {\n        const firstChar = artName[0];\n        const secondChar = artName[1];\n        if ([\"G\", \"N\", \"C\", \"Y\"].indexOf(firstChar) === -1 ||\n            [\"A\", \"T\", \"U\", \"D\", \"L\", \"N\"].indexOf(secondChar) === -1) {\n            return filename;\n        }\n        return this.applyNewTheater(filename);\n    }\n    applyNewTheater(filename: string) {\n        const firstChar = filename[0];\n        const rest = filename.substr(2);\n        const newTheaterChar = this.theater.settings.newTheaterChar.toLowerCase();\n        let newFilename = firstChar + newTheaterChar + rest;\n        if (this.images.has(newFilename)) {\n            return newFilename;\n        }\n        newFilename = firstChar + \"g\" + rest;\n        if (this.images.has(newFilename)) {\n            return newFilename;\n        }\n        return filename;\n    }\n}\n"
  },
  {
    "path": "src/engine/IsoCoords.ts",
    "content": "import { Coords } from '../game/Coords';\ninterface Point {\n    x: number;\n    y: number;\n}\ninterface Point3D extends Point {\n    z: number;\n}\nexport class IsoCoords {\n    private static worldOrigin: Point;\n    static init(origin: Point): void {\n        this.worldOrigin = origin;\n    }\n    static worldToScreen(x: number, y: number): Point {\n        if (!this.worldOrigin) {\n            throw new Error(\"Coords not initialized with world origin\");\n        }\n        x -= this.worldOrigin.x;\n        y -= this.worldOrigin.y;\n        const xScaled = x / Coords.ISO_WORLD_SCALE;\n        const yScaled = y / Coords.ISO_WORLD_SCALE;\n        return {\n            x: xScaled - yScaled,\n            y: (xScaled + yScaled) / 2\n        };\n    }\n    static screenToWorld(x: number, y: number): Point {\n        if (!this.worldOrigin) {\n            throw new Error(\"Coords not initialized with world origin\");\n        }\n        return {\n            x: ((x + 2 * y) / 2) * Coords.ISO_WORLD_SCALE + this.worldOrigin.x,\n            y: ((2 * y - x) / 2) * Coords.ISO_WORLD_SCALE + this.worldOrigin.y\n        };\n    }\n    static vecWorldToScreen(vec: Point3D): Point {\n        const screen = this.worldToScreen(vec.x, vec.z);\n        screen.y -= this.tileHeightToScreen(Coords.worldToTileHeight(vec.y));\n        return screen;\n    }\n    static tileToScreen(tileX: number, tileY: number): Point {\n        const world = Coords.tileToWorld(tileX, tileY);\n        return this.worldToScreen(world.x, world.y);\n    }\n    static tileHeightToScreen(height: number): number {\n        return height * (Coords.ISO_TILE_SIZE / 2);\n    }\n    static tile3dToScreen(tileX: number, tileY: number, height: number): Point {\n        const screen = this.tileToScreen(tileX, tileY);\n        screen.y -= this.tileHeightToScreen(height);\n        return screen;\n    }\n    static screenTileToScreen(tileX: number, tileY: number): Point {\n        return {\n            x: tileX * Coords.ISO_TILE_SIZE,\n            y: (tileY * Coords.ISO_TILE_SIZE) / 2\n        };\n    }\n    static screenToScreenTile(x: number, y: number): Point {\n        return {\n            x: x / Coords.ISO_TILE_SIZE,\n            y: y / (Coords.ISO_TILE_SIZE / 2)\n        };\n    }\n    static screenTileToWorld(tileX: number, tileY: number): Point {\n        const screen = this.screenTileToScreen(tileX, tileY);\n        return this.screenToWorld(screen.x, screen.y);\n    }\n    static getScreenTileSize(): {\n        width: number;\n        height: number;\n    } {\n        return {\n            width: this.tileToScreen(1, 0).x - this.tileToScreen(0, 1).x,\n            height: this.tileToScreen(1, 1).y - this.tileToScreen(0, 0).y\n        };\n    }\n    static screenDistanceToWorld(x: number, y: number): {\n        x: number;\n        y: number;\n    } {\n        return Coords.screenDistanceToWorld(x, y);\n    }\n}\n"
  },
  {
    "path": "src/engine/LazyAsyncResourceCollection.ts",
    "content": "import type { VirtualFile } from '../data/vfs/VirtualFile';\nexport class LazyAsyncResourceCollection<T> {\n    private resourceFactory: (file: VirtualFile | File) => Promise<T> | T;\n    private cacheByDefault: boolean;\n    private resources: Map<string, T> = new Map();\n    private rfsDir?: FileSystemDirectoryHandle;\n    constructor(resourceFactory: (file: VirtualFile | File) => Promise<T> | T, cacheByDefault: boolean = true) {\n        this.resourceFactory = resourceFactory;\n        this.cacheByDefault = cacheByDefault;\n    }\n    setDir(rfsDir: FileSystemDirectoryHandle | undefined): void {\n        this.rfsDir = rfsDir;\n    }\n    set(key: string, resource: T): void {\n        this.resources.set(key, resource);\n    }\n    async has(key: string): Promise<boolean> {\n        if (this.resources.has(key)) {\n            return true;\n        }\n        try {\n            return !!(await this.rfsDir?.getFileHandle(key));\n        }\n        catch (e) {\n            return false;\n        }\n    }\n    async get(key: string): Promise<T | undefined> {\n        let resource = this.resources.get(key);\n        if (!resource && this.rfsDir) {\n            try {\n                const fileHandle = await this.rfsDir.getFileHandle(key);\n                const file = await fileHandle.getFile();\n                resource = await this.resourceFactory(file);\n                if (this.cacheByDefault) {\n                    this.resources.set(key, resource!);\n                }\n            }\n            catch (e) {\n                return undefined;\n            }\n        }\n        return resource;\n    }\n    clear(key?: string): void {\n        if (key) {\n            this.resources.delete(key);\n        }\n        else {\n            this.resources.clear();\n        }\n    }\n    clearAll(): void {\n        this.resources.clear();\n    }\n}\n"
  },
  {
    "path": "src/engine/LazyResourceCollection.ts",
    "content": "import type { VirtualFileSystem } from '../data/vfs/VirtualFileSystem';\nimport type { VirtualFile } from '../data/vfs/VirtualFile';\nexport class LazyResourceCollection<T> {\n    private resourceFactory: (file: VirtualFile) => T;\n    private resources: Map<string, T> = new Map();\n    private vfs?: VirtualFileSystem;\n    constructor(resourceFactory: (file: VirtualFile) => T) {\n        this.resourceFactory = resourceFactory;\n    }\n    setVfs(vfs: VirtualFileSystem): void {\n        this.vfs = vfs;\n    }\n    set(key: string, resource: T): void {\n        this.resources.set(key, resource);\n    }\n    has(key: string): boolean {\n        const inMem = this.resources.has(key);\n        const inVfs = this.vfs?.fileExists(key) ?? false;\n        if (!inMem) {\n            try {\n            }\n            catch { }\n        }\n        return !!inMem || inVfs;\n    }\n    get(key: string): T | undefined {\n        let resource = this.resources.get(key);\n        if (!resource) {\n            try {\n            }\n            catch { }\n            if (this.vfs?.fileExists(key)) {\n                try {\n                    const owners = (this.vfs as any).debugListFileOwners?.(key);\n                    try {\n                    }\n                    catch { }\n                }\n                catch { }\n                const file = this.vfs.openFile(key);\n                if (file) {\n                    resource = this.resourceFactory(file);\n                    this.resources.set(key, resource!);\n                    try {\n                    }\n                    catch { }\n                }\n            }\n            else {\n                try {\n                    console.warn('[LazyResourceCollection.get] not found in VFS', { key, archives: this.vfs?.listArchives?.() });\n                }\n                catch { }\n            }\n        }\n        return resource;\n    }\n    clear(key?: string): void {\n        if (key) {\n            this.resources.delete(key);\n        }\n        else {\n            this.resources.clear();\n        }\n    }\n    clearAll(): void {\n        this.resources.clear();\n    }\n}\n"
  },
  {
    "path": "src/engine/Lighting.ts",
    "content": "import { LightingType } from './type/LightingType';\nimport { MapLighting } from '../data/map/MapLighting';\nimport { EventDispatcher } from '../util/event';\nimport { CompositeDisposable } from '../util/disposable/CompositeDisposable';\nimport * as THREE from 'three';\nexport class Lighting {\n    private baseAmbient: MapLighting;\n    private tileLights: Map<string, Set<any>>;\n    private disposables: CompositeDisposable;\n    private _onChange: EventDispatcher;\n    private ambientOverride?: MapLighting;\n    constructor(parent?: Lighting) {\n        this.baseAmbient = new MapLighting();\n        this.tileLights = new Map();\n        this.disposables = new CompositeDisposable();\n        this._onChange = new EventDispatcher();\n        if (parent) {\n            this.baseAmbient.copy(parent.getAmbient());\n            const handler = (ambient: MapLighting) => {\n                this.baseAmbient.copy(ambient);\n                this._onChange.dispatch(this, undefined);\n            };\n            parent.onChange.subscribe(handler);\n            this.disposables.add(() => parent.onChange.unsubscribe(handler));\n        }\n    }\n    get onChange() {\n        return this._onChange.asEvent();\n    }\n    get mapLighting() {\n        return this.ambientOverride ?? this.baseAmbient;\n    }\n    getAmbient(): MapLighting {\n        return this.mapLighting;\n    }\n    forceUpdate(force?: any) {\n        this._onChange.dispatch(this, force);\n    }\n    applyAmbientOverride(override: MapLighting) {\n        this.ambientOverride = override;\n        this._onChange.dispatch(this, undefined);\n    }\n    getBaseAmbient() {\n        return new MapLighting().copy(this.baseAmbient);\n    }\n    addTileLight(tile: string, light: any) {\n        if (!this.tileLights.has(tile)) {\n            this.tileLights.set(tile, new Set());\n        }\n        this.tileLights.get(tile)!.add(light);\n    }\n    removeTileLight(tile: string, light: any) {\n        const lights = this.tileLights.get(tile);\n        if (lights) {\n            lights.delete(light);\n            if (!lights.size) {\n                this.tileLights.delete(tile);\n            }\n        }\n    }\n    compute(type: LightingType, tile: any, height: number = 0): THREE.Vector3 {\n        if (type === LightingType.None) {\n            return new THREE.Vector3(1, 1, 1);\n        }\n        return this.computeTint(type)\n            .add(this.computeTileTint(tile, type, new THREE.Vector3()))\n            .multiplyScalar(this.mapLighting.ambient +\n            this.mapLighting.ground +\n            this.computeLevel(type, tile.z + height) +\n            this.computeTileLightIntensity(tile));\n    }\n    computeNoAmbient(type: LightingType, tile: any, height: number = 0): number {\n        return this.computeLevel(type, tile.z + height) + this.computeTileLightIntensity(tile);\n    }\n    computeLevel(type: LightingType, height: number): number {\n        return type >= LightingType.Level ? this.mapLighting.level * (height - 1) : 0;\n    }\n    computeTint(type: LightingType): THREE.Vector3 {\n        let red = 1, green = 1, blue = 1;\n        if (type >= LightingType.Full || this.mapLighting.forceTint) {\n            red = this.mapLighting.red;\n            green = this.mapLighting.green;\n            blue = this.mapLighting.blue;\n        }\n        return new THREE.Vector3(red, green, blue);\n    }\n    computeTileTint(tile: string, type: LightingType, result: THREE.Vector3 = new THREE.Vector3()): THREE.Vector3 {\n        let red = 0, green = 0, blue = 0;\n        if (type >= LightingType.Full) {\n            const lights = this.tileLights.get(tile);\n            if (lights?.size) {\n                for (const light of lights) {\n                    red += light.red;\n                    green += light.green;\n                    blue += light.blue;\n                }\n            }\n        }\n        return result.set(red, green, blue);\n    }\n    computeTileLightIntensity(tile: string): number {\n        let intensity = 0;\n        const lights = this.tileLights.get(tile);\n        if (lights?.size) {\n            for (const light of lights) {\n                intensity += light.intensity;\n            }\n        }\n        return intensity;\n    }\n    getAmbientIntensity(): number {\n        return this.mapLighting.ambient + this.mapLighting.ground;\n    }\n    dispose() {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/MapDigest.ts",
    "content": "import { Crc32 } from \"../data/Crc32\";\nexport class MapDigest {\n    static compute(data: {\n        getBytes(): Uint8Array;\n    }): string {\n        return Crc32.calculateCrc(data.getBytes()).toString(16);\n    }\n}\n"
  },
  {
    "path": "src/engine/MapList.ts",
    "content": "import { MapManifest } from './MapManifest';\nimport type { GameModes, GameModeEntry } from '../game/ini/GameModes';\nimport type { IniFile, IniSection } from '../data/IniFile';\nimport type { VirtualFile } from '../data/vfs/VirtualFile';\nexport class MapList {\n    private gameModes: GameModes;\n    private manifests: MapManifest[] = [];\n    constructor(gameModes: GameModes) {\n        this.gameModes = gameModes;\n    }\n    addFromIni(iniFile: IniFile): this {\n        const multiMapsSection = iniFile.getSection(\"MultiMaps\");\n        if (!multiMapsSection) {\n            throw new Error(\"Invalid map list. Missing [MultiMaps] section.\");\n        }\n        const newManifests = Array.from(multiMapsSection.entries.values()).map((rawSectionKey) => {\n            const sectionKey = Array.isArray(rawSectionKey)\n                ? rawSectionKey[0]\n                : rawSectionKey;\n            const mapSection = iniFile.getSection(sectionKey);\n            if (!mapSection) {\n                throw new Error(`Invalid map list. Missing [${sectionKey}] section.`);\n            }\n            return new MapManifest().fromIni(mapSection, this.gameModes.getAll());\n        });\n        this.manifests = this.manifests.concat(newManifests);\n        this.dedupeEntries();\n        return this;\n    }\n    add(manifest: MapManifest): void {\n        this.manifests.push(manifest);\n    }\n    addFromMapFile(mapFile: VirtualFile): void {\n        this.add(new MapManifest().fromMapFile(mapFile, this.gameModes.getAll()));\n    }\n    getAll(): MapManifest[] {\n        return this.manifests;\n    }\n    getByName(fileName: string): MapManifest | undefined {\n        return this.manifests.find((manifest) => manifest.fileName.toLowerCase() === fileName.toLowerCase());\n    }\n    sortByName(): void {\n        this.manifests.sort((a, b) => a.fileName.localeCompare(b.fileName));\n    }\n    clone(): MapList {\n        const newList = new MapList(this.gameModes);\n        newList.manifests = [...this.manifests];\n        return newList;\n    }\n    mergeWith(otherList: MapList): this {\n        this.manifests.push(...otherList.manifests);\n        this.dedupeEntries();\n        return this;\n    }\n    private dedupeEntries(): void {\n        this.manifests = [\n            ...new Map(this.manifests.map((manifest) => [manifest.fileName.toLowerCase(), manifest])).values(),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/engine/MapManifest.ts",
    "content": "import { IniFile, IniSection } from '../data/IniFile';\nimport type { GameModeEntry } from '../game/ini/GameModes';\nimport type { VirtualFile } from '../data/vfs/VirtualFile';\nimport type { Strings } from '../data/Strings';\nexport class MapManifest {\n    public fileName!: string;\n    public uiName!: string;\n    public maxSlots!: number;\n    public official!: boolean;\n    public gameModes!: GameModeEntry[];\n    fromIni(section: IniSection, availableGameModes: GameModeEntry[]): this {\n        this.fileName = section.getString(\"File\") || section.name.toLowerCase() + \".map\";\n        this.uiName = section.getString(\"Description\");\n        this.maxSlots = section.getNumber(\"MaxPlayers\");\n        this.official = true;\n        const supportedModeFilters = section.getArray(\"GameMode\");\n        this.gameModes = availableGameModes.filter((gm) => supportedModeFilters.includes(gm.mapFilter));\n        return this;\n    }\n    getFullMapTitle(strings: Strings): string {\n        const mapTitle = strings.get(this.uiName);\n        return this.addTitleSlotsSuffix(mapTitle, this.maxSlots);\n    }\n    private addTitleSlotsSuffix(title: string, maxPlayers: number): string {\n        if (!title.match(/(\\s*\\(|（)\\s*\\d(-\\d)?\\s*(\\)|）)\\s*$/)) {\n            title += ` (2${maxPlayers > 2 ? \"-\" + maxPlayers : \"\"})`;\n        }\n        return title;\n    }\n    fromMapFile(mapFile: VirtualFile, availableGameModes: GameModeEntry[]): this {\n        const mapContent = mapFile.readAsString();\n        const mapFileName = mapFile.filename;\n        const basicSectionContent = this.extractIniSection(\"Basic\", mapContent);\n        if (!basicSectionContent) {\n            throw new Error(`Map \"${mapFileName}\" is missing the [Basic] section content`);\n        }\n        const basicIniFile = new IniFile(basicSectionContent);\n        const basicSection = basicIniFile.getSection(\"Basic\");\n        if (!basicSection) {\n            throw new Error(`Map \"${mapFileName}\" is missing the [Basic] section after parsing`);\n        }\n        this.fileName = mapFileName;\n        this.uiName = \"NOSTR:\" + (basicSection.getString(\"Name\") || mapFileName.replace(/\\.[^.]+$/, \"\"));\n        const waypointsSectionContent = this.extractIniSection(\"Waypoints\", mapContent);\n        let maxPlayersFromWaypoints = 0;\n        if (waypointsSectionContent) {\n            const waypointsIniFile = new IniFile(waypointsSectionContent);\n            const waypointsSection = waypointsIniFile.getSection(\"Waypoints\");\n            if (waypointsSection) {\n                maxPlayersFromWaypoints = Array.from(waypointsSection.entries.keys()).filter((key) => Number(key) < 8).length;\n            }\n        }\n        this.maxSlots = maxPlayersFromWaypoints;\n        this.official = basicSection.getBool(\"Official\");\n        const supportedModeFilters = basicSection.getArray(\"GameMode\", /,\\s*/, [\"standard\"]);\n        this.gameModes = availableGameModes.filter((gm) => supportedModeFilters.includes(gm.mapFilter));\n        return this;\n    }\n    private extractIniSection(sectionName: string, content: string): string | undefined {\n        const sectionStartTag = `[${sectionName}]`;\n        const startIndex = content.indexOf(sectionStartTag);\n        if (startIndex !== -1) {\n            let endIndex = content.length;\n            let nextSectionIndex = startIndex + sectionStartTag.length;\n            while (nextSectionIndex < content.length) {\n                const nlIndex = content.indexOf('\\n', nextSectionIndex);\n                if (nlIndex === -1) {\n                    nextSectionIndex = content.length;\n                    break;\n                }\n                let line = content.substring(nextSectionIndex, nlIndex).trim();\n                if (line.startsWith('[') && line.endsWith(']')) {\n                    endIndex = nextSectionIndex;\n                    break;\n                }\n                nextSectionIndex = nlIndex + 1;\n                if (!line) {\n                    continue;\n                }\n                const potentialNextSectionStart = content.indexOf('\\n[', startIndex + sectionStartTag.length);\n                if (potentialNextSectionStart !== -1) {\n                    endIndex = potentialNextSectionStart + 1;\n                }\n                else {\n                    endIndex = content.length;\n                }\n                break;\n            }\n            let currentSearchIndex = startIndex + sectionStartTag.length;\n            let nextSectionFoundIndex = -1;\n            while (currentSearchIndex < content.length) {\n                let nlIndex = content.indexOf('\\n', currentSearchIndex);\n                if (nlIndex === -1)\n                    break;\n                if (content.charAt(nlIndex + 1) === '[') {\n                    nextSectionFoundIndex = nlIndex + 1;\n                    break;\n                }\n                currentSearchIndex = nlIndex + 1;\n            }\n            endIndex = nextSectionFoundIndex !== -1 ? nextSectionFoundIndex : content.length;\n            return content.slice(startIndex, endIndex).trim();\n        }\n        return undefined;\n    }\n}\n"
  },
  {
    "path": "src/engine/MapSupport.ts",
    "content": "import { MapFile } from \"@/data/MapFile\";\nimport { Strings } from \"@/data/Strings\";\nimport { Rules } from \"@/game/rules/Rules\";\nimport { Engine } from \"@/engine/Engine\";\nimport { TileSets } from \"@/game/theater/TileSets\";\nimport { TheaterType } from \"@/engine/TheaterType\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\ninterface BuildingRule {\n    undeploysInto?: string;\n}\ninterface TechnoRule {\n    spawns?: string;\n    deploysInto?: string;\n}\nexport class MapSupport {\n    static check(map: MapFile, translator: Strings): string | undefined {\n        if (map.iniFormat < 4) {\n            return translator.get(\"TS:MapUnsupportedGame\");\n        }\n        if (map.startingLocations.length < 2) {\n            return translator.get(\"TXT_SCENARIO_TOO_SMALL\", map.startingLocations.length);\n        }\n        if (!Engine.supportsTheater(map.theaterType)) {\n            return translator.get(\"TS:MapUnsupportedTheater\", TheaterType[map.theaterType]);\n        }\n        const theaterIni = Engine.getTheaterIni(Engine.getActiveEngine(), map.theaterType);\n        const tileSets = new TileSets(theaterIni);\n        if (map.maxTileNum > tileSets.readMaxTileNum()) {\n            return translator.get(\"TS:MapUnsupportedTileSet\");\n        }\n        const rules = new Rules(Engine.getRules().clone().mergeWith(map));\n        if (!rules.hasOverlayId(map.maxOverlayId)) {\n            return translator.get(\"TS:MapUnsupportedOverlay\", map.maxOverlayId);\n        }\n        for (const weaponType of rules.weaponTypes.values()) {\n            if (!rules.getIni().getSection(weaponType)) {\n                return translator.get(\"TS:MapUnsupportedWeapon\", weaponType);\n            }\n            const weaponData = rules.getWeapon(weaponType);\n            const projectile = weaponData.projectile;\n            const warhead = weaponData.warhead;\n            if (!projectile || !warhead) {\n                return translator.get(\"TS:MapUnsupportedWeapon\", weaponType);\n            }\n            if (!rules.getIni().getSection(projectile)) {\n                return translator.get(\"TS:MapUnsupportedProjectile\", projectile);\n            }\n            if (!rules.warheadRules.has(warhead.toLowerCase()) &&\n                !rules.getIni().getSection(warhead)) {\n                return translator.get(\"TS:MapUnsupportedWarhead\", warhead);\n            }\n        }\n        const general = rules.general;\n        for (const unit of [...general.baseUnit, ...general.harvesterUnit]) {\n            if (unit && !rules.hasObject(unit, ObjectType.Vehicle)) {\n                return translator.get(\"TS:MapUnsupportedTechno\", unit);\n            }\n        }\n        for (const disguise of general.defaultMirageDisguises) {\n            if (disguise && !rules.terrainRules.has(disguise)) {\n                return translator.get(\"TS:MapUnsupportedTerrain\", disguise);\n            }\n        }\n        const crewAndDisguiseUnits = [\n            general.engineer,\n            general.crew.alliedCrew,\n            general.crew.sovietCrew,\n            general.alliedDisguise,\n            general.sovietDisguise,\n        ];\n        for (const unit of crewAndDisguiseUnits) {\n            if (unit && !rules.infantryRules.has(unit)) {\n                return translator.get(\"TS:MapUnsupportedTechno\", unit);\n            }\n        }\n        const crateRules = rules.crateRules;\n        for (const crateImg of [crateRules.crateImg, crateRules.waterCrateImg]) {\n            if (crateImg && !rules.overlayRules.has(crateImg)) {\n                return translator.get(\"TS:MapUnsupportedOverlay\", crateImg);\n            }\n        }\n        for (const building of rules.buildingRules.values() as IterableIterator<BuildingRule>) {\n            if (building.undeploysInto &&\n                !rules.hasObject(building.undeploysInto, ObjectType.Vehicle)) {\n                return translator.get(\"TS:MapUnsupportedTechno\", building.undeploysInto);\n            }\n        }\n        const allTechnoRules = [\n            ...rules.infantryRules.values(),\n            ...rules.vehicleRules.values(),\n            ...rules.aircraftRules.values(),\n        ] as TechnoRule[];\n        for (const techno of allTechnoRules) {\n            if (techno.spawns && !rules.hasObject(techno.spawns, ObjectType.Aircraft)) {\n                return translator.get(\"TS:MapUnsupportedTechno\", techno.spawns);\n            }\n            if (techno.deploysInto &&\n                !rules.hasObject(techno.deploysInto, ObjectType.Building)) {\n                return translator.get(\"TS:MapUnsupportedTechno\", techno.deploysInto);\n            }\n        }\n        return undefined;\n    }\n}\n"
  },
  {
    "path": "src/engine/RenderableManager.ts",
    "content": "import { OctreeContainer } from '@/engine/gfx/OctreeContainer';\nimport { World } from '@/game/World';\nimport { WorldScene } from '@/engine/renderable/WorldScene';\nimport { Camera } from '@/engine/gfx/Camera';\nimport { RenderableFactory } from '@/engine/renderable/entity/RenderableFactory';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { Renderable } from '@/engine/renderable/Renderable';\nexport class RenderableManager {\n    private world: World;\n    private worldScene: WorldScene;\n    private camera: Camera;\n    private renderableFactory: RenderableFactory;\n    private container: OctreeContainer;\n    private renderablesByGameObject: Map<GameObject, Renderable>;\n    private renderablesById: Map<string, Renderable>;\n    private positionListeners: Map<GameObject, Function>;\n    private onCameraUpdate: () => void;\n    private onWorldObjectSpawned: (gameObject: GameObject) => void;\n    private onWorldObjectRemoved: (gameObject: GameObject) => void;\n    constructor(world: World, worldScene: WorldScene, camera: Camera, renderableFactory: RenderableFactory) {\n        this.world = world;\n        this.worldScene = worldScene;\n        this.camera = camera;\n        this.renderableFactory = renderableFactory;\n        this.renderablesByGameObject = new Map();\n        this.renderablesById = new Map();\n        this.positionListeners = new Map();\n        this.onCameraUpdate = () => {\n            this.container.cullChildren();\n        };\n        this.onWorldObjectSpawned = (gameObject: GameObject) => {\n            const isLightpost = gameObject.isTechno() && gameObject.rules.isLightpost;\n            const renderable = this.createRenderable(gameObject, isLightpost ? this.worldScene : this.container);\n            if (renderable.onCreate) {\n                renderable.onCreate(this);\n            }\n            const positionListener = ({ tileChanged }) => this.onObjectPositionChanged(gameObject, tileChanged);\n            this.positionListeners.set(gameObject, positionListener);\n            gameObject.position.onPositionChange.subscribe(positionListener);\n        };\n        this.onWorldObjectRemoved = (gameObject: GameObject) => {\n            gameObject.position.onPositionChange.unsubscribe(this.positionListeners.get(gameObject));\n            this.positionListeners.delete(gameObject);\n            const renderable = this.renderablesByGameObject.get(gameObject);\n            if (!renderable) {\n                return;\n            }\n            if (renderable.onRemove) {\n                const result = renderable.onRemove(this);\n                if (result) {\n                    result\n                        .then(() => this.removeAndDisposeRenderable(renderable, gameObject))\n                        .catch(error => console.error(error));\n                }\n                else {\n                    this.removeAndDisposeRenderable(renderable, gameObject);\n                }\n            }\n            else {\n                this.removeAndDisposeRenderable(renderable, gameObject);\n            }\n        };\n    }\n    init(): void {\n        this.container = OctreeContainer.factory(this.camera as any);\n        this.container.autoCull = false;\n        this.worldScene.add(this.container);\n        this.worldScene.onCameraUpdate.subscribe(this.onCameraUpdate);\n        this.world.getAllObjects().forEach(gameObject => this.onWorldObjectSpawned(gameObject));\n        this.world.onObjectSpawned.subscribe(this.onWorldObjectSpawned);\n        this.world.onObjectRemoved.subscribe(this.onWorldObjectRemoved);\n    }\n    getRenderableById(id: string): Renderable {\n        return this.renderablesById.get(id);\n    }\n    getRenderableByGameObject(gameObject: GameObject): Renderable {\n        return this.renderablesByGameObject.get(gameObject);\n    }\n    getRenderableContainer(): OctreeContainer {\n        return this.container;\n    }\n    onObjectPositionChanged(gameObject: GameObject, tileChanged: boolean): void {\n        const renderable = this.renderablesByGameObject.get(gameObject);\n        renderable.setPosition(gameObject.position.worldPosition);\n        if (!(gameObject.isTechno() && gameObject.rules.isLightpost)) {\n            this.container.updateChild(renderable as any as import('./gfx/RenderableContainer').Renderable);\n        }\n    }\n    removeAndDisposeRenderable(renderable: Renderable, gameObject: GameObject): void {\n        const container = gameObject.isTechno() && gameObject.rules.isLightpost\n            ? this.worldScene\n            : this.container;\n        container.remove(renderable as any);\n        renderable.dispose?.();\n        this.renderablesByGameObject.delete(gameObject);\n        this.renderablesById.delete(gameObject.id as any);\n    }\n    createTransientAnim(anim: any, callback?: (renderable: Renderable) => void): Renderable {\n        const renderable = this.renderableFactory.createTransientAnim(anim, this.container);\n        if (callback) {\n            callback(renderable);\n        }\n        this.container.add(renderable);\n        return renderable;\n    }\n    createAnim(anim: any, callback?: (renderable: Renderable) => void, skipAdd: boolean = false): Renderable {\n        const renderable = this.renderableFactory.createAnim(anim);\n        if (callback) {\n            callback(renderable);\n        }\n        if (!skipAdd) {\n            this.container.add(renderable);\n        }\n        return renderable;\n    }\n    addEffect(effect: any): void {\n        effect.setContainer(this.worldScene);\n        this.worldScene.add(effect);\n    }\n    dispose(): void {\n        this.worldScene.remove(this.container);\n        this.container = undefined;\n        this.worldScene.onCameraUpdate.unsubscribe(this.onCameraUpdate);\n        this.world.onObjectSpawned.unsubscribe(this.onWorldObjectSpawned);\n        this.world.onObjectRemoved.unsubscribe(this.onWorldObjectRemoved);\n        this.onWorldObjectRemoved = undefined;\n        this.onWorldObjectSpawned = undefined;\n        this.positionListeners.forEach((listener, gameObject) => {\n            gameObject.position.onPositionChange.unsubscribe(listener);\n        });\n        this.positionListeners.clear();\n        this.renderablesById.forEach(renderable => renderable.dispose?.());\n    }\n    createRenderable(gameObject: GameObject, container: any): Renderable {\n        const renderable = this.renderableFactory.create(gameObject as any);\n        (renderable as any).setPosition(gameObject.position.worldPosition);\n        container.add(renderable);\n        this.renderablesByGameObject.set(gameObject, renderable);\n        this.renderablesById.set(gameObject.id as any, renderable);\n        return renderable;\n    }\n    updateLighting(): void {\n        for (const renderable of this.renderablesById.values()) {\n            renderable.updateLighting();\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/ResourceCollection.ts",
    "content": "export class ResourceCollection {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/engine/ResourceLoader.ts",
    "content": "import { OperationCanceledError, type CancellationToken } from '@puzzl/core/lib/async/cancellation';\nimport { HttpRequest, DownloadError } from '../network/HttpRequest';\nimport { resourceConfigs, ResourceType, type ResourceConfig, type ResourceId } from './resourceConfigs';\ninterface FetchResourceOptions {\n    onProgress?: (loadedBytes: number) => void;\n}\ninterface LoadResourceItem {\n    id?: ResourceId;\n    src: string;\n    type: 'text' | 'binary' | 'json';\n    sizeHint?: number;\n}\nexport class LoaderResult {\n    private items: Map<ResourceId, ArrayBuffer | string | any>;\n    constructor(items: Map<ResourceId, ArrayBuffer | string | any>) {\n        this.items = items;\n    }\n    pop(resourceIdentifier: ResourceType | ResourceId): ArrayBuffer | string | any {\n        let resourceId: ResourceId;\n        if (typeof resourceIdentifier === 'string') {\n            resourceId = resourceIdentifier;\n        }\n        else {\n            const config = resourceConfigs.get(resourceIdentifier as ResourceType);\n            if (!config) {\n                throw new Error(`Missing resourceConfig for resource type \"${ResourceType[resourceIdentifier as ResourceType]}\"`);\n            }\n            if (!config.id) {\n                throw new Error(`Undefined resourceId for resourceType ${ResourceType[resourceIdentifier as ResourceType]}`);\n            }\n            resourceId = config.id;\n        }\n        const item = this.items.get(resourceId);\n        if (item === undefined) {\n            throw new Error(`Resource \"${resourceId}\" (from ${typeof resourceIdentifier === 'string' ? resourceIdentifier : ResourceType[resourceIdentifier as ResourceType]}) not found in result.`);\n        }\n        this.items.delete(resourceId);\n        return item;\n    }\n}\nexport class ResourceLoader {\n    private resourceBaseUrl: string;\n    private httpRequest: HttpRequest;\n    constructor(resourceBaseUrl: string) {\n        this.resourceBaseUrl = resourceBaseUrl.endsWith('/') ? resourceBaseUrl : resourceBaseUrl + '/';\n        this.httpRequest = new HttpRequest();\n    }\n    async prefetchResource(resourceType: ResourceType, cancellationToken?: CancellationToken): Promise<void> {\n        const resourceConfig = resourceConfigs.get(resourceType);\n        if (!resourceConfig) {\n            throw new Error(`Missing resourceConfig for resType ${ResourceType[resourceType]}`);\n        }\n        const url = this.resourceBaseUrl + resourceConfig.src;\n        const link = document.createElement(\"link\");\n        link.rel = \"prefetch\";\n        link.as = \"fetch\";\n        link.href = url;\n        link.crossOrigin = \"anonymous\";\n        return new Promise<void>((resolve, reject) => {\n            const cleanupAndReject = (error: Error) => {\n                if (link.parentNode) {\n                    document.head.removeChild(link);\n                }\n                reject(error);\n            };\n            const cleanupAndResolve = () => {\n                if (link.parentNode) {\n                    document.head.removeChild(link);\n                }\n                resolve();\n            };\n            cancellationToken?.register(() => {\n                cleanupAndReject(new OperationCanceledError(cancellationToken));\n            });\n            if (\"onload\" in link) {\n                link.onload = cleanupAndResolve;\n            }\n            else {\n                document.head.appendChild(link);\n                cleanupAndResolve();\n                return;\n            }\n            if (\"onerror\" in link) {\n                link.onerror = () => cleanupAndReject(new Error(`Couldn't prefetch URL \"${url}\"`));\n            }\n            else {\n            }\n            document.head.appendChild(link);\n        });\n    }\n    getResourceUrl(resourceTypeOrConfig: ResourceType | ResourceConfig): string {\n        const config = typeof resourceTypeOrConfig === 'object' ? resourceTypeOrConfig : resourceConfigs.get(resourceTypeOrConfig);\n        if (!config) {\n            throw new Error(`Missing resourceConfig for resType ${ResourceType[resourceTypeOrConfig as ResourceType]}`);\n        }\n        return this.resourceBaseUrl + config.src;\n    }\n    getResourceFileName(resourceType: ResourceType): string {\n        const url = this.getResourceUrl(resourceType);\n        const pathPart = url.split(\"?\")[0];\n        return pathPart.substring(pathPart.lastIndexOf('/') + 1);\n    }\n    buildResourceManifest(resources: (ResourceType | ResourceConfig)[]): LoadResourceItem[] {\n        return resources\n            .map((res): ResourceConfig => {\n            if (typeof res === 'object')\n                return res as ResourceConfig;\n            const config = resourceConfigs.get(res as ResourceType);\n            if (!config) {\n                throw new Error(`Missing resourceConfig for resType ${ResourceType[res as ResourceType]}`);\n            }\n            return config;\n        })\n            .map((config: ResourceConfig): LoadResourceItem => ({\n            id: config.id,\n            src: config.src.match(/^https?:\\/\\//)\n                ? config.src\n                : this.resourceBaseUrl + config.src,\n            type: config.type as 'text' | 'binary' | 'json',\n            sizeHint: config.sizeHint,\n        }));\n    }\n    async loadText(srcRelative: string, cancellationToken?: CancellationToken, options?: FetchResourceOptions): Promise<string> {\n        return await this.loadResource({ src: srcRelative, type: \"text\" }, cancellationToken, options) as string;\n    }\n    async loadBinary(srcRelative: string, cancellationToken?: CancellationToken, options?: FetchResourceOptions): Promise<ArrayBuffer> {\n        return await this.loadResource({ src: srcRelative, type: \"binary\" }, cancellationToken, options) as ArrayBuffer;\n    }\n    async loadJson(srcRelative: string, cancellationToken?: CancellationToken, options?: FetchResourceOptions): Promise<any> {\n        return await this.loadResource({ src: srcRelative, type: \"json\" }, cancellationToken, options);\n    }\n    private async loadResource(item: LoadResourceItem, cancellationToken?: CancellationToken, options?: FetchResourceOptions): Promise<ArrayBuffer | string | any> {\n        const absoluteSrc = item.src.match(/^https?:\\/\\//) ? item.src : this.resourceBaseUrl + item.src;\n        const result = await this.fetchResource(absoluteSrc, cancellationToken, options);\n        return this.httpRequest.parseResult(item.type, result);\n    }\n    async loadResources(resourceTypes: (ResourceType | ResourceConfig)[], cancellationToken?: CancellationToken, onTotalProgress?: (progressPercent: number) => void): Promise<LoaderResult> {\n        const manifestItems = this.buildResourceManifest(resourceTypes);\n        const resultsMap = new Map<ResourceId, ArrayBuffer | string | any>();\n        const numItems = manifestItems.length;\n        let completedItems = 0;\n        const totalSizeHint = manifestItems.reduce((sum, item) => sum + (item.sizeHint ?? 0), 0);\n        let totalLoadedBytes = 0;\n        for (const item of manifestItems) {\n            if ((cancellationToken as any)?.isCancellationRequested) {\n                throw new OperationCanceledError(cancellationToken);\n            }\n            const itemProgress = { loadedBytes: 0 };\n            const response = await this.fetchResource(item.src, cancellationToken, {\n                onProgress: (loadedBytesDelta) => {\n                    if (onTotalProgress && totalSizeHint > 0) {\n                        totalLoadedBytes += (loadedBytesDelta - itemProgress.loadedBytes);\n                        itemProgress.loadedBytes = loadedBytesDelta;\n                        onTotalProgress(Math.floor(100 * Math.min(1, totalLoadedBytes / totalSizeHint)));\n                    }\n                },\n            });\n            if (item.id) {\n                resultsMap.set(item.id, this.httpRequest.parseResult(item.type, response));\n            }\n            completedItems++;\n            if (onTotalProgress && totalSizeHint === 0 && numItems > 0) {\n                onTotalProgress(Math.floor((completedItems / numItems) * 100));\n            }\n        }\n        return new LoaderResult(resultsMap);\n    }\n    protected async fetchResource(url: string, cancellationToken?: CancellationToken, options?: FetchResourceOptions): Promise<ArrayBuffer> {\n        return await this.httpRequest.fetchRaw(url, cancellationToken as any, options?.onProgress as any);\n    }\n}\n"
  },
  {
    "path": "src/engine/Theater.ts",
    "content": "import { TileSets } from '../game/theater/TileSets';\nimport { PaletteType } from './type/PaletteType';\nimport type { TheaterType, TheaterSettings } from './TheaterType';\nimport type { Palette } from '../data/Palette';\nimport type { LazyResourceCollection } from './LazyResourceCollection';\nimport type { TmpFile } from '../data/TmpFile';\nimport type { IniFile } from '../data/IniFile';\nimport type { FileSystem } from '../data/vfs/FileSystem';\nexport class Theater {\n    public type: TheaterType;\n    public settings: TheaterSettings;\n    private palettes: LazyResourceCollection<Palette>;\n    public isoPalette: Palette;\n    public ovlPalette: Palette;\n    public unitPalette: Palette;\n    public animPalette: Palette;\n    public libPalette: Palette;\n    public tileSets: TileSets;\n    static factory(type: TheaterType, theaterIni: IniFile, settings: TheaterSettings, tileDataCollection: any, palettesCollection: LazyResourceCollection<Palette>): Theater {\n        const isoPalette = palettesCollection.get(settings.isoPaletteName);\n        if (!isoPalette) {\n            throw new Error(`Missing palette \"${settings.isoPaletteName}\"`);\n        }\n        const overlayPalette = palettesCollection.get(settings.overlayPaletteName);\n        if (!overlayPalette) {\n            throw new Error(`Missing palette \"${settings.overlayPaletteName}\"`);\n        }\n        const unitPalette = palettesCollection.get(settings.unitPaletteName);\n        if (!unitPalette) {\n            throw new Error(`Missing palette \"${settings.unitPaletteName}\"`);\n        }\n        const animPalette = palettesCollection.get(\"anim.pal\");\n        if (!animPalette) {\n            throw new Error(\"Missing anim palette\");\n        }\n        const libPalette = palettesCollection.get(settings.libPaletteName);\n        if (!libPalette) {\n            throw new Error(\"Missing lib palette \" + settings.libPaletteName);\n        }\n        const tileSetsInstance = new TileSets(theaterIni);\n        tileSetsInstance.loadTileData(tileDataCollection as FileSystem, settings.extension);\n        return new Theater(type, settings, palettesCollection, isoPalette, overlayPalette, unitPalette, animPalette, libPalette, tileSetsInstance);\n    }\n    constructor(type: TheaterType, settings: TheaterSettings, palettes: LazyResourceCollection<Palette>, isoPalette: Palette, ovlPalette: Palette, unitPalette: Palette, animPalette: Palette, libPalette: Palette, tileSets: TileSets) {\n        this.type = type;\n        this.settings = settings;\n        this.palettes = palettes;\n        this.isoPalette = isoPalette;\n        this.ovlPalette = ovlPalette;\n        this.unitPalette = unitPalette;\n        this.animPalette = animPalette;\n        this.libPalette = libPalette;\n        this.tileSets = tileSets;\n    }\n    getPalette(type: PaletteType, customPaletteName?: string): Palette {\n        switch (type) {\n            case PaletteType.Anim:\n                return this.animPalette;\n            case PaletteType.Overlay:\n                return this.ovlPalette;\n            case PaletteType.Unit:\n                return this.unitPalette;\n            case PaletteType.Custom:\n                if (customPaletteName === \"lib\")\n                    return this.libPalette;\n                if (!customPaletteName)\n                    throw new Error('Custom palette name required for PaletteType.Custom');\n                const customPalette = this.palettes.get(customPaletteName + \".pal\");\n                if (!customPalette) {\n                    throw new Error(`Custom palette \"${customPaletteName}\" not found`);\n                }\n                return customPalette;\n            default:\n                return this.isoPalette;\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/TheaterType.ts",
    "content": "export interface TheaterSettings {\n    isoPaletteName: string;\n    overlayPaletteName: string;\n    unitPaletteName: string;\n    libPaletteName: string;\n    extension: string;\n    type: TheaterType;\n    [key: string]: any;\n}\n\nexport enum TheaterType {\n    None = 0,\n    Temperate = 1,\n    Urban = 2,\n    Snow = 4,\n    Lunar = 8,\n    Desert = 16,\n    NewUrban = 32,\n    All = 63\n}\n"
  },
  {
    "path": "src/engine/UiAnimationLoop.ts",
    "content": "import { Renderer } from './gfx/Renderer';\nimport { recordUiPerformanceFrame } from '@/performance/PerformanceRuntime';\nexport class UiAnimationLoop {\n    private renderer: Renderer;\n    private isStarted: boolean = false;\n    private paused: boolean = false;\n    private rafId?: number;\n    private backgroundIntervalId?: number;\n    constructor(renderer: Renderer) {\n        this.renderer = renderer;\n    }\n    private doBackgroundFrame = (timestamp: number): void => {\n        if (this.isStarted && this.paused) {\n            this.renderer.update(timestamp);\n        }\n    };\n    private doFrame = (timestamp: number): void => {\n        if (this.isStarted && !this.paused) {\n            recordUiPerformanceFrame(timestamp);\n            const stats = this.renderer.getStats();\n            if (stats) {\n                stats.begin();\n            }\n            this.renderer.update(timestamp);\n            this.renderer.render();\n            if (stats) {\n                stats.end();\n            }\n            this.rafId = requestAnimationFrame(this.doFrame);\n        }\n    };\n    private handleVisibilityChange = (): void => {\n        const isHidden = document.hidden;\n        if (this.paused !== isHidden) {\n            this.paused = isHidden;\n            if (this.paused) {\n                if (this.rafId) {\n                    cancelAnimationFrame(this.rafId);\n                    this.rafId = undefined;\n                }\n                this.backgroundIntervalId = setInterval(() => {\n                    const timestamp = performance.now();\n                    this.doBackgroundFrame(timestamp);\n                }, 1000);\n            }\n            else {\n                if (this.backgroundIntervalId) {\n                    clearInterval(this.backgroundIntervalId);\n                    this.backgroundIntervalId = undefined;\n                }\n                this.rafId = requestAnimationFrame(this.doFrame);\n            }\n        }\n    };\n    start(): void {\n        if (!this.isStarted) {\n            this.isStarted = true;\n            this.paused = false;\n            if (document.hidden) {\n                this.handleVisibilityChange();\n            }\n            else {\n                this.rafId = requestAnimationFrame(this.doFrame);\n            }\n            document.addEventListener('visibilitychange', this.handleVisibilityChange);\n        }\n    }\n    stop(): void {\n        if (this.isStarted) {\n            this.isStarted = false;\n            if (this.rafId) {\n                cancelAnimationFrame(this.rafId);\n                this.rafId = undefined;\n            }\n            if (this.backgroundIntervalId) {\n                clearInterval(this.backgroundIntervalId);\n                this.backgroundIntervalId = undefined;\n            }\n            document.removeEventListener('visibilitychange', this.handleVisibilityChange);\n        }\n    }\n    destroy(): void {\n        this.stop();\n        this.renderer.flush();\n    }\n}\n"
  },
  {
    "path": "src/engine/animation/Runner.ts",
    "content": "export class Runner {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/engine/animation/SimpleRunner.ts",
    "content": "import { Animation, AnimationState } from '../Animation';\nexport class SimpleRunner {\n    animation?: Animation;\n    constructor() { }\n    tick(time: number): void {\n        const animation = this.animation;\n        if (animation) {\n            switch (animation.getState()) {\n                case AnimationState.STOPPED:\n                    return;\n                case AnimationState.NOT_STARTED:\n                    animation.start(time);\n                    animation.update(time);\n                    return;\n                case AnimationState.RUNNING:\n                default:\n                    animation.update(time);\n                    return;\n            }\n        }\n    }\n    getCurrentFrame(): number {\n        return this.animation?.getCurrentFrame() ?? 0;\n    }\n    shouldUpdate(): boolean {\n        return this.animation?.getState() !== AnimationState.STOPPED;\n    }\n    setAnimation(animation: Animation): void {\n        this.animation = animation;\n    }\n    start(time: number, delayFrames?: number): void {\n        if (this.animation) {\n            this.animation.start(time, delayFrames);\n        }\n    }\n    stop(): void {\n        if (this.animation) {\n            this.animation.stop();\n        }\n    }\n    update(time: number): void {\n        if (this.animation) {\n            this.animation.update(time);\n        }\n    }\n    isStopped(): boolean {\n        return this.animation?.getState() === AnimationState.STOPPED;\n    }\n    getState(): AnimationState | undefined {\n        return this.animation?.getState();\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/CdnManifest.ts",
    "content": "export interface CdnManifest {\n    version: number;\n    format: string;\n    checksums?: Record<string, number | string>;\n    [key: string]: any;\n}\n"
  },
  {
    "path": "src/engine/gameRes/CdnResourceLoader.ts",
    "content": "import { DataStream } from '../../data/DataStream';\nimport { Crc32 } from '../../data/Crc32';\nimport { VirtualFile } from '../../data/vfs/VirtualFile';\nimport { ResourceLoader } from '../ResourceLoader';\nimport { DownloadError } from '../../network/HttpRequest';\nimport type { CancellationToken } from '@puzzl/core/lib/async/cancellation';\nimport { RealFileSystemDir } from '../../data/vfs/RealFileSystemDir';\nimport type { RealFileSystem } from '../../data/vfs/RealFileSystem';\ninterface CdnManifest {\n    version: number;\n    format: string;\n    checksums: {\n        [fileName: string]: number;\n    };\n}\ninterface CdnFetchOptions {\n    onProgress?: (loadedBytesDelta: number, totalLength?: number) => void;\n}\nexport class CdnResourceLoader extends ResourceLoader {\n    private static readonly cachePrefix = \"cdncache_\";\n    private cdnManifest: CdnManifest;\n    private cacheDir?: RealFileSystemDir;\n    private rfsForCache?: RealFileSystem;\n    static async clearCache(cacheDir: RealFileSystemDir): Promise<void> {\n        try {\n            for await (const entryName of cacheDir.getEntries()) {\n                if (entryName.startsWith(CdnResourceLoader.cachePrefix)) {\n                    await cacheDir.deleteFile(entryName, true);\n                }\n            }\n        }\n        catch (e) {\n            console.error(\"Error clearing CDN cache:\", e);\n        }\n    }\n    constructor(baseUrl: string, manifest: CdnManifest, cacheDirHandle?: FileSystemDirectoryHandle, rfsForCache?: RealFileSystem) {\n        super(baseUrl);\n        this.cdnManifest = manifest;\n        if (cacheDirHandle) {\n            this.cacheDir = new RealFileSystemDir(cacheDirHandle);\n        }\n        this.rfsForCache = rfsForCache;\n    }\n    private getFileNameFromUrl(url: string): string {\n        const pathPart = url.split(\"?\")[0];\n        return pathPart.substring(pathPart.lastIndexOf('/') + 1);\n    }\n    protected async fetchResource(url: string, cancellationToken?: CancellationToken, options?: CdnFetchOptions): Promise<ArrayBuffer> {\n        const fileName = this.getFileNameFromUrl(url);\n        const cacheFileName = CdnResourceLoader.cachePrefix + fileName;\n        const expectedChecksum = this.cdnManifest.checksums[fileName];\n        if (this.cacheDir && fileName.endsWith(\".mix\") && expectedChecksum !== undefined) {\n            try {\n                if (await this.cacheDir.containsEntry(cacheFileName)) {\n                    const cachedFile = await this.cacheDir.getRawFile(cacheFileName, true);\n                    const cachedData = await cachedFile.arrayBuffer();\n                    const fileUint8Array = new Uint8Array(cachedData);\n                    if (Crc32.calculateCrc(fileUint8Array) === expectedChecksum) {\n                        options?.onProgress?.(fileUint8Array.length, fileUint8Array.length);\n                        return cachedData;\n                    }\n                    try {\n                        await this.cacheDir.deleteFile(cacheFileName, true);\n                    }\n                    catch (delError) {\n                        console.error(`Couldn't delete stale cache file \"${cacheFileName}\"`, delError);\n                    }\n                }\n            }\n            catch (cacheReadError) {\n                console.error(`Couldn't read file \"${cacheFileName}\" from local CDN cache`, cacheReadError);\n            }\n        }\n        let urlToFetch = url;\n        if (expectedChecksum !== undefined) {\n            urlToFetch += (urlToFetch.includes(\"?\") ? \"&\" : \"?\") + \"h=\" + expectedChecksum.toString(16);\n        }\n        const networkDataBuffer = await super.fetchResource(urlToFetch, cancellationToken, options);\n        const networkDataUint8 = new Uint8Array(networkDataBuffer);\n        if (expectedChecksum !== undefined && Crc32.calculateCrc(networkDataUint8) !== expectedChecksum) {\n            throw new DownloadError(`Checksum mismatch for URL \"${urlToFetch}\"`);\n        }\n        if (this.cacheDir && expectedChecksum !== undefined && fileName.endsWith(\".mix\")) {\n            try {\n                const virtualFile = VirtualFile.fromBytes(networkDataUint8, cacheFileName);\n                await this.cacheDir.writeFile(virtualFile);\n            }\n            catch (cacheWriteError) {\n                console.error(`Couldn't write file \"${cacheFileName}\" to local CDN cache`, cacheWriteError);\n            }\n        }\n        return networkDataBuffer;\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/FileSystemAccessLib.ts",
    "content": "export interface FileSystemAccessAdapterSupport {\n    native?: boolean;\n    cache?: boolean;\n    [key: string]: any;\n}\nexport interface FileSystemAccessAdapters {\n    indexeddb?: any;\n    cache?: any;\n    [key: string]: any;\n}\nexport interface FileSystemAccessLib {\n    support: {\n        adapter: FileSystemAccessAdapterSupport;\n    };\n    adapters: FileSystemAccessAdapters;\n    getOriginPrivateDirectory: (adapterModule?: any) => Promise<FileSystemDirectoryHandle>;\n    polyfillDataTransferItem?: () => Promise<void>;\n    showDirectoryPicker?: (options?: any) => Promise<FileSystemDirectoryHandle>;\n    showOpenFilePicker?: (options?: any) => Promise<FileSystemFileHandle[]>;\n    showSaveFilePicker?: (options?: any) => Promise<FileSystemFileHandle>;\n    [key: string]: any;\n}\n"
  },
  {
    "path": "src/engine/gameRes/FileSystemUtil.ts",
    "content": "import { FileNotFoundError } from '../../data/vfs/FileNotFoundError';\nimport { IOError } from '../../data/vfs/IOError';\nexport class FileSystemUtil {\n    static async getDirContents(directoryHandle: FileSystemDirectoryHandle): Promise<FileSystemHandle[]> {\n        const entries: FileSystemHandle[] = [];\n        try {\n            for await (const handle of directoryHandle.values()) {\n                entries.push(handle);\n            }\n        }\n        catch (e: any) {\n            if (e.name === \"NotFoundError\") {\n                const err = new FileNotFoundError(`Directory \"${directoryHandle.name}\" not found while getting contents`);\n                (err as any).cause = e;\n                throw err;\n            }\n            if (e instanceof DOMException) {\n                const err = new IOError(`Directory \"${directoryHandle.name}\" could not be read (${e.name}) while getting contents`);\n                (err as any).cause = e;\n                throw err;\n            }\n            throw e;\n        }\n        return entries;\n    }\n    static async listDir(directoryHandle: FileSystemDirectoryHandle): Promise<string[]> {\n        const entries: string[] = [];\n        try {\n            for await (const key of directoryHandle.keys()) {\n                entries.push(key);\n            }\n        }\n        catch (e: any) {\n            if (e.name === \"NotFoundError\") {\n                const err = new FileNotFoundError(`Directory \"${directoryHandle.name}\" not found while listing dir`);\n                (err as any).cause = e;\n                throw err;\n            }\n            if (e instanceof DOMException) {\n                const err = new IOError(`Directory \"${directoryHandle.name}\" could not be read (${e.name}) while listing dir`);\n                (err as any).cause = e;\n                throw err;\n            }\n            throw e;\n        }\n        return entries;\n    }\n    static async showArchivePicker(fsAccessLib?: any): Promise<FileSystemFileHandle | null> {\n        const pickerOptions = {\n            types: [\n                {\n                    description: \"Archive Files\",\n                    accept: {\n                        \"application/zip\": [\".zip\"],\n                        \"application/x-7z-compressed\": [\".7z\"],\n                        \"application/vnd.rar\": [\".rar\"],\n                        \"application/x-tar\": [\".tar\"],\n                        \"application/gzip\": [\".gz\", \".tgz\"],\n                        \"application/x-bzip2\": [\".bz2\", \".tbz2\"],\n                        \"application/x-xz\": [\".xz\"],\n                        \"application/octet-stream\": [\".exe\", \".mix\"],\n                    },\n                },\n            ],\n            multiple: false,\n        };\n        const pickerFn = fsAccessLib?.showOpenFilePicker || (window as any).showOpenFilePicker;\n        if (!pickerFn) {\n            return null;\n        }\n        try {\n            const handles = await pickerFn(pickerOptions);\n            if (Array.isArray(handles)) {\n                if (handles.length === 0)\n                    return null;\n                return handles[0];\n            }\n            return handles as FileSystemFileHandle;\n        }\n        catch (e: any) {\n            if (e.name === 'AbortError') {\n                console.log('File picker aborted by user.');\n                return null;\n            }\n            console.error(\"Error showing file picker:\", e);\n            throw e;\n        }\n    }\n    static polyfillGetFile(): void {\n        if (typeof FileSystemFileHandle !== 'undefined' && FileSystemFileHandle.prototype) {\n            const originalGetFile = FileSystemFileHandle.prototype.getFile;\n            if (originalGetFile && originalGetFile.toString().includes(\"this.name\")) {\n                return;\n            }\n            if (originalGetFile) {\n                FileSystemFileHandle.prototype.getFile = function (this: FileSystemFileHandle): Promise<File> {\n                    const handleName = this.name;\n                    return originalGetFile.call(this).then((file: File) => new File([file], handleName, {\n                        type: file.type,\n                        lastModified: file.lastModified,\n                    }));\n                };\n            }\n            else {\n            }\n        }\n        else {\n        }\n    }\n}\nexport {};\n"
  },
  {
    "path": "src/engine/gameRes/GameRes.ts",
    "content": "import { DataStream } from '../../data/DataStream';\nimport { MixFile } from '../../data/MixFile';\nimport { MixEntry } from '../../data/MixEntry';\nimport { VirtualFileSystem } from '../../data/vfs/VirtualFileSystem';\nimport { Engine, EngineType } from '../Engine';\nimport { ResourceLoader, LoaderResult } from '../ResourceLoader';\nimport { DownloadError } from '../../network/HttpRequest';\nimport { AppLogger } from '../../util/logger';\nimport { GameResConfig } from './GameResConfig';\nimport { ChecksumError } from './importError/ChecksumError';\nimport { FileNotFoundError as GameResFileNotFoundError } from './importError/FileNotFoundError';\nimport { NoStorageError } from './importError/NoStorageError';\nimport { Crc32 } from '../../data/Crc32';\nimport { Palette } from '../../data/Palette';\nimport { ShpFile } from '../../data/ShpFile';\nimport { PcxFile } from '../../data/PcxFile';\nimport { ImageUtils } from '../gfx/ImageUtils';\nimport { RgbaBitmap } from '../../data/Bitmap';\nimport { CanvasUtils } from '../gfx/CanvasUtils';\nimport { GameResBoxApi } from '../../gui/component/GameResBoxApi';\nimport { GameResSource } from './GameResSource';\nimport { RealFileSystem } from '../../data/vfs/RealFileSystem';\nimport { ResourceType, resourcesForPrefetch, theaterSpecificResources } from '../resourceConfigs';\nimport { CdnResourceLoader } from './CdnResourceLoader';\nimport { LocalPrefs, StorageKey } from '../../LocalPrefs';\nimport { FileSystemUtil } from './FileSystemUtil';\nimport { StorageQuotaError } from '../../data/vfs/StorageQuotaError';\nimport { FileNotFoundError as VfsFileNotFoundError } from '../../data/vfs/FileNotFoundError';\nimport { IOError } from '../../data/vfs/IOError';\nimport { GameResImporter, type ImportProgressCallback } from './GameResImporter';\nimport type { Strings } from '../../data/Strings';\nimport SplashScreen from '../../gui/component/SplashScreen';\nimport type { Viewport } from '../../gui/Viewport';\nimport type { Config } from '../../Config';\nimport { RealFileSystemDir } from '../../data/vfs/RealFileSystemDir';\ninterface FsAccessLibrary {\n    support: {\n        adapter: {\n            native?: boolean;\n            cache?: boolean;\n            indexeddb?: boolean;\n        };\n    };\n    adapters: {\n        indexeddb?: any;\n        cache?: any;\n    };\n    getOriginPrivateDirectory: (module?: any) => Promise<FileSystemDirectoryHandle>;\n}\ninterface InitResult {\n    configToPersist?: GameResConfig;\n    cdnResLoader?: CdnResourceLoader;\n}\ntype LoadProgressCallback = (loadingText?: string, backgroundImage?: string | Blob) => void;\ntype FatalErrorCallback = (error: Error, strings: Strings) => Promise<void>;\ntype ImportErrorCallback = (error: Error, strings: Strings) => Promise<void>;\nexport class GameRes {\n    private appVersion: string;\n    private modName?: string;\n    private fsAccessLib: FsAccessLibrary;\n    private localPrefs: LocalPrefs;\n    private strings: Strings;\n    private rootEl: HTMLElement;\n    private splashScreen: any;\n    private viewport: Viewport;\n    private appConfig: Config;\n    private appResPath: string;\n    private sentry?: any;\n    constructor(appVersion: string, modName: string | undefined, fsAccessLib: FsAccessLibrary, localPrefs: LocalPrefs, strings: Strings, rootEl: HTMLElement, splashScreen: any, viewport: Viewport, appConfig: Config, appResPath: string, sentry?: any) {\n        this.appVersion = appVersion;\n        this.modName = modName;\n        this.fsAccessLib = fsAccessLib;\n        this.localPrefs = localPrefs;\n        this.strings = strings;\n        this.rootEl = rootEl;\n        this.splashScreen = splashScreen;\n        this.viewport = viewport;\n        this.appConfig = appConfig;\n        this.appResPath = appResPath;\n        this.sentry = sentry;\n    }\n    async init(persistedConfig: GameResConfig | undefined, onFatalError: FatalErrorCallback, onImportError: ImportErrorCallback): Promise<InitResult> {\n        let resourcesLoadedSuccessfully = false;\n        let configRequiresSave = false;\n        let createdBlobUrl: string | undefined;\n        let cdnResourceLoader: CdnResourceLoader | undefined = undefined;\n        const updateSplashScreen: LoadProgressCallback = (text, image) => {\n            if (text)\n                this.splashScreen.setLoadingText(text);\n            if (image) {\n                let imageUrl: string;\n                if (typeof image === 'string') {\n                    imageUrl = image;\n                }\n                else {\n                    if (createdBlobUrl)\n                        URL.revokeObjectURL(createdBlobUrl);\n                    createdBlobUrl = URL.createObjectURL(image);\n                    imageUrl = createdBlobUrl;\n                }\n                this.splashScreen.setBackgroundImage(imageUrl);\n            }\n        };\n        let nativeFsHandle: FileSystemDirectoryHandle | undefined;\n        try {\n            nativeFsHandle = await this.getBrowserFsHandle(\"native\");\n        }\n        catch (e) {\n            if (!(e instanceof NoStorageError))\n                throw e;\n        }\n        let migrationDone = false;\n        try {\n            if (nativeFsHandle) {\n                migrationDone = await this.migrateStorageToNative(nativeFsHandle, updateSplashScreen);\n            }\n        }\n        catch (e: any) {\n            console.warn(\"Storage migration to native failed\", e);\n            const error = new Error(\"Failed to migrate files to native file system\");\n            (error as any).cause = e;\n            this.sentry?.captureException(error);\n            migrationDone = false;\n        }\n        finally {\n            updateSplashScreen(this.strings.get(\"GUI:LoadingEx\"));\n        }\n        let rfs: RealFileSystem | undefined;\n        try {\n            const fsHandleToUse = migrationDone && nativeFsHandle ? nativeFsHandle : await this.getBrowserFsHandle(\"fallback\");\n            if (fsHandleToUse) {\n                rfs = await Engine.initRfs(fsHandleToUse);\n            }\n        }\n        catch (e) {\n            if (!(e instanceof NoStorageError))\n                throw e;\n            console.warn(\"No storage adapters available.\");\n        }\n        let currentConfig = persistedConfig;\n        if (!currentConfig && rfs) {\n            const rootDir = rfs.getRootDirectory();\n            console.log('[GameRes] Checking for existing game files. RFS rootDir:', rootDir);\n            if (rootDir && await this.lookForGameFiles(rootDir)) {\n                console.log('[GameRes] Found game files in local storage, creating config');\n                currentConfig = new GameResConfig(\"\");\n                currentConfig.source = GameResSource.Local;\n                configRequiresSave = true;\n            }\n            else {\n                console.log('[GameRes] No game files found in local storage');\n            }\n        }\n        else {\n            console.log('[GameRes] Skipping game file check. currentConfig:', currentConfig, 'rfs:', rfs);\n        }\n        let modRfsDir: RealFileSystemDir | undefined;\n        if (rfs) {\n            const modDirHandle = await Engine.getModDir();\n            if (modDirHandle) {\n                modRfsDir = await this.loadMod(rfs, modDirHandle);\n            }\n            const mapDirHandle = await Engine.getMapDir();\n            if (mapDirHandle && rfs && typeof (rfs as any).addDirectoryHandle === 'function') {\n                const mapRfsDir = new RealFileSystemDir(mapDirHandle);\n                rfs.addDirectory(mapRfsDir);\n            }\n        }\n        if (currentConfig) {\n            const splashBg = await this.loadSplashScreenBackground(rfs?.getRootDirectory(), modRfsDir, currentConfig);\n            if (typeof splashBg === 'string') {\n                this.splashScreen.setBackgroundImage(splashBg);\n            }\n            else if (splashBg) {\n                if (createdBlobUrl)\n                    URL.revokeObjectURL(createdBlobUrl);\n                createdBlobUrl = URL.createObjectURL(splashBg);\n                this.splashScreen.setBackgroundImage(createdBlobUrl);\n            }\n            try {\n                this.splashScreen.setLoadingText(this.strings.get(\"GUI:LoadingEx\"));\n                cdnResourceLoader = await this.loadResources(rfs, currentConfig, updateSplashScreen);\n                resourcesLoadedSuccessfully = true;\n            }\n            catch (e: any) {\n                console.error(\"Failed to load initial game resources\", e);\n                console.error(\"Error details:\", {\n                    name: e.name,\n                    message: e.message,\n                    stack: e.stack,\n                    cause: e.cause\n                });\n                this.splashScreen.setLoadingText(\"\");\n                this.splashScreen.setBackgroundImage(\"\");\n                await onFatalError(e, this.strings);\n            }\n        }\n        const gameResBoxApi = new GameResBoxApi(this.viewport, this.strings, this.rootEl, this.fsAccessLib as any);\n        let archiveUrlFallback = this.appConfig.gameResArchiveUrl;\n        while (!resourcesLoadedSuccessfully) {\n            console.log('[GameRes] Resources not loaded successfully, prompting user for game files');\n            this.splashScreen.setLoadingText(\"\");\n            this.splashScreen.setBackgroundImage(\"\");\n            if (createdBlobUrl) {\n                URL.revokeObjectURL(createdBlobUrl);\n                createdBlobUrl = undefined;\n            }\n            console.log('[GameRes] Calling gameResBoxApi.promptForGameRes');\n            const userSelection = await gameResBoxApi.promptForGameRes(archiveUrlFallback, !!this.appConfig.gameresBaseUrl && !this.modName);\n            console.log('[GameRes] User selection from prompt:', userSelection);\n            currentConfig = new GameResConfig(this.appConfig.gameresBaseUrl ?? \"\");\n            configRequiresSave = true;\n            let selectedSource: GameResSource;\n            if (userSelection) {\n                if (userSelection instanceof URL) {\n                    selectedSource = GameResSource.Archive;\n                    archiveUrlFallback = userSelection.toString();\n                }\n                else {\n                    if (userSelection.kind === \"file\") {\n                        selectedSource = GameResSource.Archive;\n                    }\n                    else if (userSelection.kind === \"directory\") {\n                        selectedSource = GameResSource.Local;\n                    }\n                    else {\n                        const kind = (userSelection as any).kind;\n                        console.error(\"Unexpected FileSystemHandle kind:\", kind, userSelection);\n                        throw new Error(`Unexpected FileSystemHandle type from prompt: ${kind}`);\n                    }\n                }\n            }\n            else {\n                selectedSource = GameResSource.Cdn;\n            }\n            currentConfig.source = selectedSource;\n            if (selectedSource !== GameResSource.Cdn) {\n                try {\n                    if (!rfs) {\n                        if (selectedSource === GameResSource.Local && userSelection && !(userSelection instanceof URL) && userSelection.kind === 'directory') {\n                            const handle = userSelection as FileSystemDirectoryHandle;\n                            rfs = await Engine.initRfs(handle);\n                        }\n                        else {\n                            throw new NoStorageError(\"No storage adapters available for import.\");\n                        }\n                    }\n                    const rootDir = rfs.getRootDirectory();\n                    if (!rootDir)\n                        throw new Error(\"RFS root directory not available for import\");\n                    await new GameResImporter(this.appConfig, this.strings, this.sentry).import(userSelection, rootDir, (text, image) => {\n                        updateSplashScreen(text, image);\n                        if (text)\n                            console.info(text);\n                    });\n                    console.info(\"Game assets successfully imported.\");\n                }\n                catch (e: any) {\n                    console.error(\"Failed to import game assets\", e);\n                    console.error(\"Import error details:\", {\n                        name: e.name,\n                        message: e.message,\n                        stack: e.stack,\n                        originalError: e.originalError,\n                        userSelection: userSelection\n                    });\n                    this.splashScreen.setLoadingText(\"\");\n                    this.splashScreen.setBackgroundImage(\"\");\n                    await onImportError(e, this.strings);\n                    continue;\n                }\n                finally {\n                    this.splashScreen.setLoadingText(\"\");\n                }\n            }\n            try {\n                this.splashScreen.setLoadingText(this.strings.get(\"GUI:LoadingEx\"));\n                cdnResourceLoader = await this.loadResources(rfs, currentConfig, updateSplashScreen);\n                resourcesLoadedSuccessfully = true;\n            }\n            catch (e: any) {\n                console.error(\"Failed to load game assets after prompt/import\", e);\n                console.error(\"Load error details:\", {\n                    name: e.name,\n                    message: e.message,\n                    stack: e.stack,\n                    cause: e.cause,\n                    config: currentConfig\n                });\n                this.splashScreen.setLoadingText(\"\");\n                this.splashScreen.setBackgroundImage(\"\");\n                await onFatalError(e, this.strings);\n            }\n        }\n        if (createdBlobUrl)\n            URL.revokeObjectURL(createdBlobUrl);\n        return { configToPersist: configRequiresSave ? currentConfig : undefined, cdnResLoader: cdnResourceLoader };\n    }\n    private async loadMod(rfs: RealFileSystem, modDirHandle: FileSystemDirectoryHandle): Promise<RealFileSystemDir | undefined> {\n        let modName = this.modName;\n        let specificModDir: RealFileSystemDir | undefined;\n        if (modName) {\n            const baseModRfsDir = new RealFileSystemDir(modDirHandle);\n            if (await baseModRfsDir.containsEntry(modName)) {\n                console.info(`Loading mod \"${modName}\"...`);\n                specificModDir = await baseModRfsDir.getDirectory(modName);\n                rfs.addDirectory(specificModDir);\n                Engine.setActiveMod(modName);\n            }\n            else {\n                console.info(`Mod \"${modName}\" not found. Ignoring.`);\n                this.modName = undefined;\n                Engine.setActiveMod(undefined);\n            }\n        }\n        return specificModDir;\n    }\n    private async lookForGameFiles(rfsDir: RealFileSystemDir): Promise<boolean> {\n        const entries = await rfsDir.listEntries();\n        console.log('[GameRes.lookForGameFiles] Entries in directory:', entries);\n        const requiredFiles = [\"language.mix\", \"multi.mix\", \"ra2.mix\"];\n        const hasAllFiles = requiredFiles.every((fileName) => entries.includes(fileName));\n        console.log('[GameRes.lookForGameFiles] Required files:', requiredFiles, 'Has all files:', hasAllFiles);\n        return hasAllFiles;\n    }\n    private async migrateStorageToNative(nativeFsHandle: FileSystemDirectoryHandle, onProgress: LoadProgressCallback): Promise<boolean> {\n        const migrationPendingKey = \"_storage_migration_pending\";\n        if (this.localPrefs.getItem(migrationPendingKey)) {\n            console.info(\"Resuming pending native storage migration: clearing native storage first.\");\n            for await (const key of nativeFsHandle.keys()) {\n                await nativeFsHandle.removeEntry(key, { recursive: true });\n            }\n            this.localPrefs.removeItem(migrationPendingKey);\n        }\n        else {\n            let hasContent = false;\n            for await (const _ of nativeFsHandle.keys()) {\n                hasContent = true;\n                break;\n            }\n            if (hasContent) {\n                console.info(\"Native storage appears to have content. Migration not attempted.\");\n                return true;\n            }\n        }\n        if (this.localPrefs.getItem(StorageKey.LastGpuTier) === undefined) {\n            console.info(\"LastGpuTier not set in LocalPrefs. Migration skipped.\");\n            return true;\n        }\n        console.info(\"Attempting to migrate old storage to new native storage...\");\n        let fallbackFsHandle: FileSystemDirectoryHandle | undefined;\n        try {\n            fallbackFsHandle = await this.getBrowserFsHandle(\"fallback\");\n        }\n        catch (e) {\n            if (e instanceof NoStorageError) {\n                console.info(\"No existing fallback storage found. Migration skipped.\");\n                return false;\n            }\n            throw e;\n        }\n        if (navigator.storage?.estimate) {\n            try {\n                const usage = await navigator.storage.estimate();\n                if (usage.usage !== undefined && usage.quota !== undefined) {\n                    if (usage.usage > (usage.quota - 5 * 1024 * 1024) / 2) {\n                        console.info(\"Migration to native storage skipped due to insufficient space estimate.\");\n                        return false;\n                    }\n                }\n            }\n            catch (estError) {\n                console.warn(\"Could not estimate storage quota, proceeding with migration carefully:\", estError);\n            }\n        }\n        const fallbackRfsDir = new RealFileSystemDir(fallbackFsHandle);\n        const filesInFallback = await FileSystemUtil.listDir(fallbackRfsDir.getNativeHandle());\n        if (filesInFallback.includes(Engine.rfsSettings.cacheDir)) {\n            console.info(`Removing old cache directory: ${Engine.rfsSettings.cacheDir}`);\n            await fallbackRfsDir.deleteDirectory(Engine.rfsSettings.cacheDir, true);\n        }\n        this.localPrefs.setItem(migrationPendingKey, \"1\");\n        try {\n            await this.migrateDir(fallbackRfsDir, nativeFsHandle, onProgress);\n        }\n        catch (e) {\n            console.error(\"Error during directory migration, attempting to clear native target:\", e);\n            for await (const key of nativeFsHandle.keys()) {\n                try {\n                    await nativeFsHandle.removeEntry(key, { recursive: true });\n                }\n                catch { }\n            }\n            throw e;\n        }\n        finally {\n            this.localPrefs.removeItem(migrationPendingKey);\n        }\n        try {\n            console.info(\"Attempting to delete old IndexedDB database: fileSystem\");\n            indexedDB.deleteDatabase(\"fileSystem\");\n            if (this.fsAccessLib.support.adapter.cache && globalThis.caches) {\n                console.info(\"Attempting to delete old Cache API storage: sandboxed-fs\");\n                await globalThis.caches.delete(\"sandboxed-fs\");\n            }\n        }\n        catch (cleanupError) {\n            console.warn(\"Error during old storage cleanup:\", cleanupError);\n        }\n        console.info(\"Storage migration to native completed.\");\n        return true;\n    }\n    private async migrateDir(sourceDirHandleWrapper: RealFileSystemDir, targetDirHandle: FileSystemDirectoryHandle, onProgress: LoadProgressCallback): Promise<void> {\n        for await (const entry of sourceDirHandleWrapper.getNativeHandle().values()) {\n            onProgress(this.strings.get(\"TS:storage_migrating_file\", `${targetDirHandle.name}/${entry.name}`));\n            if (entry.kind === 'directory') {\n                const targetSubDir = await targetDirHandle.getDirectoryHandle(entry.name, { create: true });\n                const sourceSubDirWrapper = new RealFileSystemDir(entry as FileSystemDirectoryHandle);\n                await this.migrateDir(sourceSubDirWrapper, targetSubDir, onProgress);\n            }\n            else if (entry.kind === 'file') {\n                const cleanedName = entry.name.replace(/\\u200f/g, \"\");\n                const targetFileHandle = await targetDirHandle.getFileHandle(cleanedName, { create: true });\n                const writable = await targetFileHandle.createWritable();\n                const sourceFile = await (entry as FileSystemFileHandle).getFile();\n                await sourceFile.stream().pipeTo(writable);\n            }\n        }\n    }\n    private async loadResources(rfs: RealFileSystem | undefined, config: GameResConfig, onProgress: LoadProgressCallback): Promise<CdnResourceLoader | undefined> {\n        if (config.source === undefined) {\n            throw new Error(\"GameResConfig source is undefined before initializing game resource source in Engine.\");\n        }\n        Engine.initGameResSource(config.source);\n        let cdnLoader: CdnResourceLoader | undefined;\n        if (config.isCdn()) {\n            const cdnBaseUrl = config.getCdnBaseUrl();\n            if (!cdnBaseUrl)\n                throw new Error(\"CDN base URL not available in config\");\n            const tempResourceLoader = new ResourceLoader(cdnBaseUrl);\n            const manifest = await tempResourceLoader.loadJson(\"manifest.json\");\n            if (manifest.version !== 2) {\n                throw new Error(\"Unknown manifest version \" + manifest.version);\n            }\n            if (manifest.format !== \"mix\") {\n                throw new Error(\"Unsupported CDN resource format \" + manifest.format);\n            }\n            const cacheDirHandle = await Engine.getCacheDir();\n            if (!cacheDirHandle) {\n                console.warn(\"Cache directory handle not available, CDN resources might not be cached effectively.\");\n            }\n            cdnLoader = new CdnResourceLoader(cdnBaseUrl, manifest, cacheDirHandle, rfs || new RealFileSystem());\n        }\n        else {\n            if (!rfs) {\n                throw new NoStorageError(\"No available storage adapters for local/archive resources.\");\n            }\n            console.info(\"Checking integrity of mix files...\");\n            const rootDir = rfs.getRootDirectory();\n            if (!rootDir)\n                throw new Error(\"RFS root not available for mix integrity check\");\n            await this.checkMixesIntegrity(rootDir);\n            console.info(\"Mixes are valid.\");\n        }\n        const logger = AppLogger.get(\"vfs\");\n        logger.info(\"Initializing virtual filesystem...\");\n        const vfs = await Engine.initVfs(rfs, logger);\n        await vfs.loadStandaloneFiles({\n            exclude: [\"keyboard.ini\", \"theme.ini\"].map((fileName) => Engine.getFileNameVariant(fileName)),\n        });\n        await vfs.loadExtraMixFiles(Engine.getActiveEngine());\n        await this.loadCustomMix(vfs);\n        await this.loadMixes(config, cdnLoader, vfs, onProgress);\n        await Engine.loadMapList();\n        await this.initUiCssVariables(this.rootEl);\n        return cdnLoader;\n    }\n    private async checkMixesIntegrity(rfsDir: RealFileSystemDir): Promise<void> {\n        const mixesToVerify = new Map<string, string[]>([\n            [\"ra2.mix\", [\"E7BA3BE\", \"5DC70844\"]],\n            [\"multi.mix\", [\"984EFDB6\", \"3CDB648F\"]],\n        ]);\n        for (const [mixName, expectedCrcs] of mixesToVerify.entries()) {\n            let file: File;\n            let buffer: ArrayBuffer;\n            try {\n                file = await rfsDir.getRawFile(mixName, true);\n                buffer = await file.arrayBuffer();\n            }\n            catch (e: any) {\n                if (e instanceof VfsFileNotFoundError) {\n                    throw new GameResFileNotFoundError(mixName);\n                }\n                if (e instanceof DOMException) {\n                    const ioErr = new IOError(`Failed to read file (${e.name}) for CRC check`);\n                    (ioErr as any).cause = e;\n                    throw ioErr;\n                }\n                throw e;\n            }\n            const calculatedCrc = Crc32.calculateCrc(new Uint8Array(buffer));\n            if (!expectedCrcs.includes(calculatedCrc.toString(16).toUpperCase())) {\n                throw new ChecksumError(`Checksum mismatch for \"${mixName}\" (size: ${file.size}). ` +\n                    `Checksum \"${calculatedCrc.toString(16).toUpperCase()}\" doesn't match known values: ${expectedCrcs.join(', ')}`, mixName);\n            }\n        }\n    }\n    private async loadCustomMix(vfs: VirtualFileSystem): Promise<void> {\n        const resourceLoader = new ResourceLoader(this.appResPath);\n        const mixDataBuffer = await resourceLoader.loadBinary(`ra2cd.mix?v=${this.appVersion}`);\n        const mixFile = new MixFile(new DataStream(mixDataBuffer));\n        vfs.addArchive(mixFile, \"ra2cd.mix\");\n    }\n    private async loadMixes(config: GameResConfig, cdnLoader: CdnResourceLoader | undefined, vfs: VirtualFileSystem, onProgress: LoadProgressCallback): Promise<void> {\n        if (config.isCdn() && cdnLoader) {\n            const cdnBaseUrl = config.getCdnBaseUrl();\n            if (!cdnBaseUrl)\n                throw new Error(\"CDN Load: Base URL missing.\");\n            onProgress(this.strings.get(\"TS:Downloading\"), cdnBaseUrl + Engine.rfsSettings.splashImgFileName);\n            const coreMixesToLoad: ResourceType[] = [\n                ResourceType.Ini,\n                ResourceType.Ui,\n                ResourceType.Strings,\n            ];\n            const loadedCoreMixes = await cdnLoader.loadResources(coreMixesToLoad, undefined, (percent) => {\n                onProgress(this.strings.get(\"TS:DownloadingPg\", percent));\n            });\n            onProgress(this.strings.get(\"GUI:LoadingEx\"));\n            for (const resType of coreMixesToLoad) {\n                const mixFileName = cdnLoader.getResourceFileName(resType);\n                const mixData = loadedCoreMixes.pop(resType);\n                if (mixData instanceof ArrayBuffer) {\n                    const mixFile = new MixFile(new DataStream(mixData));\n                    vfs.addArchive(mixFile, mixFileName);\n                }\n                else {\n                    console.error(`Failed to load mix ${mixFileName} from CDN: incorrect data type.`);\n                }\n            }\n        }\n        else {\n            await vfs.loadImplicitMixFiles(Engine.getActiveEngine());\n            const cacheDirHandle = await Engine.getCacheDir();\n            if (cacheDirHandle) {\n                try {\n                    await CdnResourceLoader.clearCache(new RealFileSystemDir(cacheDirHandle));\n                }\n                catch (e) {\n                    if (!(e instanceof StorageQuotaError))\n                        throw e;\n                    console.warn(\"Could not clear CDN cache due to quota error:\", e);\n                }\n            }\n        }\n    }\n    private async initUiCssVariables(rootElement: HTMLElement): Promise<void> {\n        const imagesToConvert: [\n            string,\n            string?\n        ][] = [\n            [\"pudlgbgn.shp\", \"dialog.pal\"],\n            [\"mnbttn.shp\", \"mainbttn.pal\"],\n            [\"cue_i.pcx\"],\n            [\"cce_i.pcx\"],\n            [\"cce_il.pcx\"],\n            [\"cce_ir.pcx\"],\n        ];\n        if (!Engine.vfs)\n            throw new Error(\"VFS not initialized for UI CSS Variables\");\n        const convertedImageBlobs = await this.convertImagesToPng(Engine.vfs, imagesToConvert);\n        try {\n            const menuLogoFile = Engine.vfs.openFile(\"menulogo.png\");\n            convertedImageBlobs.set(\"menulogo.png\", menuLogoFile.asFile(\"image/png\"));\n        }\n        catch (e) {\n            console.warn('Failed to load menulogo.png from VFS for CSS variables', e);\n        }\n        try {\n            const iconSpriteBlob = await this.generateIconSprite(Engine.vfs);\n            if (iconSpriteBlob) {\n                convertedImageBlobs.set(\"icons24.pcx\", iconSpriteBlob);\n            }\n            else {\n                console.warn('Icon sprite generation failed or returned null, not adding to CSS variables.');\n            }\n        }\n        catch (e) {\n            console.warn('Failed to generate icon sprite for CSS variables', e);\n        }\n        const cssVarMap: {\n            [cssVar: string]: string;\n        } = {\n            \"--res-menu-logo\": \"menulogo.png\",\n            \"--res-icons-24\": \"icons24.pcx\",\n            \"--res-dlg-bgn\": \"pudlgbgn.shp\",\n            \"--res-mnbttn\": \"mnbttn.shp\",\n            \"--res-cue-i\": \"cue_i.pcx\",\n            \"--res-cce-i\": \"cce_i.pcx\",\n            \"--res-cce-il\": \"cce_il.pcx\",\n            \"--res-cce-ir\": \"cce_ir.pcx\",\n        };\n        const blobUrlsToRevoke: string[] = [];\n        for (const cssVar in cssVarMap) {\n            const fileNameKey = cssVarMap[cssVar];\n            const blob = convertedImageBlobs.get(fileNameKey);\n            if (blob) {\n                const blobUrl = URL.createObjectURL(blob);\n                blobUrlsToRevoke.push(blobUrl);\n                rootElement.style.setProperty(cssVar, `url(\"${blobUrl}\")`);\n            }\n            else {\n                console.warn(`Image for CSS variable \"${cssVar}\" (file: \"${fileNameKey}\") not found.`);\n            }\n        }\n    }\n    private async loadSplashScreenBackground(rfsDir: RealFileSystemDir | undefined, modDir: RealFileSystemDir | undefined, config: GameResConfig): Promise<string | Blob | undefined> {\n        const splashFileName = Engine.rfsSettings.splashImgFileName;\n        if (config.isCdn()) {\n            const cdnBaseUrl = config.getCdnBaseUrl();\n            return cdnBaseUrl ? cdnBaseUrl + splashFileName : undefined;\n        }\n        let splashFile: File | undefined;\n        if (modDir) {\n            try {\n                splashFile = await modDir.getRawFile(splashFileName, false, \"image/png\");\n            }\n            catch (e) {\n                if (!(e instanceof VfsFileNotFoundError))\n                    console.warn(\"Failed to load splash from mod dir\", e);\n            }\n        }\n        if (!splashFile && rfsDir) {\n            try {\n                splashFile = await rfsDir.getRawFile(splashFileName, false, \"image/png\");\n            }\n            catch (e) {\n                if (!(e instanceof VfsFileNotFoundError))\n                    console.warn(\"Failed to load splash from main game dir\", e);\n            }\n        }\n        return splashFile;\n    }\n    private async getBrowserFsHandle(preference: \"native\" | \"fallback\"): Promise<FileSystemDirectoryHandle> {\n        const adaptersToTry: {\n            name: string;\n            module?: any;\n        }[] = [];\n        if (preference === \"native\" && this.fsAccessLib.support.adapter.native) {\n            adaptersToTry.push({ name: \"native\", module: undefined });\n        }\n        if (preference === \"fallback\" || adaptersToTry.length === 0) {\n            if (this.fsAccessLib.support.adapter.indexeddb) {\n                adaptersToTry.push({ name: \"indexeddb\", module: this.fsAccessLib.adapters.indexeddb });\n            }\n            if (this.fsAccessLib.support.adapter.cache) {\n                adaptersToTry.push({ name: \"cache\", module: this.fsAccessLib.adapters.cache });\n            }\n        }\n        for (const adapterInfo of adaptersToTry) {\n            try {\n                console.info(`Loading storage adapter \"${adapterInfo.name}\"...`);\n                const fsHandle = await this.fsAccessLib.getOriginPrivateDirectory(adapterInfo.module);\n                try {\n                    const testFile = await fsHandle.getFileHandle(\"_browsercheck.tmp\", { create: true });\n                    if (typeof testFile.createWritable !== 'function') {\n                        throw new Error(\"createWritable is not supported on this file handle.\");\n                    }\n                    const actualFile = await testFile.getFile();\n                    if (actualFile.name !== testFile.name) {\n                        console.warn(\"Browser check: FileHandle.name and File.name mismatch. Polyfill might be needed.\");\n                    }\n                }\n                catch (checkError: any) {\n                    if (checkError.name === \"QuotaExceededError\") {\n                        console.error(`Storage adapter \"${adapterInfo.name}\" failed browser check due to QuotaExceededError.`);\n                        throw checkError;\n                    }\n                    else if (adapterInfo.name === \"indexeddb\" && checkError.name === \"NotFoundError\") {\n                        console.warn(\"IndexedDB NotFoundError during browser check, attempting reset...\");\n                        await new Promise<void>(resolve => {\n                            indexedDB.deleteDatabase(\"fileSystem\");\n                            this.localPrefs.removeItem(StorageKey.GameRes);\n                            console.warn(\"Reloading page to attempt IndexedDB recovery...\");\n                            location.reload();\n                        });\n                    }\n                    console.warn(`Browser check for adapter \"${adapterInfo.name}\" encountered an issue:`, checkError);\n                }\n                finally {\n                    try {\n                        await fsHandle.removeEntry(\"_browsercheck.tmp\");\n                    }\n                    catch {\n                    }\n                }\n                console.info(`Storage adapter \"${adapterInfo.name}\" loaded successfully.`);\n                return fsHandle;\n            }\n            catch (e: any) {\n                console.warn(`Couldn't load FS adapter \"${adapterInfo.name}\"`, e);\n            }\n        }\n        throw new NoStorageError(\"No available/functional FS adapters found.\");\n    }\n    private async convertImagesToPng(vfs: VirtualFileSystem, imageDefs: [\n        string,\n        string?\n    ][]): Promise<Map<string, Blob>> {\n        const results = new Map<string, Blob>();\n        for (const [fileName, paletteName] of imageDefs) {\n            let imageBlob: Blob | undefined;\n            try {\n                if (fileName.endsWith(\".shp\")) {\n                    const shpFile = vfs.openFile(fileName);\n                    const shpFileInstance = new ShpFile(shpFile);\n                    if (!paletteName) {\n                        throw new Error(`No palette specified for SHP image \"${fileName}\"`);\n                    }\n                    const palFile = vfs.openFile(paletteName);\n                    const paletteInstance = new Palette(palFile);\n                    imageBlob = await ImageUtils.convertShpToPng(shpFileInstance, paletteInstance);\n                }\n                else if (fileName.endsWith(\".pcx\")) {\n                    const pcxFile = vfs.openFile(fileName);\n                    const pcxFileInstance = new PcxFile(pcxFile);\n                    imageBlob = await pcxFileInstance.toPngBlob();\n                }\n                else {\n                    console.warn(`Unknown image type for conversion: \"${fileName}\"`);\n                    continue;\n                }\n                if (imageBlob) {\n                    results.set(fileName, imageBlob);\n                }\n            }\n            catch (e) {\n                console.error(`Failed to convert image \"${fileName}\":`, e);\n            }\n        }\n        return results;\n    }\n    private async generateIconSprite(vfs: VirtualFileSystem): Promise<Blob | null> {\n        const iconFiles = [\n            \"wouref.pcx\", \"wodref.pcx\", \"wouact.pcx\", \"wodact.pcx\",\n            \"dnarrowr.pcx\", \"dnarrowp.pcx\", \"uparrowr.pcx\", \"uparrowp.pcx\",\n            \"sbgript.pcx\", \"sbgripm.pcx\", \"sbgripb.pcx\", \"trakgrip.pcx\",\n        ];\n        const pcxFiles: PcxFile[] = [];\n        for (const fileName of iconFiles) {\n            try {\n                const virtualFile = vfs.openFile(fileName);\n                pcxFiles.push(new PcxFile(virtualFile));\n            }\n            catch (e) {\n                console.error(`Failed to load PCX for icon sprite: ${fileName}`, e);\n            }\n        }\n        if (pcxFiles.length === 0)\n            throw new Error(\"No PCX files loaded for icon sprite generation\");\n        const iconSize = 24;\n        const finalBitmap = new RgbaBitmap(iconSize * pcxFiles.length, iconSize);\n        for (let i = 0; i < pcxFiles.length; i++) {\n            const pcx = pcxFiles[i];\n            if (pcx.width && pcx.height && pcx.data) {\n                const iconBitmap = new RgbaBitmap(pcx.width, pcx.height, pcx.data);\n                finalBitmap.drawRgbaImage(iconBitmap, iconSize * i, 0, iconSize, iconSize);\n            }\n            else {\n                console.warn(`PCX file ${iconFiles[i]} missing data/dimensions for icon sprite.`);\n            }\n        }\n        const canvas = CanvasUtils.canvasFromRgbaImageData(finalBitmap.data, finalBitmap.width, finalBitmap.height);\n        return await CanvasUtils.canvasToBlob(canvas, \"image/png\");\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/GameResConfig.ts",
    "content": "import { GameResSource } from './GameResSource';\nexport class GameResConfig {\n    private defaultCdnBaseUrl: string;\n    public source?: GameResSource;\n    public cdnUrl?: string;\n    constructor(defaultCdnBaseUrl: string) {\n        this.defaultCdnBaseUrl = defaultCdnBaseUrl;\n    }\n    unserialize(serializedConfig: string): void {\n        const parts = serializedConfig.split(\",\");\n        const sourceNum = Number(parts[0]);\n        if (!(sourceNum in GameResSource)) {\n            throw new Error(`Unknown game res source type number: \"${sourceNum}\"`);\n        }\n        this.source = sourceNum as GameResSource;\n        this.cdnUrl = parts[1] ? decodeURIComponent(parts[1]) : undefined;\n    }\n    serialize(): string {\n        if (this.source === undefined) {\n            throw new Error(\"GameResConfig source is undefined, cannot serialize.\");\n        }\n        let serialized = String(this.source);\n        if (this.cdnUrl) {\n            serialized += \",\" + encodeURIComponent(this.cdnUrl);\n        }\n        return serialized;\n    }\n    isCdn(): boolean {\n        return this.source === GameResSource.Cdn;\n    }\n    getCdnBaseUrl(): string | undefined {\n        return this.cdnUrl ?? this.defaultCdnBaseUrl;\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/GameResImporter.ts",
    "content": "import { MixFile } from '../../data/MixFile';\nimport { Engine, EngineType } from '../Engine';\nimport { sleep } from '../../util/time';\nimport { ChecksumError } from './importError/ChecksumError';\nimport { FileNotFoundError as GameResFileNotFoundError } from './importError/FileNotFoundError';\nimport { ArchiveExtractionError } from './importError/ArchiveExtractionError';\nimport { VirtualFile } from '../../data/vfs/VirtualFile';\nimport { mixDatabase } from '../mixDatabase';\nimport { Palette } from '../../data/Palette';\nimport { ShpFile } from '../../data/ShpFile';\nimport { ImageUtils } from '../gfx/ImageUtils';\nimport * as stringUtils from '../../util/string';\nimport { VideoConverter } from './VideoConverter';\nimport { InvalidArchiveError } from './importError/InvalidArchiveError';\nimport { FileNotFoundError as VfsFileNotFoundError } from '../../data/vfs/FileNotFoundError';\nimport { IOError } from '../../data/vfs/IOError';\nimport { RealFileSystemDir } from '../../data/vfs/RealFileSystemDir';\nimport { NoWebAssemblyError } from './importError/NoWebAssemblyError';\nimport { HttpRequest, DownloadError } from '../../network/HttpRequest';\nimport { ArchiveDownloadError } from './importError/ArchiveDownloadError';\nimport type { Config } from '../../Config';\nimport type { Strings } from '../../data/Strings';\nimport type { DataStream } from '../../data/DataStream';\nimport type { FFmpeg } from '@ffmpeg/ffmpeg';\ninterface SevenZipWasmModule {\n    FS: any;\n    callMain: (args: string[]) => void;\n}\ninterface SevenZipWasmOptions {\n    quit?: (code: number, message?: string) => void;\n}\ndeclare function createSevenZipWasm(options?: SevenZipWasmOptions): Promise<SevenZipWasmModule>;\nconst REQUIRED_MIX_SIZES = new Map<string, number>()\n    .set(\"ra2.mix\", 281895456)\n    .set(\"language.mix\", 53116040)\n    .set(\"multi.mix\", 25856283)\n    .set(\"theme.mix\", 76862662);\nfunction formatBytes(bytes: number): string {\n    return (bytes / 1024 / 1024).toFixed(2) + \" MB\";\n}\nfunction wrapFsOpen(originalFsOpen: any, prefilledContents: Map<string, Uint8Array>) {\n    return function (this: any, path: string, flags: string, mode?: any, unknown1?: any, unknown2?: any) {\n        let stream = originalFsOpen.call(this, path, flags, mode, unknown1, unknown2);\n        const prefilledData = prefilledContents.get(stream.node.name);\n        if (prefilledData) {\n            stream.node.contents = new Uint8Array(prefilledData);\n            const originalWrite = stream.stream_ops.write;\n            stream.stream_ops = { ...stream.stream_ops };\n            stream.stream_ops.write = function (this: any, str: any, buffer: any, offset: number, length: number, position?: number, canOwn?: boolean) {\n                if (!position) {\n                    str.node.usedBytes = str.node.contents.byteLength;\n                }\n                const bytesWritten = originalWrite.call(this, str, buffer, offset, length, position, canOwn);\n                if (!position) {\n                    str.node.usedBytes = bytesWritten;\n                }\n                return bytesWritten;\n            };\n        }\n        return stream;\n    };\n}\nexport type ImportProgressCallback = (text?: string, backgroundImage?: Blob | string) => void;\nexport type ImportSource = URL | File | FileSystemDirectoryHandle | FileSystemFileHandle;\nexport class GameResImporter {\n    private appConfig: Config;\n    private strings: Strings;\n    private sentry?: any;\n    constructor(appConfig: Config, strings: Strings, sentry?: any) {\n        this.appConfig = appConfig;\n        this.strings = strings;\n        this.sentry = sentry;\n    }\n    async import(source: ImportSource | undefined, targetRfsRootDir: RealFileSystemDir, onProgress: ImportProgressCallback): Promise<void> {\n        const essentialMixes = [\"ra2.mix\", \"language.mix\", \"multi.mix\", \"theme.mix\"];\n        const optionalMixes = new Set([\"theme.mix\"]);\n        const tauntsDirName = Engine.rfsSettings.tauntsDir;\n        const S = this.strings;\n        console.log('[GameResImporter] Starting import process');\n        console.log('[GameResImporter] Source:', source);\n        console.log('[GameResImporter] WebAssembly available:', typeof WebAssembly);\n        console.log('[GameResImporter] Dynamic import supported: true');\n        onProgress(S.get(\"ts:import_preparing_for_import\"));\n        if (!source) {\n            throw new Error(\"Import source is undefined.\");\n        }\n        if (source instanceof URL || source instanceof File || (source as any).kind === \"file\") {\n            console.log('[GameResImporter] Processing archive file');\n            if (typeof WebAssembly !== 'object' || typeof WebAssembly.instantiate !== 'function') {\n                throw new NoWebAssemblyError(\"WebAssembly is not available or not an object.\");\n            }\n            console.log('[GameResImporter] WebAssembly check passed');\n            let sevenZipModule: SevenZipWasmModule;\n            let sevenZipExitCode: number | undefined;\n            let sevenZipErrorMessage: string | undefined;\n            try {\n                console.log('[GameResImporter] Attempting to load 7z-wasm module');\n                const sevenZipWasmModule = await import(\"7z-wasm\");\n                const sevenZipFactory = sevenZipWasmModule.default as any;\n                console.log('[GameResImporter] 7z-wasm module loaded, creating instance');\n                sevenZipModule = await sevenZipFactory({\n                    locateFile: (path: string, scriptDirectory: string) => {\n                        if (path === '7zz.wasm') {\n                            return '/7zz.wasm';\n                        }\n                        return path;\n                    },\n                    quit: (code: number, exitStatus: any) => {\n                        sevenZipExitCode = code;\n                        sevenZipErrorMessage = exitStatus?.message || String(exitStatus);\n                        console.log('[GameResImporter] 7z quit callback:', code, exitStatus);\n                    },\n                });\n                console.log('[GameResImporter] 7z-wasm instance created successfully');\n            }\n            catch (e: any) {\n                console.error('[GameResImporter] Failed to load/create 7z-wasm:', e);\n                if (e.message?.match(/Load failed|Failed to fetch/i)) {\n                    const error = new DownloadError(\"Failed to load 7z-wasm module\");\n                    (error as any).originalError = e;\n                    throw error;\n                }\n                if (e instanceof WebAssembly.RuntimeError) {\n                    const error = new IOError(\"Couldn't load 7z-wasm due to runtime error\");\n                    (error as any).originalError = e;\n                    throw error;\n                }\n                throw e;\n            }\n            let archiveData: Uint8Array;\n            let archiveName: string;\n            if (source instanceof URL) {\n                let downloadedBytes = 0;\n                const urlStr = source.toString();\n                const corsProxy = this.appConfig.getCorsProxy?.(source.hostname);\n                let effectiveUrl = urlStr;\n                if (corsProxy) {\n                    effectiveUrl = `${corsProxy}${encodeURIComponent(urlStr)}`;\n                }\n                try {\n                    const buffer = await new HttpRequest().fetchBinary(effectiveUrl, undefined, {\n                        onProgress: (delta, total) => {\n                            downloadedBytes += delta;\n                            const progressText = total\n                                ? S.get(\"ts:downloadingpgsize\", formatBytes(downloadedBytes), formatBytes(total), (downloadedBytes / total) * 100)\n                                : S.get(\"ts:downloadingpgunkn\", formatBytes(downloadedBytes));\n                            onProgress(progressText);\n                        },\n                    });\n                    archiveData = new Uint8Array(buffer);\n                    archiveName = source.pathname.split('/').pop() || \"archive.7z\";\n                }\n                catch (e: any) {\n                    if (downloadedBytes === 0 && e instanceof DownloadError) {\n                        const error = new ArchiveDownloadError(urlStr, \"Archive download failed at start\");\n                        (error as any).originalError = e;\n                        throw error;\n                    }\n                    throw e;\n                }\n            }\n            else if (source instanceof File) {\n                archiveData = new Uint8Array(await source.arrayBuffer());\n                archiveName = source.name;\n            }\n            else {\n                const fileHandle = source as FileSystemFileHandle;\n                const file = await fileHandle.getFile();\n                archiveData = new Uint8Array(await file.arrayBuffer());\n                archiveName = file.name;\n            }\n            onProgress(S.get(\"ts:import_loading_archive\"));\n            sevenZipModule.FS.chdir(\"/tmp\");\n            try {\n                const fileStream = sevenZipModule.FS.open(archiveName, \"w+\");\n                sevenZipModule.FS.write(fileStream, archiveData, 0, archiveData.byteLength, 0, true);\n                sevenZipModule.FS.close(fileStream);\n            }\n            catch (e: any) {\n                if (e instanceof DOMException) {\n                    const error = new IOError(`Could not write archive to Emscripten FS \"${archiveName}\" (${e.name})`);\n                    (error as any).originalError = e;\n                    throw error;\n                }\n                throw e;\n            }\n            const entriesToExtract = [...essentialMixes, tauntsDirName];\n            for (const entryName of entriesToExtract) {\n                onProgress(S.get(\"ts:import_extracting\", entryName));\n                await sleep(100);\n                sevenZipExitCode = undefined;\n                sevenZipErrorMessage = undefined;\n                sevenZipModule.callMain([\"x\", \"-ssc-\", \"-aoa\", archiveName, entryName]);\n                if (sevenZipExitCode !== 0 && sevenZipExitCode !== undefined) {\n                    if (sevenZipExitCode === 1 && entryName === tauntsDirName) {\n                        console.warn(`Taunts directory \"${entryName}\" not found in archive, or non-fatal extraction issue. Skipping.`);\n                    }\n                    else if (sevenZipExitCode === 1 && optionalMixes.has(entryName)) {\n                        console.warn(`Optional mix file \"${entryName}\" not found in archive or non-fatal extraction issue. Skipping.`);\n                    }\n                    else {\n                        const baseErrorMsg = `7-Zip exited with code ${sevenZipExitCode} for ${entryName}`;\n                        if (sevenZipErrorMessage?.match(/out of memory|allocation/i)) {\n                            const error = new RangeError(`${baseErrorMsg} - Out of memory`);\n                            (error as any).originalError = new Error(sevenZipErrorMessage);\n                            throw error;\n                        }\n                        const error = new ArchiveExtractionError(`${baseErrorMsg}`);\n                        (error as any).originalError = new Error(sevenZipErrorMessage);\n                        throw error;\n                    }\n                }\n                const emFsCurrentDirContents = sevenZipModule.FS.lookupPath(sevenZipModule.FS.cwd())[\"node\"].contents;\n                const extractedEntryNames = Object.keys(emFsCurrentDirContents);\n                if (entryName !== tauntsDirName) {\n                    const mixFileNameInFs = extractedEntryNames.find(name => stringUtils.equalsIgnoreCase(name, entryName)) || entryName;\n                    onProgress(S.get(\"ts:import_importing\", mixFileNameInFs));\n                    let fileData;\n                    try {\n                        fileData = this.readFileFromEmFs(sevenZipModule.FS, mixFileNameInFs);\n                        sevenZipModule.FS.unlink(mixFileNameInFs);\n                    }\n                    catch (e: any) {\n                        if (e.errno === 44 && optionalMixes.has(entryName)) {\n                            console.warn(`Optional Mix file \"${entryName}\" not found in Emscripten FS after extraction. Skipping.`);\n                            continue;\n                        }\n                        if (e.errno === 44 && !REQUIRED_MIX_SIZES.has(entryName.toLowerCase())) {\n                            console.warn(`File \"${entryName}\" not found in Emscripten FS and not strictly required. Skipping.`);\n                            continue;\n                        }\n                        throw new GameResFileNotFoundError(entryName);\n                    }\n                    await this.importMixArchive(fileData, targetRfsRootDir, onProgress, S);\n                }\n                else {\n                    const tauntsDirInFs = extractedEntryNames.find(name => stringUtils.equalsIgnoreCase(name, tauntsDirName));\n                    if (tauntsDirInFs) {\n                        const tauntsDirNode = sevenZipModule.FS.lookupPath(tauntsDirInFs)[\"node\"];\n                        const tauntFileNames = Object.keys(tauntsDirNode.contents).map(name => `${tauntsDirInFs}/${name}`);\n                        try {\n                            const targetTauntsDir = await targetRfsRootDir.getOrCreateDirectory(tauntsDirName, true);\n                            for (const tauntFilePath of tauntFileNames) {\n                                onProgress(S.get(\"ts:import_importing\", tauntFilePath));\n                                const fileData = this.readFileFromEmFs(sevenZipModule.FS, tauntFilePath);\n                                sevenZipModule.FS.unlink(tauntFilePath);\n                                await targetTauntsDir.writeFile(fileData);\n                            }\n                        }\n                        catch (e: any) {\n                            if (!(e instanceof DOMException || e instanceof IOError || e.errno === 44))\n                                throw e;\n                            console.warn(\"Failed to copy taunts folder. Skipping.\", e);\n                        }\n                    }\n                    else {\n                        console.warn(\"Taunts folder not found in archive after extraction. Skipping.\");\n                    }\n                }\n            }\n            sevenZipModule.FS.unlink(archiveName);\n            try {\n                await targetRfsRootDir.openFile(\"ra2.mix\");\n            }\n            catch (e) {\n                if (e instanceof VfsFileNotFoundError || e instanceof IOError) {\n                    onProgress(this.strings.get(\"GUI:LoadingEx\"));\n                    console.error(\"Essential file ra2.mix not found after import. Reloading might be necessary.\");\n                    throw new Error(\"Import verification failed: ra2.mix not found.\");\n                }\n                throw e;\n            }\n        }\n        else {\n            const sourceDirWrapper = new RealFileSystemDir(source as FileSystemDirectoryHandle, true);\n            const sourceEntries = await sourceDirWrapper.listEntries();\n            for (const mixName of essentialMixes) {\n                onProgress(S.get(\"ts:import_importing\", mixName));\n                const actualFileName = sourceEntries.find(entry => stringUtils.equalsIgnoreCase(entry, mixName)) || mixName;\n                let virtualFile;\n                try {\n                    virtualFile = await sourceDirWrapper.openFile(actualFileName);\n                }\n                catch (e: any) {\n                    if (e instanceof VfsFileNotFoundError) {\n                        if (optionalMixes.has(mixName)) {\n                            console.warn(`Optional Mix file \"${mixName}\" not found in source directory. Skipping.`);\n                            continue;\n                        }\n                        throw new GameResFileNotFoundError(mixName);\n                    }\n                    throw e;\n                }\n                await this.importMixArchive(virtualFile, targetRfsRootDir, onProgress, S);\n            }\n            const tauntsDirInSource = sourceEntries.find(entry => stringUtils.equalsIgnoreCase(entry, tauntsDirName)) || tauntsDirName;\n            let sourceTauntsDir: RealFileSystemDir | undefined;\n            try {\n                sourceTauntsDir = await sourceDirWrapper.getDirectory(tauntsDirInSource);\n            }\n            catch (e: any) {\n                if (!(e instanceof VfsFileNotFoundError || e instanceof IOError))\n                    throw e;\n                console.warn(`Taunts directory \"${tauntsDirInSource}\" not found in source (${e.name}). Skipping.`);\n            }\n            if (sourceTauntsDir) {\n                try {\n                    const targetTauntsRfsDir = await targetRfsRootDir.getOrCreateDirectory(tauntsDirName, true);\n                    for await (const rawFile of sourceTauntsDir.getRawFiles()) {\n                        onProgress(S.get(\"ts:import_importing\", `${targetTauntsRfsDir.name}/${rawFile.name}`));\n                        const virtualFile = await VirtualFile.fromRealFile(rawFile);\n                        await targetTauntsRfsDir.writeFile(virtualFile);\n                    }\n                }\n                catch (e: any) {\n                    if (!(e instanceof IOError))\n                        throw e;\n                    console.warn(\"Failed to copy taunts folder from source. Skipping.\", e);\n                }\n            }\n        }\n        onProgress(\"Game assets successfully imported.\");\n    }\n    private readFileFromEmFs(emFs: any, filePath: string): VirtualFile {\n        emFs.chmod(filePath, 0o700);\n        const fileNode = emFs.lookupPath(filePath)[\"node\"];\n        if (!fileNode || !fileNode.contents) {\n            throw new VfsFileNotFoundError(`File node or contents missing in Emscripten FS for ${filePath}`);\n        }\n        const fileData = fileNode.contents.subarray(0, fileNode.usedBytes);\n        const fileName = filePath.slice(filePath.lastIndexOf('/') + 1);\n        return VirtualFile.fromBytes(fileData, fileName);\n    }\n    private async importMixArchive(mixVirtualFile: VirtualFile, targetRfsRootDir: RealFileSystemDir, onProgress: ImportProgressCallback, S: Strings): Promise<void> {\n        const mixFileNameLower = mixVirtualFile.filename.toLowerCase();\n        const isThemeMix = !!mixFileNameLower.match(/^theme[^.]*\\.mix$/);\n        if (mixVirtualFile.getSize() === 0) {\n            if (isThemeMix) {\n                console.warn(`Mix file ${mixVirtualFile.filename} is empty. Skipping theme import.`);\n                return;\n            }\n            throw new ChecksumError(`Mix file \"${mixFileNameLower}\" is empty`, mixFileNameLower);\n        }\n        if (!isThemeMix) {\n            await targetRfsRootDir.writeFile(mixVirtualFile, mixFileNameLower);\n        }\n        if (isThemeMix) {\n            const musicDirName = Engine.rfsSettings.musicDir;\n            const targetMusicDir = await targetRfsRootDir.getOrCreateDirectory(musicDirName, true);\n            await this.importMusic(mixVirtualFile, targetMusicDir, (percent) => onProgress(S.get(\"ts:import_importing_pg\", mixFileNameLower, percent.toFixed(0))));\n        }\n        else if (mixFileNameLower.match(/language\\.mix$/)) {\n            onProgress(S.get(\"ts:import_importing_long\", mixFileNameLower));\n            await this.importVideo(mixVirtualFile, targetRfsRootDir);\n        }\n        else if (mixFileNameLower.match(/ra2\\.mix$/)) {\n            const splashImageBlob = await this.importSplashImage(mixVirtualFile, targetRfsRootDir);\n            if (splashImageBlob)\n                onProgress(undefined, splashImageBlob);\n        }\n    }\n    private async importMusic(mixVirtualFile: VirtualFile, targetMusicDir: RealFileSystemDir, onProgressPercent: (percent: number) => void): Promise<void> {\n        let mixFileInstance: MixFile;\n        try {\n            mixFileInstance = new MixFile(mixVirtualFile.stream as DataStream);\n        }\n        catch (e) {\n            console.warn(`Failed to read music mix archive \"${mixVirtualFile.filename}\". Skipping.`, e);\n            return;\n        }\n        const knownMusicFiles = mixDatabase.get(mixVirtualFile.filename.toLowerCase());\n        if (!knownMusicFiles) {\n            console.warn(`File \"${mixVirtualFile.filename}\" not found in mix database. Skipping music import.`);\n            return;\n        }\n        const totalFiles = knownMusicFiles.length;\n        let processedFiles = 0;\n        for (const wavFileNameInMix of knownMusicFiles) {\n            processedFiles++;\n            onProgressPercent((processedFiles / totalFiles) * 100);\n            if (!wavFileNameInMix.toLowerCase().endsWith('.wav')) {\n                console.warn(`Music file \"${wavFileNameInMix}\" in mix ${mixVirtualFile.filename} is not a WAV file. Skipping.`);\n                continue;\n            }\n            const mp3FileName = wavFileNameInMix.replace(/\\.wav$/i, \".mp3\");\n            if (mixFileInstance.containsFile(wavFileNameInMix)) {\n                const wavFileEntry = mixFileInstance.openFile(wavFileNameInMix);\n                if (wavFileEntry.stream.byteLength > 0) {\n                    let mp3Data: Uint8Array | undefined;\n                    try {\n                        const ffmpeg = await this.createFFmpeg();\n                        const wavData = new Uint8Array(wavFileEntry.stream.buffer, wavFileEntry.stream.byteOffset, wavFileEntry.stream.byteLength);\n                        await ffmpeg.writeFile(wavFileNameInMix, wavData);\n                        await ffmpeg.exec([\"-i\", wavFileNameInMix, \"-vn\", \"-ar\", \"22050\", \"-q:a\", \"5\", mp3FileName]);\n                        mp3Data = await ffmpeg.readFile(mp3FileName) as Uint8Array;\n                        await ffmpeg.deleteFile(wavFileNameInMix);\n                        await ffmpeg.deleteFile(mp3FileName);\n                    }\n                    catch (e) {\n                        console.warn(`Failed to convert music file \"${wavFileNameInMix}\" to MP3. Skipping.`, e);\n                        this.sentry?.captureException(new Error(`FFmpeg conversion failed for ${wavFileNameInMix}`), { extra: { error: e } });\n                        continue;\n                    }\n                    if (mp3Data) {\n                        const mp3Blob = new Blob([mp3Data as any], { type: \"audio/mpeg\" });\n                        try {\n                            const virtualMp3 = VirtualFile.fromBytes(mp3Data, mp3FileName);\n                            await targetMusicDir.writeFile(virtualMp3);\n                        }\n                        catch (e) {\n                            console.warn(`Failed to write music file \"${mp3FileName}\" to target. Skipping.`, e);\n                        }\n                    }\n                }\n                else {\n                    console.warn(`Music file \"${wavFileNameInMix}\" is empty in the mix archive. Skipping.`);\n                }\n            }\n            else {\n                console.warn(`Music file \"${wavFileNameInMix}\" was not found in mix archive \"${mixVirtualFile.filename}\". Skipping.`);\n            }\n        }\n    }\n    private async importVideo(languageMixVirtualFile: VirtualFile, targetRfsRootDir: RealFileSystemDir): Promise<void> {\n        let ffmpeg: FFmpeg;\n        try {\n            ffmpeg = await this.createFFmpeg();\n        }\n        catch (e: any) {\n            if (e.message?.match(/Load failed|Failed to fetch/i)) {\n                const error = new DownloadError(\"Failed to load FFmpeg for video conversion\");\n                (error as any).originalError = e;\n                throw error;\n            }\n            this.sentry?.captureException(new Error(\"FFmpeg creation failed for video import\"));\n            console.error(\"Skipping video import due to FFmpeg creation failure.\", e);\n            return;\n        }\n        const langMix = new MixFile(languageMixVirtualFile.stream as DataStream);\n        console.log('[GameResImporter] language.mix loaded, checking detailed contents...');\n        console.log('[GameResImporter] File size:', languageMixVirtualFile.getSize(), 'bytes');\n        console.log('[GameResImporter] MixFile index size:', (langMix as any).index?.size || 'unknown');\n        if ((langMix as any).index) {\n            const index = (langMix as any).index as Map<number, any>;\n            console.log('[GameResImporter] language.mix index entries (first 20):');\n            let entryCount = 0;\n            for (const [hash, entry] of index.entries()) {\n                entryCount++;\n                console.log(`[GameResImporter]   Entry ${entryCount}: hash=0x${hash.toString(16).toUpperCase()}, offset=${entry.offset}, length=${entry.length}`);\n                if (entryCount >= 20) {\n                    console.log(`[GameResImporter]   ... and ${index.size - 20} more entries`);\n                    break;\n                }\n            }\n        }\n        const binkFileName = \"ra2ts_l.bik\";\n        const webmFileName = Engine.rfsSettings.menuVideoFileName;\n        const videoFileVariants = [\n            'ra2ts_l.bik', 'RA2TS_L.BIK', 'Ra2ts_l.bik', 'RA2TS_L.bik'\n        ];\n        console.log('[GameResImporter] Testing video file variants:');\n        let foundVideoFile = false;\n        let actualVideoFileName = binkFileName;\n        for (const variant of videoFileVariants) {\n            const exists = langMix.containsFile(variant);\n            console.log(`[GameResImporter]   \"${variant}\" exists: ${exists}`);\n            if (exists && !foundVideoFile) {\n                foundVideoFile = true;\n                actualVideoFileName = variant;\n            }\n        }\n        if (!foundVideoFile) {\n            console.warn(`Video file \"${binkFileName}\" not found in ${languageMixVirtualFile.filename}, skipping menu video import`);\n            return;\n        }\n        console.log(`[GameResImporter] Using video file: \"${actualVideoFileName}\"`);\n        const binkFileEntry = langMix.openFile(actualVideoFileName);\n        let webmBuffer: Uint8Array;\n        try {\n            webmBuffer = await new VideoConverter().convertBinkVideo(ffmpeg, binkFileEntry);\n        }\n        catch (e) {\n            this.sentry?.captureException(new Error(`Bink to WebM conversion failed for ${actualVideoFileName}`), { extra: { error: e } });\n            console.error(\"Bink video conversion failed, skipping menu video.\", e);\n            return;\n        }\n        const webmBlob = new Blob([webmBuffer as any], { type: \"video/webm\" });\n        const virtualWebmFile = VirtualFile.fromBytes(webmBuffer, webmFileName);\n        await targetRfsRootDir.writeFile(virtualWebmFile);\n    }\n    private async createFFmpeg(): Promise<FFmpeg> {\n        const ffmpegModule = await import(\"@ffmpeg/ffmpeg\");\n        const FFmpegClass = ffmpegModule.FFmpeg;\n        if (typeof FFmpegClass !== 'function') {\n            console.error('[GameResImporter] FFmpeg class is not available:', typeof FFmpegClass);\n            throw new Error('FFmpeg class is not available from @ffmpeg/ffmpeg module');\n        }\n        const ffmpeg = new FFmpegClass();\n        const originalDefine = (window as any).define;\n        (window as any).define = undefined;\n        try {\n            await ffmpeg.load();\n        }\n        finally {\n            (window as any).define = originalDefine;\n        }\n        return ffmpeg;\n    }\n    private async importSplashImage(ra2MixVirtualFile: VirtualFile, targetRfsRootDir: RealFileSystemDir): Promise<Blob | undefined> {\n        console.log('[GameResImporter] Starting splash image import from ra2.mix...');\n        const ra2Mix = new MixFile(ra2MixVirtualFile.stream as DataStream);\n        if (!ra2Mix.containsFile(\"local.mix\")) {\n            throw new GameResFileNotFoundError(\"local.mix\");\n        }\n        console.log('[GameResImporter] Found local.mix, opening...');\n        const localMixFile = ra2Mix.openFile(\"local.mix\");\n        const localMix = new MixFile(localMixFile.stream);\n        if (!localMix.containsFile(\"glsl.shp\")) {\n            throw new GameResFileNotFoundError(\"glsl.shp\");\n        }\n        if (!localMix.containsFile(\"gls.pal\")) {\n            throw new GameResFileNotFoundError(\"gls.pal\");\n        }\n        console.log('[GameResImporter] Found glsl.shp and gls.pal, extracting...');\n        const glslShpFile = localMix.openFile(\"glsl.shp\");\n        const glsPalFile = localMix.openFile(\"gls.pal\");\n        console.log('[GameResImporter] Parsing SHP and palette...');\n        const shpFile = new ShpFile(glslShpFile);\n        const palette = new Palette(glsPalFile);\n        console.log('[GameResImporter] Converting SHP to PNG...');\n        const pngBlob = await ImageUtils.convertShpToPng(shpFile, palette);\n        const splashImgFileName = Engine.rfsSettings.splashImgFileName;\n        console.log(`[GameResImporter] Creating file \"${splashImgFileName}\" for RFS...`);\n        let splashFile: File | undefined;\n        try {\n            splashFile = new File([pngBlob], splashImgFileName, { type: pngBlob.type });\n        }\n        catch (e) {\n            console.error('[GameResImporter] Failed to create splash image file. Skipping.', e);\n            this.sentry?.captureException(new Error(`Failed to create splash image file (type=${pngBlob.type})`), { extra: { error: e } });\n        }\n        if (splashFile) {\n            console.log(`[GameResImporter] Writing \"${splashImgFileName}\" to RFS...`);\n            const virtualSplashFile = VirtualFile.fromBytes(new Uint8Array(await splashFile.arrayBuffer()), splashImgFileName);\n            await targetRfsRootDir.writeFile(virtualSplashFile);\n            console.log(`[GameResImporter] ✅ Successfully wrote \"${splashImgFileName}\" to RFS`);\n        }\n        return pngBlob;\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/GameResSource.ts",
    "content": "export enum GameResSource {\n    Archive = 0,\n    Cdn = 1,\n    Local = 2\n}\n"
  },
  {
    "path": "src/engine/gameRes/VideoConverter.ts",
    "content": "import type { VirtualFile } from '../../data/vfs/VirtualFile';\nimport type { DataStream } from '../../data/DataStream';\nimport type { FFmpeg } from '@ffmpeg/ffmpeg';\nexport class VideoConverter {\n    async convertBinkVideo(ffmpeg: FFmpeg, binkFile: VirtualFile, outputFormat: \"webm\" | \"mp4\" = \"webm\"): Promise<Uint8Array> {\n        const inputFileName = binkFile.filename;\n        const outputFileName = inputFileName.replace(/\\.[^.]+$/, \"\") + \".\" + outputFormat;\n        const binkDataStream = binkFile.stream as DataStream;\n        const binkFileData = new Uint8Array(binkDataStream.buffer, binkDataStream.byteOffset, binkDataStream.byteLength);\n        await ffmpeg.writeFile(inputFileName, binkFileData);\n        if (outputFormat === \"webm\") {\n            await ffmpeg.exec([\n                \"-i\", inputFileName,\n                \"-vcodec\", \"libvpx\",\n                \"-crf\", \"10\",\n                \"-b:v\", \"2M\",\n                \"-an\",\n                outputFileName,\n            ]);\n        }\n        else if (outputFormat === \"mp4\") {\n            await ffmpeg.exec([\n                \"-i\", inputFileName,\n                \"-vcodec\", \"libx264\",\n                \"-crf\", \"25\",\n                \"-b:v\", \"2M\",\n                \"-an\",\n                outputFileName,\n            ]);\n        }\n        else {\n            await ffmpeg.deleteFile(inputFileName);\n            throw new Error(`Unsupported video output format: ${outputFormat}`);\n        }\n        const convertedData = await ffmpeg.readFile(outputFileName) as Uint8Array;\n        await ffmpeg.deleteFile(inputFileName);\n        await ffmpeg.deleteFile(outputFileName);\n        return convertedData;\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/browserFileSystemAccess.ts",
    "content": "import {\n    getOriginPrivateDirectory,\n    polyfillDataTransferItem,\n    showDirectoryPicker,\n    showOpenFilePicker,\n    showSaveFilePicker,\n    support,\n} from 'file-system-access';\nimport cache from 'file-system-access/lib/adapters/cache.js';\nimport indexeddb from 'file-system-access/lib/adapters/indexeddb.js';\nimport type { FileSystemAccessLib } from './FileSystemAccessLib';\n\nexport const browserFileSystemAccess: FileSystemAccessLib = {\n    support,\n    adapters: {\n        indexeddb,\n        cache,\n    },\n    getOriginPrivateDirectory,\n    async polyfillDataTransferItem() {\n        await polyfillDataTransferItem();\n    },\n    showDirectoryPicker,\n    showOpenFilePicker,\n    showSaveFilePicker,\n};\n"
  },
  {
    "path": "src/engine/gameRes/importError/ArchiveDownloadError.ts",
    "content": "export class ArchiveDownloadError extends Error {\n    public url: string;\n    public cause?: any;\n    constructor(url: string, message: string, options?: {\n        cause?: any;\n    }) {\n        super(message);\n        this.name = \"ArchiveDownloadError\";\n        this.url = url;\n        if (options?.cause) {\n            this.cause = options.cause;\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/importError/ArchiveExtractionError.ts",
    "content": "export class ArchiveExtractionError extends Error {\n    constructor(message: string, options?: ErrorOptions) {\n        super(message, options);\n        this.name = \"ArchiveExtractionError\";\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/importError/ChecksumError.ts",
    "content": "export class ChecksumError extends Error {\n    public fileName?: string;\n    public expectedChecksum?: string | string[];\n    public actualChecksum?: string;\n    constructor(message: string, fileName?: string, expectedChecksum?: string | string[], actualChecksum?: string) {\n        super(message);\n        this.name = \"ChecksumError\";\n        this.fileName = fileName;\n        this.expectedChecksum = expectedChecksum;\n        this.actualChecksum = actualChecksum;\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/importError/FileNotFoundError.ts",
    "content": "export class FileNotFoundError extends Error {\n    public fileName?: string;\n    constructor(messageOrFileName: string, fileName?: string) {\n        if (fileName) {\n            super(`Game resource file not found: ${fileName}. ${messageOrFileName}`);\n            this.fileName = fileName;\n        }\n        else {\n            super(messageOrFileName);\n            this.fileName = messageOrFileName;\n        }\n        this.name = \"GameResFileNotFoundError\";\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/importError/InvalidArchiveError.ts",
    "content": "export class InvalidArchiveError extends Error {\n    public cause?: any;\n    constructor(message: string, options?: {\n        cause?: any;\n    }) {\n        super(message);\n        this.name = \"InvalidArchiveError\";\n        if (options?.cause) {\n            this.cause = options.cause;\n        }\n        Object.setPrototypeOf(this, InvalidArchiveError.prototype);\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/importError/NoStorageError.ts",
    "content": "export class NoStorageError extends Error {\n    constructor(message: string = \"No available or functional storage adapters found.\") {\n        super(message);\n        this.name = \"NoStorageError\";\n    }\n}\n"
  },
  {
    "path": "src/engine/gameRes/importError/NoWebAssemblyError.ts",
    "content": "export class NoWebAssemblyError extends Error {\n    public cause?: any;\n    constructor(message: string, options?: {\n        cause?: any;\n    }) {\n        super(message);\n        this.name = \"NoWebAssemblyError\";\n        if (options?.cause) {\n            this.cause = options.cause;\n        }\n        Object.setPrototypeOf(this, NoWebAssemblyError.prototype);\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/BufferGeometryUtils.ts",
    "content": "import * as THREE from 'three';\nexport class BufferGeometryUtils {\n    static mergeVertices(geometry: THREE.BufferGeometry, tolerance: number = 1e-4): THREE.BufferGeometry {\n        tolerance = Math.max(tolerance, Number.EPSILON);\n        const hashToIndex: {\n            [key: string]: number;\n        } = {};\n        const indices = geometry.getIndex();\n        const positionAttribute = geometry.getAttribute(\"position\");\n        const vertexCount = (indices || positionAttribute).count;\n        let nextIndex = 0;\n        const attributeNames = Object.keys(geometry.attributes);\n        const newAttributes: {\n            [key: string]: number[];\n        } = {};\n        const morphAttributes: {\n            [key: string]: number[][];\n        } = {};\n        const newIndices: number[] = [];\n        const getters = [\n            (attr: THREE.BufferAttribute, index: number) => attr.getX(index),\n            (attr: THREE.BufferAttribute, index: number) => attr.getY(index),\n            (attr: THREE.BufferAttribute, index: number) => attr.getZ(index),\n            (attr: THREE.BufferAttribute, index: number) => attr.getW(index),\n        ];\n        for (let i = 0, l = attributeNames.length; i < l; i++) {\n            const name = attributeNames[i];\n            newAttributes[name] = [];\n            const morphAttribute = geometry.morphAttributes[name];\n            if (morphAttribute) {\n                morphAttributes[name] = new Array(morphAttribute.length).fill(undefined).map(() => []);\n            }\n        }\n        const decimalShift = Math.log10(1 / tolerance);\n        const decimalFactor = Math.pow(10, decimalShift);\n        const hashPrecision = Math.max(10000, decimalFactor);\n        for (let i = 0; i < vertexCount; i++) {\n            const index = indices ? indices.getX(i) : i;\n            let hash = \"\";\n            for (let a = 0, l = attributeNames.length; a < l; a++) {\n                const name = attributeNames[a];\n                const attribute = geometry.getAttribute(name) as THREE.BufferAttribute;\n                const itemSize = attribute.itemSize;\n                for (let j = 0; j < itemSize; j++) {\n                    hash += ~~(getters[j](attribute, index) * hashPrecision) + \",\";\n                }\n            }\n            if (hash in hashToIndex) {\n                newIndices.push(hashToIndex[hash]);\n            }\n            else {\n                for (let a = 0, l = attributeNames.length; a < l; a++) {\n                    const name = attributeNames[a];\n                    const attribute = geometry.getAttribute(name) as THREE.BufferAttribute;\n                    const morphAttribute = geometry.morphAttributes[name];\n                    const itemSize = attribute.itemSize;\n                    const newAttributeArray = newAttributes[name];\n                    const newMorphAttributeArrays = morphAttributes[name];\n                    for (let j = 0; j < itemSize; j++) {\n                        const getter = getters[j];\n                        newAttributeArray.push(getter(attribute, index));\n                        if (morphAttribute) {\n                            for (let k = 0, kl = morphAttribute.length; k < kl; k++) {\n                                newMorphAttributeArrays[k].push(getter(morphAttribute[k] as THREE.BufferAttribute, index));\n                            }\n                        }\n                    }\n                }\n                hashToIndex[hash] = nextIndex;\n                newIndices.push(nextIndex);\n                nextIndex++;\n            }\n        }\n        const result = geometry.clone();\n        for (let i = 0, l = attributeNames.length; i < l; i++) {\n            const name = attributeNames[i];\n            const originalAttribute = geometry.getAttribute(name);\n            const newArray = new (originalAttribute.array.constructor as any)(newAttributes[name]);\n            const newAttribute = new THREE.BufferAttribute(newArray, originalAttribute.itemSize, originalAttribute.normalized);\n            result.setAttribute(name, newAttribute);\n            if (name in morphAttributes) {\n                for (let j = 0; j < morphAttributes[name].length; j++) {\n                    const originalMorphAttribute = geometry.morphAttributes[name][j];\n                    const newMorphArray = new (originalMorphAttribute.array.constructor as any)(morphAttributes[name][j]);\n                    const newMorphAttribute = new THREE.BufferAttribute(newMorphArray, originalMorphAttribute.itemSize, originalMorphAttribute.normalized);\n                    result.morphAttributes[name][j] = newMorphAttribute;\n                }\n            }\n        }\n        result.setIndex(new THREE.BufferAttribute(new Uint32Array(newIndices), 1));\n        return result;\n    }\n    static mergeBufferGeometries(geometries: THREE.BufferGeometry[], useGroups: boolean = false): THREE.BufferGeometry {\n        const isIndexed = geometries[0].index !== null;\n        const attributesUsed = new Set(Object.keys(geometries[0].attributes));\n        const mergedAttributes: {\n            [key: string]: THREE.BufferAttribute[];\n        } = {};\n        const mergedGeometry = new THREE.BufferGeometry();\n        let offset = 0;\n        for (let i = 0; i < geometries.length; ++i) {\n            const geometry = geometries[i];\n            let attributeCount = 0;\n            if (isIndexed !== (geometry.index !== null)) {\n                throw new Error(\"mergeBufferGeometries() failed with geometry at index \" + i +\n                    \". All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.\");\n            }\n            if (Object.keys(geometry.morphAttributes).length) {\n                throw new Error(\"mergeBufferGeometries() failed with geometry at index \" + i +\n                    \". Morph attributes are not supported\");\n            }\n            for (const name in geometry.attributes) {\n                if (!attributesUsed.has(name)) {\n                    throw new Error(\"mergeBufferGeometries() failed with geometry at index \" + i +\n                        '. All geometries must have compatible attributes; make sure \"' + name +\n                        '\" attribute exists among all geometries, or in none of them.');\n                }\n                if (mergedAttributes[name] === undefined) {\n                    mergedAttributes[name] = [];\n                }\n                mergedAttributes[name].push(geometry.attributes[name] as THREE.BufferAttribute);\n                attributeCount++;\n            }\n            if (attributeCount !== attributesUsed.size) {\n                throw new Error(\"mergeBufferGeometries() failed with geometry at index \" + i +\n                    \". Make sure all geometries have the same number of attributes.\");\n            }\n            if (useGroups) {\n                let count: number;\n                if (isIndexed) {\n                    count = geometry.index!.count;\n                }\n                else {\n                    if (geometry.attributes.position === undefined) {\n                        throw new Error(\"mergeBufferGeometries() failed with geometry at index \" + i +\n                            \". The geometry must have either an index or a position attribute\");\n                    }\n                    count = geometry.attributes.position.count;\n                }\n                mergedGeometry.addGroup(offset, count, i);\n                offset += count;\n            }\n        }\n        if (isIndexed) {\n            let indexOffset = 0;\n            const mergedIndex: number[] = [];\n            for (let i = 0; i < geometries.length; ++i) {\n                const index = geometries[i].index!;\n                for (let j = 0; j < index.count; ++j) {\n                    mergedIndex.push(index.getX(j) + indexOffset);\n                }\n                indexOffset += geometries[i].attributes.position.count;\n            }\n            mergedGeometry.setIndex(new THREE.BufferAttribute(new (mergedIndex.length > 65535 ? Uint32Array : Uint16Array)(mergedIndex), 1));\n        }\n        for (const name in mergedAttributes) {\n            const mergedAttribute = this.mergeBufferAttributes(mergedAttributes[name]);\n            if (!mergedAttribute) {\n                throw new Error(\"mergeBufferGeometries() failed while trying to merge the \" + name + \" attribute.\");\n            }\n            mergedGeometry.setAttribute(name, mergedAttribute);\n        }\n        return mergedGeometry;\n    }\n    static mergeBufferAttributes(attributes: THREE.BufferAttribute[]): THREE.BufferAttribute | null {\n        let arrayType: any;\n        let itemSize: number;\n        let normalized: boolean;\n        let arrayLength = 0;\n        for (let i = 0; i < attributes.length; ++i) {\n            const attribute = attributes[i];\n            if ((attribute as any).isInterleavedBufferAttribute) {\n                throw new Error(\"mergeBufferAttributes() failed. InterleavedBufferAttributes are not supported.\");\n            }\n            if (arrayType === undefined) {\n                arrayType = attribute.array.constructor;\n            }\n            if (arrayType !== attribute.array.constructor) {\n                throw new Error(\"mergeBufferAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.\");\n            }\n            if (itemSize === undefined) {\n                itemSize = attribute.itemSize;\n            }\n            if (itemSize !== attribute.itemSize) {\n                throw new Error(\"mergeBufferAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.\");\n            }\n            if (normalized === undefined) {\n                normalized = attribute.normalized;\n            }\n            if (normalized !== attribute.normalized) {\n                throw new Error(\"mergeBufferAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.\");\n            }\n            arrayLength += attribute.array.length;\n        }\n        const mergedArray = new arrayType(arrayLength);\n        let offset = 0;\n        for (let i = 0; i < attributes.length; ++i) {\n            mergedArray.set(attributes[i].array, offset);\n            offset += attributes[i].array.length;\n        }\n        return new THREE.BufferAttribute(mergedArray, itemSize!, normalized!);\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/Camera.ts",
    "content": "export class Camera {\n    [key: string]: any;\n    top: number;\n    right: number;\n}\n"
  },
  {
    "path": "src/engine/gfx/CanvasUtils.ts",
    "content": "import { Palette } from '../../data/Palette';\ninterface DrawTextOptions {\n    color?: string;\n    backgroundColor?: string;\n    outlineColor?: string;\n    outlineWidth?: number;\n    fontSize?: number;\n    fontFamily?: string;\n    fontWeight?: string;\n    borderColor?: string;\n    borderWidth?: number;\n    paddingTop?: number;\n    paddingBottom?: number;\n    paddingLeft?: number;\n    paddingRight?: number;\n    textAlign?: CanvasTextAlign;\n    width?: number;\n    height?: number;\n    autoEnlargeCanvas?: boolean;\n}\ninterface TextRect {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\nexport class CanvasUtils {\n    static canvasFromRgbaImageData(data: Uint8Array, width: number, height: number): HTMLCanvasElement {\n        const canvas = document.createElement('canvas');\n        const ctx = canvas.getContext('2d');\n        if (!ctx) {\n            throw new Error(\"Couldn't acquire canvas 2d context\");\n        }\n        canvas.width = width;\n        canvas.height = height;\n        const imageData = ctx.createImageData(width, height);\n        let dataIndex = 0;\n        for (let i = 0; i < data.length; i += 4) {\n            imageData.data[dataIndex] = data[i];\n            imageData.data[dataIndex + 1] = data[i + 1];\n            imageData.data[dataIndex + 2] = data[i + 2];\n            imageData.data[dataIndex + 3] = data[i + 3];\n            dataIndex += 4;\n        }\n        ctx.putImageData(imageData, 0, 0);\n        return canvas;\n    }\n    static canvasFromRgbImageData(data: Uint8Array, width: number, height: number): HTMLCanvasElement {\n        const canvas = document.createElement('canvas');\n        const ctx = canvas.getContext('2d');\n        if (!ctx) {\n            throw new Error(\"Couldn't acquire canvas 2d context\");\n        }\n        canvas.width = width;\n        canvas.height = height;\n        const imageData = ctx.createImageData(width, height);\n        let dataIndex = 0;\n        for (let i = 0; i < data.length; i += 3) {\n            imageData.data[dataIndex] = data[i];\n            imageData.data[dataIndex + 1] = data[i + 1];\n            imageData.data[dataIndex + 2] = data[i + 2];\n            imageData.data[dataIndex + 3] = 255;\n            dataIndex += 4;\n        }\n        ctx.putImageData(imageData, 0, 0);\n        return canvas;\n    }\n    static canvasFromIndexedImageData(data: Uint8Array, width: number, height: number, palette: any): HTMLCanvasElement {\n        const canvas = document.createElement('canvas');\n        const ctx = canvas.getContext('2d');\n        if (!ctx) {\n            throw new Error(\"Couldn't acquire canvas 2d context\");\n        }\n        canvas.width = width;\n        canvas.height = height;\n        const imageData = ctx.createImageData(width, height);\n        const colors = palette.colors;\n        let dataIndex = 0;\n        for (let i = 0; i < data.length; i++) {\n            const colorIndex = data[i];\n            const color = colors[colorIndex] || { r: 0, g: 0, b: 0 };\n            imageData.data[dataIndex] = color.r;\n            imageData.data[dataIndex + 1] = color.g;\n            imageData.data[dataIndex + 2] = color.b;\n            imageData.data[dataIndex + 3] = colorIndex ? 255 : 0;\n            dataIndex += 4;\n        }\n        ctx.putImageData(imageData, 0, 0);\n        return canvas;\n    }\n    static async canvasToBlob(canvas: HTMLCanvasElement, mimeType: string = \"image/png\"): Promise<Blob> {\n        let blob = await new Promise<Blob | null>((resolve) => {\n            try {\n                canvas.toBlob((blob) => {\n                    resolve(blob);\n                });\n            }\n            catch (error) {\n                console.error(error);\n                resolve(null);\n            }\n        });\n        if (!blob) {\n            console.warn('Failed to convert canvas to blob. Falling back to dataURL generation.');\n            try {\n                blob = this.dataUrlToBlob(canvas.toDataURL());\n            }\n            catch (error) {\n                throw new Error(`Failed to generate image from canvas using fallback ${error}`);\n            }\n        }\n        return blob;\n    }\n    static dataUrlToBlob(dataUrl: string): Blob {\n        const match = dataUrl.match(/^data:((.*?)(;charset=.*?)?)(;base64)?,/);\n        if (!match) {\n            throw new Error('invalid dataURI');\n        }\n        const mimeType = match[2] ? match[1] : 'text/plain' + (match[3] || ';charset=utf-8');\n        const isBase64 = !!match[4];\n        const data = dataUrl.slice(match[0].length);\n        const bytes = (isBase64 ? atob : decodeURIComponent)(data);\n        const byteArray: number[] = [];\n        for (let i = 0; i < bytes.length; i++) {\n            byteArray.push(bytes.charCodeAt(i));\n        }\n        return new Blob([new Uint8Array(byteArray)], { type: mimeType });\n    }\n    static drawText(ctx: CanvasRenderingContext2D, text: string, x: number = 0, y: number = 0, options: DrawTextOptions = {}): TextRect {\n        const { color = \"white\", backgroundColor, outlineColor, outlineWidth, fontSize = 10, fontFamily = \"Arial, sans-serif\", fontWeight = \"normal\", borderColor, borderWidth = 0, paddingTop = 0, paddingBottom = 0, paddingLeft = 0, paddingRight = 0, textAlign = \"left\", width: explicitWidth, height: explicitHeight, autoEnlargeCanvas = false, } = options;\n        const fontStyle = `${fontWeight} ${fontSize}px ${fontFamily}`;\n        ctx.font = fontStyle;\n        const textMetrics = ctx.measureText(text);\n        const capAHeightMetrics = ctx.measureText(\"A\");\n        const textHeightEstimate = capAHeightMetrics.actualBoundingBoxAscent + capAHeightMetrics.actualBoundingBoxDescent;\n        const measuredTextWidth = Math.ceil(Math.max(textMetrics.width, Math.abs(textMetrics.actualBoundingBoxLeft || 0) + Math.abs(textMetrics.actualBoundingBoxRight || 0)));\n        const boxWidth = explicitWidth ?? (measuredTextWidth + paddingLeft + paddingRight + 2 * borderWidth);\n        const boxHeight = explicitHeight ?? (textHeightEstimate + paddingTop + paddingBottom + 2 * borderWidth);\n        let drawX = x;\n        if (textAlign === \"right\" && explicitWidth === undefined) {\n            drawX = ctx.canvas.width - boxWidth - x;\n        }\n        else if (textAlign === \"center\" && explicitWidth === undefined) {\n            drawX = x - boxWidth / 2;\n        }\n        const rect: TextRect = {\n            x: drawX,\n            y: y,\n            width: boxWidth,\n            height: boxHeight,\n        };\n        if (autoEnlargeCanvas) {\n            let needsResize = false;\n            let newCanvasWidth = ctx.canvas.width;\n            let newCanvasHeight = ctx.canvas.height;\n            if (rect.x + rect.width > newCanvasWidth) {\n                newCanvasWidth = rect.x + rect.width;\n                needsResize = true;\n            }\n            if (rect.y + rect.height > newCanvasHeight) {\n                newCanvasHeight = rect.y + rect.height;\n                needsResize = true;\n            }\n            if (needsResize) {\n                const currentContent = (ctx.canvas.width > 0 && ctx.canvas.height > 0) ? ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height) : undefined;\n                ctx.canvas.width = newCanvasWidth;\n                ctx.canvas.height = newCanvasHeight;\n                if (currentContent)\n                    ctx.putImageData(currentContent, 0, 0);\n                ctx.font = fontStyle;\n            }\n        }\n        if (backgroundColor) {\n            ctx.fillStyle = backgroundColor;\n            ctx.fillRect(rect.x, rect.y, rect.width, rect.height);\n        }\n        if (borderColor && borderWidth > 0) {\n            ctx.strokeStyle = borderColor;\n            ctx.lineWidth = borderWidth;\n            ctx.strokeRect(rect.x + borderWidth / 2, rect.y + borderWidth / 2, rect.width - borderWidth, rect.height - borderWidth);\n        }\n        ctx.fillStyle = color;\n        ctx.font = fontStyle;\n        ctx.textAlign = textAlign;\n        let textDrawX = rect.x + paddingLeft + borderWidth;\n        if (textAlign === 'center') {\n            textDrawX = rect.x + rect.width / 2;\n        }\n        else if (textAlign === 'right') {\n            textDrawX = rect.x + rect.width - paddingRight - borderWidth;\n        }\n        const textDrawY = rect.y + paddingTop + borderWidth + (textMetrics.actualBoundingBoxAscent || fontSize * 0.8);\n        if (outlineColor && outlineWidth && outlineWidth > 0) {\n            ctx.strokeStyle = outlineColor;\n            ctx.lineWidth = outlineWidth * 2;\n            ctx.strokeText(text, textDrawX, textDrawY, explicitWidth ? rect.width - paddingLeft - paddingRight - 2 * borderWidth : undefined);\n        }\n        ctx.fillText(text, textDrawX, textDrawY, explicitWidth ? rect.width - paddingLeft - paddingRight - 2 * borderWidth : undefined);\n        return rect;\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/DebugUtils.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport { IndexedBitmap } from '@/data/Bitmap';\nimport * as THREE from 'three';\nexport class DebugUtils {\n    static createWireframe(size: {\n        width: number;\n        height: number;\n    }, height: number): THREE.Mesh {\n        return new THREE.Mesh(this.createBoxGeometry(size, height), new THREE.MeshBasicMaterial({ wireframe: true }));\n    }\n    static createBoxGeometry(size: {\n        width: number;\n        height: number;\n    }, height: number, center: boolean = false): THREE.BoxGeometry {\n        const tileSize = Coords.getWorldTileSize();\n        const width = size.width * tileSize;\n        const depth = size.height * tileSize;\n        const boxHeight = Coords.tileHeightToWorld(height);\n        const geometry = new THREE.BoxGeometry(width, boxHeight, depth);\n        if (center) {\n            geometry.translate(0, boxHeight / 2, 0);\n        }\n        else {\n            geometry.translate(width / 2, boxHeight / 2, depth / 2);\n        }\n        return geometry;\n    }\n    static createIndexedCheckerTex(color1: number, color2: number): THREE.DataTexture {\n        const bitmap = new IndexedBitmap(64, 64, new Uint8Array(4096).fill(color1));\n        for (let y = 0; y < 32; y++) {\n            for (let x = 0; x < 32; x++) {\n                bitmap.data[x + 64 * y] = color2;\n                bitmap.data[x + 32 + 64 * (y + 32)] = color2;\n            }\n        }\n        const texture = new THREE.DataTexture(bitmap.data, 64, 64, THREE.RedFormat);\n        texture.needsUpdate = true;\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        (texture as THREE.Texture & {\n            colorSpace: THREE.ColorSpace;\n        }).colorSpace = THREE.NoColorSpace;\n        return texture;\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/FrustumCuller.ts",
    "content": "import * as THREE from 'three';\nimport { Octree } from '@brakebein/threeoctree';\nexport class FrustumCuller {\n    cull<T extends THREE.Mesh = THREE.Mesh>(octree: Octree<T>, frustum: THREE.Frustum): any[] {\n        const visibleNodes: any[] = [];\n        const traverse = (node: any): void => {\n            const BOX_KEY: unique symbol = Symbol.for('__ra2web_box');\n            let box = (node as any)[BOX_KEY] as THREE.Box3 | undefined;\n            if (!box) {\n                const r = node.radius + (node.overlap ?? 0);\n                const pos = node.position;\n                box = new THREE.Box3(new THREE.Vector3(pos.x - r, pos.y - r, pos.z - r), new THREE.Vector3(pos.x + r, pos.y + r, pos.z + r));\n                (node as any)[BOX_KEY] = box;\n            }\n            if (frustum.intersectsBox(box)) {\n                (node as any).visible = true;\n                if (Array.isArray(node.nodesIndices) && node.nodesIndices.length > 0) {\n                    for (const index of node.nodesIndices) {\n                        const child = node.nodesByIndex[index];\n                        if (child) {\n                            traverse(child);\n                        }\n                    }\n                }\n                visibleNodes.push(node);\n            }\n            else {\n                (node as any).visible = false;\n            }\n        };\n        traverse(octree.root);\n        return visibleNodes;\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/GrowingPacker.ts",
    "content": "export interface GrowingPackerBlock {\n    w: number;\n    h: number;\n    fit?: GrowingPackerNode;\n}\nexport interface GrowingPackerNode {\n    x: number;\n    y: number;\n    w: number;\n    h: number;\n    used?: boolean;\n    right?: GrowingPackerNode;\n    down?: GrowingPackerNode;\n}\nexport class GrowingPacker {\n    root!: GrowingPackerNode;\n    fit(blocks: GrowingPackerBlock[]): void {\n        const width = blocks.length > 0 ? blocks[0].w : 0;\n        const height = blocks.length > 0 ? blocks[0].h : 0;\n        this.root = { x: 0, y: 0, w: width, h: height };\n        for (const block of blocks) {\n            const node = this.findNode(this.root, block.w, block.h);\n            block.fit = node ? this.splitNode(node, block.w, block.h) : this.growNode(block.w, block.h);\n        }\n    }\n    private findNode(root: GrowingPackerNode | undefined, width: number, height: number): GrowingPackerNode | undefined {\n        if (!root) {\n            return undefined;\n        }\n        if (root.used) {\n            return this.findNode(root.right, width, height) ?? this.findNode(root.down, width, height);\n        }\n        if (width <= root.w && height <= root.h) {\n            return root;\n        }\n        return undefined;\n    }\n    private splitNode(node: GrowingPackerNode, width: number, height: number): GrowingPackerNode {\n        node.used = true;\n        node.down = { x: node.x, y: node.y + height, w: node.w, h: node.h - height };\n        node.right = { x: node.x + width, y: node.y, w: node.w - width, h: height };\n        return node;\n    }\n    private growNode(width: number, height: number): GrowingPackerNode | undefined {\n        const canGrowDown = width <= this.root.w;\n        const canGrowRight = height <= this.root.h;\n        const shouldGrowRight = canGrowRight && this.root.h >= this.root.w + width;\n        const shouldGrowDown = canGrowDown && this.root.w >= this.root.h + height;\n        if (shouldGrowRight) {\n            return this.growRight(width, height);\n        }\n        if (shouldGrowDown) {\n            return this.growDown(width, height);\n        }\n        if (canGrowRight) {\n            return this.growRight(width, height);\n        }\n        if (canGrowDown) {\n            return this.growDown(width, height);\n        }\n        return undefined;\n    }\n    private growRight(width: number, height: number): GrowingPackerNode | undefined {\n        this.root = {\n            used: true,\n            x: 0,\n            y: 0,\n            w: this.root.w + width,\n            h: this.root.h,\n            down: this.root,\n            right: { x: this.root.w, y: 0, w: width, h: this.root.h },\n        };\n        const node = this.findNode(this.root, width, height);\n        return node ? this.splitNode(node, width, height) : undefined;\n    }\n    private growDown(width: number, height: number): GrowingPackerNode | undefined {\n        this.root = {\n            used: true,\n            x: 0,\n            y: 0,\n            w: this.root.w,\n            h: this.root.h + height,\n            down: { x: 0, y: this.root.h, w: this.root.w, h: height },\n            right: this.root,\n        };\n        const node = this.findNode(this.root, width, height);\n        return node ? this.splitNode(node, width, height) : undefined;\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/ImageUtils.ts",
    "content": "import type { ShpFile } from '../../data/ShpFile';\nimport type { Palette } from '../../data/Palette';\nimport { IndexedBitmap } from '../../data/Bitmap';\nimport { CanvasUtils } from './CanvasUtils';\nexport class ImageUtils {\n    static async convertShpToPng(shpFile: ShpFile, palette: Palette): Promise<Blob> {\n        const canvas = this.convertShpToCanvas(shpFile, palette);\n        return await CanvasUtils.canvasToBlob(canvas);\n    }\n    static convertShpToBitmap(shpFile: ShpFile, palette: Palette, forceSquare: boolean = false): IndexedBitmap {\n        let offsetX = 0;\n        let offsetY = 0;\n        let finalWidth = shpFile.width;\n        let finalHeight = shpFile.height;\n        if (finalWidth !== finalHeight && forceSquare) {\n            offsetX = finalWidth > finalHeight ? 0 : Math.floor((finalHeight - finalWidth) / 2);\n            offsetY = finalWidth > finalHeight ? Math.floor((finalWidth - finalHeight) / 2) : 0;\n            finalWidth = finalHeight = Math.max(finalWidth, finalHeight);\n        }\n        const bitmap = new IndexedBitmap(shpFile.numImages * finalWidth, finalHeight);\n        for (let i = 0; i < shpFile.numImages; i++) {\n            const image = shpFile.getImage(i);\n            const imageBitmap = new IndexedBitmap(image.width, image.height, image.imageData);\n            bitmap.drawIndexedImage(imageBitmap, i * finalWidth + image.x + offsetX, image.y + offsetY);\n        }\n        return bitmap;\n    }\n    static convertShpToCanvas(shpFile: ShpFile, palette: Palette, forceSquare: boolean = false): HTMLCanvasElement {\n        const bitmap = this.convertShpToBitmap(shpFile, palette, forceSquare);\n        return CanvasUtils.canvasFromIndexedImageData(bitmap.data, bitmap.width, bitmap.height, palette);\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/MathUtils.ts",
    "content": "import * as THREE from 'three';\nexport class MathUtils {\n    static rotateObjectAboutPoint(object: THREE.Object3D, point: THREE.Vector3, axis: THREE.Vector3, angle: number, useWorldSpace: boolean = false): void {\n        if (useWorldSpace && object.parent) {\n            object.parent.localToWorld(object.position);\n        }\n        object.position.sub(point);\n        object.position.applyAxisAngle(axis, angle);\n        object.position.add(point);\n        if (useWorldSpace && object.parent) {\n            object.parent.worldToLocal(object.position);\n        }\n        object.rotateOnAxis(axis, angle);\n    }\n    static translateTowardsCamera(object: THREE.Object3D, camera: THREE.Camera, distance: number): void {\n        const quaternion = new THREE.Quaternion().setFromEuler(camera.rotation);\n        object.setRotationFromQuaternion(quaternion);\n        object.translateZ(distance * Math.cos(camera.rotation.y));\n        object.setRotationFromEuler(new THREE.Euler(0, 0, 0));\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/OctreeContainer.ts",
    "content": "import { RenderableContainer } from './RenderableContainer';\nimport { FrustumCuller } from './FrustumCuller';\nimport { Coords } from '@/game/Coords';\nimport * as THREE from 'three';\nimport { Octree } from '@brakebein/threeoctree';\nconst CAMERA_PADDING = 3;\nlet cameraClone: THREE.OrthographicCamera | THREE.PerspectiveCamera;\nexport class OctreeContainer extends RenderableContainer {\n    autoCull: boolean;\n    private lastCameraPosition: THREE.Vector3;\n    private tree: Octree;\n    private frustumCuller: FrustumCuller;\n    private camera: THREE.Camera;\n    static factory(camera: THREE.Camera): OctreeContainer {\n        const perspCamera = camera as THREE.PerspectiveCamera;\n        const { near, far } = perspCamera;\n        const octree = new Octree({\n            undeferred: false,\n            depthMax: Math.ceil(Math.log2((2 * (far - near)) / 128)),\n            objectsThreshold: 10,\n            overlapPct: 0.15\n        });\n        const frustumCuller = new FrustumCuller();\n        return new OctreeContainer(octree, frustumCuller, camera);\n    }\n    constructor(tree: Octree, frustumCuller: FrustumCuller, camera: THREE.Camera) {\n        const dummyObject = new THREE.Object3D();\n        dummyObject.name = 'octree-container';\n        super(dummyObject);\n        this.autoCull = true;\n        this.lastCameraPosition = new THREE.Vector3();\n        this.tree = tree;\n        this.frustumCuller = frustumCuller;\n        this.camera = camera;\n    }\n    update(deltaTime: number): void {\n        super.update(deltaTime);\n        if (this.autoCull) {\n            this.cullChildren();\n        }\n    }\n    cullChildren(): void {\n        if (!this.camera.position.equals(this.lastCameraPosition)) {\n            this.lastCameraPosition.copy(this.camera.position);\n            let matrix = this.computeProjectionMatrix();\n            this.camera.updateMatrixWorld(false);\n            this.camera.matrixWorldInverse.copy(this.camera.matrixWorld).invert();\n            matrix = new THREE.Matrix4().multiplyMatrices(matrix, this.camera.matrixWorldInverse);\n            const frustum = new THREE.Frustum();\n            frustum.setFromProjectionMatrix(matrix);\n            this.frustumCuller.cull(this.tree, frustum);\n        }\n    }\n    computeProjectionMatrix(): THREE.Matrix4 {\n        if (!cameraClone) {\n            cameraClone = this.camera.clone() as THREE.OrthographicCamera | THREE.PerspectiveCamera;\n        }\n        else {\n            cameraClone.copy(this.camera as any);\n        }\n        if ('top' in cameraClone && 'bottom' in cameraClone && 'left' in cameraClone && 'right' in cameraClone) {\n            const orthoCamera = cameraClone as THREE.OrthographicCamera;\n            orthoCamera.top += CAMERA_PADDING * Coords.LEPTONS_PER_TILE * Coords.COS_ISO_CAMERA_BETA;\n            orthoCamera.bottom -= CAMERA_PADDING * Coords.LEPTONS_PER_TILE * Coords.COS_ISO_CAMERA_BETA;\n            orthoCamera.left -= CAMERA_PADDING * (2 * Coords.LEPTONS_PER_TILE) * Coords.COS_ISO_CAMERA_BETA;\n            orthoCamera.right += CAMERA_PADDING * (2 * Coords.LEPTONS_PER_TILE) * Coords.COS_ISO_CAMERA_BETA;\n        }\n        cameraClone.updateProjectionMatrix();\n        return cameraClone.projectionMatrix;\n    }\n    updateChild(child: any): void {\n        const obj3D = child.get3DObject();\n        if (obj3D && obj3D.parent) {\n            this.tree.remove(obj3D);\n            this.tree.add(obj3D);\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/OverlayUtils.ts",
    "content": "import * as THREE from 'three';\nimport { CanvasUtils } from './CanvasUtils';\nexport class OverlayUtils {\n    static createGroundCircle(radius: number, color: THREE.ColorRepresentation): THREE.Line {\n        const material = new THREE.LineBasicMaterial({\n            color: color,\n            transparent: true,\n            depthTest: false,\n            depthWrite: false,\n        });\n        const segments = 64;\n        const curve = new THREE.EllipseCurve(0, 0, radius, radius, 0, Math.PI * 2, false, 0);\n        const points2D = curve.getPoints(segments);\n        const points3D = points2D.map((p) => new THREE.Vector3(p.x, p.y, 0));\n        points3D.push(points3D[0].clone());\n        const geometry = new THREE.BufferGeometry();\n        geometry.setFromPoints(points3D);\n        const line = new THREE.Line(geometry, material);\n        line.rotation.x = Math.PI / 2;\n        line.renderOrder = 1000000;\n        return line;\n    }\n    static createTextBox(text: string, options: any): HTMLCanvasElement {\n        const canvas = document.createElement('canvas');\n        canvas.width = canvas.height = 0;\n        const context = canvas.getContext('2d', {\n            alpha: !options.backgroundColor || !!options.backgroundColor.match(/^rgba/),\n        });\n        CanvasUtils.drawText(context, text, 0, 0, {\n            ...options,\n            autoEnlargeCanvas: true,\n        });\n        return canvas;\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/Renderable.ts",
    "content": "export class Renderable {\n    [key: string]: any;\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/RenderableContainer.ts",
    "content": "import * as THREE from 'three';\nexport interface Renderable {\n    create3DObject(): void;\n    get3DObject(): THREE.Object3D | undefined;\n    update(deltaTime: number, ...args: any[]): void;\n    destroy?(): void;\n}\nexport class RenderableContainer {\n    private children: Set<Renderable> = new Set();\n    private renderQueue: Renderable[] = [];\n    private container?: THREE.Object3D;\n    constructor(container?: THREE.Object3D) {\n        if (container) {\n            this.set3DObject(container);\n        }\n    }\n    set3DObject(container: THREE.Object3D): void {\n        this.container = container;\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.container;\n    }\n    getChildren(): Renderable[] {\n        return [...this.children];\n    }\n    add(...objects: Renderable[]): void {\n        for (const obj of objects) {\n            if (!this.children.has(obj)) {\n                this.children.add(obj);\n                this.renderQueue.push(obj);\n            }\n        }\n    }\n    remove(...objects: Renderable[]): void {\n        for (const obj of objects) {\n            if (this.children.has(obj)) {\n                this.children.delete(obj);\n                const queueIndex = this.renderQueue.indexOf(obj);\n                if (queueIndex === -1) {\n                    const obj3d = obj.get3DObject();\n                    if (obj3d && obj3d.parent && this.get3DObject()) {\n                        this.get3DObject()!.remove(obj3d);\n                    }\n                }\n                else {\n                    this.renderQueue.splice(queueIndex, 1);\n                }\n            }\n        }\n    }\n    removeAll(): void {\n        this.remove(...this.children);\n    }\n    processRenderQueue(): void {\n        if (!this.get3DObject()) {\n            throw new Error('A THREE.Object3D must be passed in the constructor or using the setter.');\n        }\n        let obj: Renderable | undefined;\n        while ((obj = this.renderQueue.shift())) {\n            obj.create3DObject();\n            const obj3d = obj.get3DObject();\n            if (obj3d) {\n                this.get3DObject()!.add(obj3d);\n            }\n        }\n    }\n    create3DObject(): void {\n        this.processRenderQueue();\n    }\n    update(deltaTime: number, ...args: any[]): void {\n        if (this.renderQueue.length) {\n            this.processRenderQueue();\n        }\n        for (const child of this.children) {\n            if (this.renderQueue.length) {\n                this.processRenderQueue();\n            }\n            child.update(deltaTime, ...args);\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/Renderer.ts",
    "content": "import * as THREE from 'three';\nimport Stats from 'stats.js';\nimport { EventDispatcher } from '../../util/event';\nimport { RendererError } from './RendererError';\nexport class Renderer {\n    private width: number;\n    private height: number;\n    private renderer!: THREE.WebGLRenderer;\n    private scenes: Set<any> = new Set();\n    private isContextLost: boolean = false;\n    private stats?: Stats;\n    private _onFrame = new EventDispatcher<string, number>();\n    constructor(width: number, height: number) {\n        this.width = width;\n        this.height = height;\n    }\n    get onFrame() {\n        return this._onFrame.asEvent();\n    }\n    getCanvas(): HTMLCanvasElement {\n        return this.renderer.domElement;\n    }\n    getStats(): Stats | undefined {\n        return this.stats;\n    }\n    supportsInstancing(): boolean {\n        if (!this.renderer) {\n            throw new Error('Renderer not yet initialized');\n        }\n        return !!this.renderer.extensions.get('ANGLE_instanced_arrays');\n    }\n    initStats(container: HTMLElement): void {\n        if (!this.stats) {\n            this.stats = new Stats();\n            this.stats.showPanel(0);\n            this.stats.dom.style.top = 'auto';\n            this.stats.dom.style.bottom = '0px';\n            this.stats.dom.classList.add('stats-layer');\n            container.appendChild(this.stats.dom);\n        }\n    }\n    destroyStats(): void {\n        if (this.stats) {\n            if (this.stats.dom.parentNode) {\n                this.stats.dom.parentNode.removeChild(this.stats.dom);\n            }\n            this.stats = undefined;\n        }\n    }\n    init(container: HTMLElement): void {\n        const renderer = this.createGlRenderer();\n        container.appendChild(renderer.domElement);\n        renderer.domElement.addEventListener('contextmenu', (event) => {\n            event.preventDefault();\n        });\n        renderer.domElement.addEventListener('mousedown', (event) => {\n            event.preventDefault();\n        });\n        renderer.domElement.addEventListener('wheel', (event) => {\n            event.stopPropagation();\n        }, { passive: true });\n        renderer.domElement.addEventListener('webglcontextlost', this.handleContextLost);\n        renderer.domElement.addEventListener('webglcontextrestored', this.handleContextRestored);\n        this.renderer = renderer;\n    }\n    createGlRenderer(canvas?: HTMLCanvasElement): THREE.WebGLRenderer {\n        let renderer: THREE.WebGLRenderer;\n        try {\n            renderer = new THREE.WebGLRenderer({\n                canvas: canvas,\n                preserveDrawingBuffer: true,\n                powerPreference: 'high-performance',\n            });\n        }\n        catch (error) {\n            throw new RendererError('Failed to initialize WebGL renderer');\n        }\n        renderer.setSize(this.width, this.height);\n        renderer.autoClear = false;\n        renderer.autoClearDepth = false;\n        renderer.shadowMap.enabled = true;\n        renderer.localClippingEnabled = true;\n        renderer.toneMapping = THREE.NoToneMapping;\n        renderer.outputColorSpace = (THREE as any).SRGBColorSpace ?? THREE.LinearSRGBColorSpace;\n        return renderer;\n    }\n    setSize(width: number, height: number): void {\n        this.width = width;\n        this.height = height;\n        if (this.renderer) {\n            this.renderer.setSize(width, height);\n        }\n    }\n    addScene(scene: any): void {\n        this.scenes.add(scene);\n        scene.create3DObject();\n    }\n    removeScene(scene: any): void {\n        this.scenes.delete(scene);\n    }\n    getScenes(): any[] {\n        return [...this.scenes];\n    }\n    update(deltaTime: number, ...args: any[]): void {\n        this.scenes.forEach((scene) => {\n            scene.update(deltaTime, ...args);\n        });\n        this._onFrame.dispatch('frame', deltaTime);\n    }\n    render(): void {\n        if (this.isContextLost)\n            return;\n        this.renderer.clear();\n        this.scenes.forEach((scene) => {\n            this.renderer.clearDepth();\n            const viewportY = this.height - scene.viewport.y - scene.viewport.height;\n            this.renderer.setViewport(scene.viewport.x, viewportY, scene.viewport.width, scene.viewport.height);\n            this.renderer.render(scene.scene, scene.camera);\n        });\n    }\n    flush(): void {\n        this.renderer.renderLists.dispose();\n    }\n    dispose(): void {\n        this.renderer.domElement.remove();\n        this.renderer.domElement.removeEventListener('webglcontextlost', this.handleContextLost);\n        this.renderer.domElement.removeEventListener('webglcontextrestored', this.handleContextRestored);\n        this.renderer.dispose();\n        this.destroyStats();\n    }\n    private handleContextLost = (event: Event): void => {\n        event.preventDefault();\n        this.isContextLost = true;\n    };\n    private handleContextRestored = (): void => {\n        const canvas = this.renderer.domElement;\n        this.renderer.dispose();\n        this.renderer = this.createGlRenderer(canvas);\n        this.isContextLost = false;\n    };\n}\n"
  },
  {
    "path": "src/engine/gfx/RendererError.ts",
    "content": "export class RendererError extends Error {\n    constructor(message?: string) {\n        super(message);\n        this.name = 'RendererError';\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/Scene.ts",
    "content": "export class Scene {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/SpriteUtils.ts",
    "content": "import { isBetween } from \"../../util/math\";\nimport { BufferGeometryUtils } from \"./BufferGeometryUtils\";\nimport * as THREE from 'three';\ninterface TextureArea {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface Offset {\n    x: number;\n    y: number;\n}\ninterface Align {\n    x: number;\n    y: number;\n}\ninterface ImageSize {\n    width: number;\n    height: number;\n}\ninterface SpriteGeometryOptions {\n    camera: THREE.Camera;\n    texture: THREE.Texture;\n    textureArea?: TextureArea;\n    offset?: Offset;\n    scale?: number;\n    flat?: boolean;\n    depth?: boolean;\n    depthOffset?: number;\n    align: Align;\n}\nclass SpriteUtilsClass {\n    static readonly MAGIC_DEPTH_SCALE: number = 0.8;\n    public readonly USE_INDEXED_GEOMETRY: boolean;\n    public readonly VERTICES_PER_SPRITE: number;\n    public readonly TRIANGLES_PER_SPRITE: number;\n    constructor() {\n        this.USE_INDEXED_GEOMETRY = true;\n        this.VERTICES_PER_SPRITE = this.USE_INDEXED_GEOMETRY ? 8 : 12;\n        this.TRIANGLES_PER_SPRITE = 4;\n    }\n    createSpriteGeometry(options: SpriteGeometryOptions): THREE.BufferGeometry {\n        if (typeof options !== \"object\") {\n            throw new Error(\"Invalid argument\");\n        }\n        const camera = options.camera;\n        const texture = options.texture;\n        if (!options.textureArea) {\n            options.textureArea = {\n                x: 0,\n                y: 0,\n                width: (texture.image as any).width,\n                height: (texture.image as any).height,\n            };\n        }\n        if (!options.offset) {\n            options.offset = { x: 0, y: 0 };\n        }\n        const textureWidth = options.textureArea.width;\n        const textureHeight = options.textureArea.height;\n        const imageSize: ImageSize = {\n            width: (options.texture.image as any).width,\n            height: (options.texture.image as any).height,\n        };\n        const cosY = Math.cos(camera.rotation.y) * (options.scale ?? 1);\n        const flatScale = cosY / Math.sin(-camera.rotation.x);\n        const spriteWidth = textureWidth * cosY;\n        const spriteHeight = textureHeight * (options.flat ? flatScale : cosY);\n        const useDepth = options.depth && !options.flat;\n        const splitX = useDepth && isBetween(-options.offset.x, 0, spriteWidth / cosY)\n            ? -options.offset.x\n            : spriteWidth / cosY / 2;\n        let leftGeometry = this.createRectGeometry(splitX * cosY, spriteHeight);\n        let rightGeometry = this.createRectGeometry(spriteWidth - splitX * cosY, spriteHeight);\n        this.addRectUvs(leftGeometry, { ...options.textureArea, width: splitX }, imageSize);\n        this.addRectUvs(rightGeometry, {\n            ...options.textureArea,\n            x: options.textureArea.x + splitX,\n            width: options.textureArea.width - splitX,\n        }, imageSize);\n        rightGeometry.applyMatrix4(new THREE.Matrix4().makeTranslation((spriteWidth - splitX * cosY + splitX * cosY) / 2, 0, 0));\n        let geometry = BufferGeometryUtils.mergeBufferGeometries([leftGeometry, rightGeometry]);\n        geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(-(spriteWidth / 2 - (splitX * cosY) / 2), 0, 0));\n        const align = options.align;\n        const offset = options.offset;\n        geometry.applyMatrix4(new THREE.Matrix4().makeTranslation((align.x * spriteWidth) / 2 + offset.x * cosY, (align.y * spriteHeight) / 2 - offset.y * (options.flat ? flatScale : cosY), 0));\n        if (useDepth) {\n            this.applyDepth(geometry, camera, options.depthOffset ?? 0);\n        }\n        else if (options.depth && options.flat && options.depthOffset) {\n            this.applyFlatDepth(geometry, options.depthOffset);\n        }\n        const rotation = new THREE.Euler(camera.rotation.x, camera.rotation.y, 0, \"YXZ\");\n        geometry.applyMatrix4(new THREE.Matrix4()\n            .makeRotationFromEuler(rotation)\n            .multiply(options.flat\n            ? new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(-camera.rotation.x - Math.PI / 2, 0, 0))\n            : new THREE.Matrix4().identity()));\n        return geometry;\n    }\n    createRectGeometry(width: number, height: number): THREE.BufferGeometry {\n        return this.USE_INDEXED_GEOMETRY\n            ? this.createIndexedRectGeometry(width, height)\n            : this.createNonIndexedRectGeometry(width, height);\n    }\n    createNonIndexedRectGeometry(width: number, height: number): THREE.BufferGeometry {\n        let geometry = new THREE.BufferGeometry();\n        const vertices = new Float32Array([\n            -0.5 * width, 0.5 * height, 0,\n            -0.5 * width, -0.5 * height, 0,\n            0.5 * width, 0.5 * height, 0,\n            -0.5 * width, -0.5 * height, 0,\n            0.5 * width, -0.5 * height, 0,\n            0.5 * width, 0.5 * height, 0,\n        ]);\n        geometry.setAttribute(\"position\", new THREE.BufferAttribute(vertices, 3));\n        return geometry;\n    }\n    createIndexedRectGeometry(width: number, height: number): THREE.BufferGeometry {\n        let geometry = new THREE.BufferGeometry();\n        const vertices = new Float32Array([\n            -0.5 * width, 0.5 * height, 0,\n            0.5 * width, 0.5 * height, 0,\n            -0.5 * width, -0.5 * height, 0,\n            0.5 * width, -0.5 * height, 0,\n        ]);\n        geometry.setAttribute(\"position\", new THREE.BufferAttribute(vertices, 3));\n        const indices = new Uint16Array([0, 2, 1, 2, 3, 1]);\n        geometry.setIndex(new THREE.BufferAttribute(indices, 1));\n        return geometry;\n    }\n    addRectUvs(geometry: THREE.BufferGeometry, textureArea: TextureArea, imageSize: ImageSize): void {\n        const uvs = new Float32Array(2 * geometry.getAttribute(\"position\")!.count);\n        if (this.USE_INDEXED_GEOMETRY) {\n            this.writeIndexedRectUvsIntoBuffer(uvs, 0, textureArea, imageSize);\n        }\n        else {\n            this.writeNonIndexedRectUvsIntoBuffer(uvs, 0, textureArea, imageSize);\n        }\n        geometry.setAttribute(\"uv\", new THREE.BufferAttribute(uvs, 2));\n    }\n    writeNonIndexedRectUvsIntoBuffer(buffer: Float32Array, offset: number, textureArea: TextureArea, imageSize: ImageSize): void {\n        const u = textureArea.x / imageSize.width;\n        const v = 1 - (textureArea.y + textureArea.height) / imageSize.height;\n        const uWidth = textureArea.width / imageSize.width;\n        const vHeight = textureArea.height / imageSize.height;\n        buffer.set([u, v + vHeight, u, v, u + uWidth, v + vHeight, u, v, u + uWidth, v, u + uWidth, v + vHeight], 12 * offset);\n    }\n    writeIndexedRectUvsIntoBuffer(buffer: Float32Array, offset: number, textureArea: TextureArea, imageSize: ImageSize): void {\n        const u = textureArea.x / imageSize.width;\n        const v = 1 - (textureArea.y + textureArea.height) / imageSize.height;\n        const uWidth = textureArea.width / imageSize.width;\n        const vHeight = textureArea.height / imageSize.height;\n        buffer.set([u, v + vHeight, u + uWidth, v + vHeight, u, v, u + uWidth, v], 8 * offset);\n    }\n    applyDepth(geometry: THREE.BufferGeometry, camera: THREE.Camera, depthOffset: number): void {\n        let positions = geometry.getAttribute(\"position\") as THREE.BufferAttribute;\n        for (let i = 0, count = positions.count; i < count; i++) {\n            const x = positions.getX(i) * SpriteUtilsClass.MAGIC_DEPTH_SCALE;\n            let z: number;\n            if (x < 0) {\n                z = depthOffset - (Math.abs(x) / Math.cos(camera.rotation.x)) * Math.tan(camera.rotation.y);\n            }\n            else {\n                z = depthOffset - x / Math.cos(camera.rotation.x) / Math.tan(camera.rotation.y);\n            }\n            positions.setZ(i, z);\n        }\n    }\n    applyFlatDepth(geometry: THREE.BufferGeometry, depthOffset: number): void {\n        let positions = geometry.getAttribute(\"position\") as THREE.BufferAttribute;\n        for (let i = 0, count = positions.count; i < count; i++) {\n            positions.setZ(i, depthOffset);\n        }\n    }\n}\nconst spriteUtilsInstance = new SpriteUtilsClass();\nexport const createSpriteGeometry = spriteUtilsInstance.createSpriteGeometry.bind(spriteUtilsInstance);\nexport const VERTICES_PER_SPRITE: number = spriteUtilsInstance.VERTICES_PER_SPRITE;\nexport const TRIANGLES_PER_SPRITE: number = spriteUtilsInstance.TRIANGLES_PER_SPRITE;\nexport const MAGIC_DEPTH_SCALE: number = SpriteUtilsClass.MAGIC_DEPTH_SCALE;\nexport const SpriteUtils = spriteUtilsInstance;\nexport type { TextureArea, Offset, Align, ImageSize, SpriteGeometryOptions };\n"
  },
  {
    "path": "src/engine/gfx/TextureAtlas.ts",
    "content": "import { IndexedBitmap } from '../../data/Bitmap';\nimport * as THREE from 'three';\nimport { GrowingPacker } from './GrowingPacker';\nfunction createAtlasBitmap(blocks: any[], width: number, height: number, imageRects?: Map<IndexedBitmap, any>): IndexedBitmap {\n    const atlasBitmap = new IndexedBitmap(width, height);\n    blocks.forEach(block => {\n        if (!block.fit) {\n            throw new Error(\"Couldn't fit all images in a single texture\");\n        }\n        const image = block.image;\n        const x = block.fit.x;\n        const y = block.fit.y;\n        imageRects?.set(image, { x, y, width: block.w, height: block.h });\n        atlasBitmap.drawIndexedImage(image, x, y);\n    });\n    return atlasBitmap;\n}\nfunction createAtlasRgbaData(bitmap: IndexedBitmap): Uint8Array {\n    const rgbaData = new Uint8Array(bitmap.width * bitmap.height * 4);\n    for (let i = 0; i < bitmap.data.length; i++) {\n        const rgbaIndex = i * 4;\n        const paletteIndex = bitmap.data[i];\n        rgbaData[rgbaIndex] = 0;\n        rgbaData[rgbaIndex + 1] = 0;\n        rgbaData[rgbaIndex + 2] = 0;\n        rgbaData[rgbaIndex + 3] = paletteIndex;\n    }\n    return rgbaData;\n}\nexport class TextureAtlas {\n    private texture?: THREE.DataTexture;\n    private imageRects?: Map<IndexedBitmap, any>;\n    private width: number = 0;\n    private height: number = 0;\n    getTexture(): THREE.DataTexture {\n        if (!this.texture) {\n            throw new Error('Texture atlas not initialized');\n        }\n        return this.texture;\n    }\n    getImageRect(image: IndexedBitmap): any {\n        if (!this.imageRects) {\n            throw new Error('Texture atlas not initialized');\n        }\n        const rect = this.imageRects.get(image);\n        if (!rect) {\n            throw new Error('Image not found in atlas');\n        }\n        return rect;\n    }\n    pack(images: IndexedBitmap[]): void {\n        const blocks: any[] = [];\n        images.forEach(image => {\n            blocks.push({\n                w: image.width + (image.width % 2),\n                h: image.height + (image.height % 2),\n                image: image\n            });\n        });\n        blocks.sort((a, b) => (b.w - a.w) * 10000 + b.h - a.h);\n        const packer = new GrowingPacker();\n        packer.fit(blocks);\n        const width = packer.root.w;\n        const height = packer.root.h;\n        const imageRects = new Map<IndexedBitmap, any>();\n        const atlasBitmap = createAtlasBitmap(blocks, width, height, imageRects);\n        const rgbaData = createAtlasRgbaData(atlasBitmap);\n        const texture = new THREE.DataTexture(rgbaData, width, height, THREE.RGBAFormat);\n        texture.needsUpdate = true;\n        texture.flipY = true;\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        texture.colorSpace = THREE.NoColorSpace;\n        this.width = width;\n        this.height = height;\n        this.imageRects = imageRects;\n        this.texture = texture;\n    }\n    dispose(): void {\n        this.texture?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/TextureUtils.ts",
    "content": "import { RgbaBitmap } from \"../../data/Bitmap\";\nimport { Palette } from \"../../data/Palette\";\nimport { fnv32a } from \"../../util/math\";\nimport { CanvasUtils } from \"./CanvasUtils\";\nimport { PalDrawable } from \"./drawable/PalDrawable\";\nimport * as THREE from 'three';\nclass TextureUtilsClass {\n    static cache = new Map<number, THREE.Texture>();\n    static textureFromPalette(palette: Palette): THREE.Texture {\n        const hash = palette.hash;\n        let texture = TextureUtilsClass.cache.get(hash);\n        if (texture) {\n            return texture;\n        }\n        const bitmap = new PalDrawable(palette).draw();\n        texture = this.textureFromPalBitmap(bitmap);\n        TextureUtilsClass.cache.set(hash, texture);\n        return texture;\n    }\n    static textureFromPalettes(palettes: Palette[]): THREE.Texture {\n        if (!palettes.length) {\n            throw new Error(\"At least one palette is required\");\n        }\n        const hash = fnv32a(palettes.map((palette) => palette.hash));\n        let texture = TextureUtilsClass.cache.get(hash);\n        if (texture) {\n            return texture;\n        }\n        const bitmaps = palettes.map((palette) => new PalDrawable(palette).draw());\n        let combinedBitmap = new RgbaBitmap(bitmaps[0].width, bitmaps.length);\n        let row = 0;\n        for (const bitmap of bitmaps) {\n            combinedBitmap.drawRgbaImage(bitmap, 0, row++);\n        }\n        texture = this.textureFromPalBitmap(combinedBitmap);\n        TextureUtilsClass.cache.set(hash, texture);\n        return texture;\n    }\n    static textureFromPalBitmap(bitmap: RgbaBitmap): THREE.Texture {\n        const canvas = CanvasUtils.canvasFromRgbaImageData(bitmap.data, bitmap.width, bitmap.height);\n        let texture = new THREE.Texture(canvas);\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        texture.needsUpdate = true;\n        texture.flipY = false;\n        texture.colorSpace = (THREE as any).SRGBColorSpace ?? THREE.LinearSRGBColorSpace;\n        return texture;\n    }\n}\nexport const textureFromPalette = TextureUtilsClass.textureFromPalette.bind(TextureUtilsClass);\nexport const textureFromPalettes = TextureUtilsClass.textureFromPalettes.bind(TextureUtilsClass);\nexport const textureFromPalBitmap = TextureUtilsClass.textureFromPalBitmap.bind(TextureUtilsClass);\nexport const TextureUtils = TextureUtilsClass;\n"
  },
  {
    "path": "src/engine/gfx/batch/BatchedMesh.ts",
    "content": "import * as THREE from 'three';\nexport enum BatchMode {\n    Instancing = 0,\n    Merging = 1\n}\nexport class BatchedMesh extends THREE.Mesh {\n    public batchMode: BatchMode;\n    public isBatchedMesh: boolean = true;\n    public opacity: number = 1;\n    public extraLight: THREE.Vector3 = new THREE.Vector3(0, 0, 0);\n    public paletteIndex: number = 0;\n    public clippingPlanes: THREE.Plane[] = [];\n    public clippingPlanesHash: string = \"\";\n    constructor(geometry: THREE.BufferGeometry, material: THREE.Material, batchMode: BatchMode = BatchMode.Instancing) {\n        super(geometry, material);\n        this.geometry = geometry;\n        this.material = material;\n        this.batchMode = batchMode;\n        this.castShadow = false;\n        this.layers.disable(0);\n        this.layers.enable(1);\n    }\n    getOpacity(): number {\n        return this.opacity;\n    }\n    setOpacity(opacity: number): void {\n        this.opacity = opacity;\n    }\n    getExtraLight(): THREE.Vector3 {\n        return this.extraLight;\n    }\n    setExtraLight(extraLight: THREE.Vector3): void {\n        this.extraLight = extraLight;\n    }\n    getPaletteIndex(): number {\n        return this.paletteIndex;\n    }\n    setPaletteIndex(paletteIndex: number): void {\n        this.paletteIndex = paletteIndex;\n    }\n    getClippingPlanes(): THREE.Plane[] {\n        return this.clippingPlanes;\n    }\n    setClippingPlanes(clippingPlanes: THREE.Plane[]): void {\n        this.clippingPlanes = clippingPlanes;\n        this.updateClippingPlanesHash(clippingPlanes);\n    }\n    private updateClippingPlanesHash(clippingPlanes: THREE.Plane[]): void {\n        this.clippingPlanesHash = clippingPlanes\n            .map((plane) => [...plane.normal.toArray(), plane.constant])\n            .flat()\n            .join(\",\");\n    }\n    getClippingPlanesHash(): string {\n        return this.clippingPlanesHash;\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/batch/InstancedMesh.ts",
    "content": "import * as THREE from 'three';\nconst depthMaterial = new THREE.MeshDepthMaterial();\ndepthMaterial.depthPacking = THREE.RGBADepthPacking;\n(depthMaterial as any).clipping = true;\n(depthMaterial as any).defines = { INSTANCE_TRANSFORM: \"\" };\nconst distanceShader = THREE.ShaderLib.distance;\nconst distanceUniforms = THREE.UniformsUtils.clone(distanceShader.uniforms);\nconst distanceDefines = { USE_SHADOWMAP: \"\", INSTANCE_TRANSFORM: \"\" };\nconst distanceMaterial = new THREE.ShaderMaterial({\n    defines: distanceDefines,\n    uniforms: distanceUniforms,\n    vertexShader: distanceShader.vertexShader,\n    fragmentShader: distanceShader.fragmentShader,\n    clipping: true,\n});\nexport class InstancedMesh extends THREE.Mesh {\n    public maxInstances: number;\n    public uniformScale: boolean;\n    public useInstanceColor: boolean;\n    private instanceMatrixAttributes: THREE.InstancedBufferAttribute[];\n    constructor(geometry: THREE.BufferGeometry, material: THREE.Material, maxInstances: number, uniformScale: boolean, useInstanceColor: boolean = false) {\n        const instancedGeometry = new THREE.InstancedBufferGeometry();\n        (instancedGeometry as any).copy(geometry);\n        super(instancedGeometry);\n        this.maxInstances = maxInstances;\n        this.uniformScale = uniformScale;\n        this.useInstanceColor = useInstanceColor;\n        this.initAttributes(this.geometry as THREE.InstancedBufferGeometry);\n        this.material = this.decorateMaterial(material.clone());\n        this.frustumCulled = false;\n        this.customDepthMaterial = depthMaterial;\n        this.customDistanceMaterial = distanceMaterial;\n    }\n    private initAttributes(geometry: THREE.InstancedBufferGeometry): void {\n        const attributes: Array<{\n            name: string;\n            data: Float32Array | Uint8Array;\n            itemSize: number;\n            normalized: boolean;\n        }> = [];\n        for (let i = 0; i < 4; i++) {\n            attributes.push({\n                name: \"instanceMatrix\" + i,\n                data: new Float32Array(4 * this.maxInstances),\n                itemSize: 4,\n                normalized: true,\n            });\n        }\n        if (this.useInstanceColor) {\n            attributes.push({\n                name: \"instanceColor\",\n                data: new Uint8Array(3 * this.maxInstances),\n                itemSize: 3,\n                normalized: true,\n            });\n        }\n        attributes.push({\n            name: \"instanceOpacity\",\n            data: new Float32Array(this.maxInstances).fill(1),\n            itemSize: 1,\n            normalized: true,\n        });\n        for (const { name, data, itemSize, normalized } of attributes) {\n            const attribute = new THREE.InstancedBufferAttribute(data, itemSize, normalized, 1);\n            attribute.setUsage(THREE.DynamicDrawUsage);\n            geometry.setAttribute(name, attribute);\n        }\n        this.instanceMatrixAttributes = new Array(4)\n            .fill(0)\n            .map((_, i) => geometry.getAttribute(\"instanceMatrix\" + i) as THREE.InstancedBufferAttribute);\n    }\n    private decorateMaterial(material: THREE.Material): THREE.Material {\n        const mat = material as any;\n        if (!mat.defines) {\n            mat.defines = {};\n        }\n        mat.defines.INSTANCE_TRANSFORM = \"\";\n        if (this.uniformScale) {\n            mat.defines.INSTANCE_UNIFORM = \"\";\n        }\n        else {\n            delete mat.defines.INSTANCE_UNIFORM;\n        }\n        if (this.useInstanceColor) {\n            mat.defines.INSTANCE_COLOR = \"\";\n        }\n        else {\n            delete mat.defines.INSTANCE_COLOR;\n        }\n        mat.defines.INSTANCE_OPACITY = \"\";\n        return material;\n    }\n    public setRenderCount(count: number): void {\n        if (count > this.maxInstances) {\n            throw new RangeError(\"Exceeded maximum number of instances\");\n        }\n        (this.geometry as THREE.InstancedBufferGeometry).instanceCount = count;\n    }\n    public setMatrixAt(index: number, matrix: THREE.Matrix4): void {\n        for (let row = 0; row < 4; row++) {\n            let offset = 4 * row;\n            this.instanceMatrixAttributes[row].setXYZW(index, matrix.elements[offset++], matrix.elements[offset++], matrix.elements[offset++], matrix.elements[offset]);\n        }\n    }\n    public updateFromMeshes(meshes: any[]): void {\n        if (meshes.length === 0)\n            return;\n        const hasPalette = !!meshes[0].material.palette;\n        const attributes = (this.geometry as THREE.InstancedBufferGeometry).attributes;\n        const opacityAttr = attributes.instanceOpacity as THREE.InstancedBufferAttribute;\n        const paletteOffsetAttr = attributes.instancePaletteOffset as THREE.InstancedBufferAttribute;\n        const extraLightAttr = attributes.instanceExtraLight as THREE.InstancedBufferAttribute;\n        for (let i = 0, len = meshes.length; i < len; i++) {\n            const mesh = meshes[i];\n            this.setMatrixAt(i, mesh.matrixWorld);\n            const opacity = mesh.getOpacity();\n            if (opacityAttr.getX(i) !== opacity) {\n                opacityAttr.setX(i, opacity);\n                opacityAttr.needsUpdate = true;\n            }\n            if (hasPalette) {\n                const paletteIndex = mesh.getPaletteIndex();\n                if (paletteOffsetAttr.getX(i) !== paletteIndex) {\n                    paletteOffsetAttr.setX(i, paletteIndex);\n                    paletteOffsetAttr.needsUpdate = true;\n                }\n                const extraLight = mesh.getExtraLight();\n                const x = Math.fround(extraLight.x);\n                const y = Math.fround(extraLight.y);\n                const z = Math.fround(extraLight.z);\n                if (x !== extraLightAttr.getX(i) || y !== extraLightAttr.getY(i) || z !== extraLightAttr.getZ(i)) {\n                    extraLightAttr.setXYZ(i, x, y, z);\n                    extraLightAttr.needsUpdate = true;\n                }\n            }\n        }\n        this.setRenderCount(meshes.length);\n        for (const attr of this.instanceMatrixAttributes) {\n            attr.needsUpdate = true;\n        }\n    }\n    public dispose(): void {\n        this.geometry.dispose();\n        (this.material as THREE.Material).dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/batch/MergedSpriteMesh.ts",
    "content": "import * as THREE from 'three';\nimport * as arrayUtils from '../../../util/array';\nimport { PaletteBasicMaterial } from '../material/PaletteBasicMaterial';\nconst tempVector3 = new THREE.Vector3();\nconst tempVector4 = new THREE.Vector4();\nexport class MergedSpriteMesh extends THREE.Mesh {\n    public maxInstances: number;\n    public verticesPerItem: number;\n    public indicesPerItem: number | undefined;\n    static createMergedGeometry(sourceGeometry: THREE.BufferGeometry, maxInstances: number, material: THREE.Material): THREE.BufferGeometry {\n        const mergedGeometry = new THREE.BufferGeometry();\n        for (const attributeName of Object.keys(sourceGeometry.attributes)) {\n            const sourceAttribute = sourceGeometry.getAttribute(attributeName);\n            const ArrayConstructor = sourceAttribute.array.constructor as any;\n            const mergedArray = new ArrayConstructor(maxInstances * sourceAttribute.array.length);\n            mergedGeometry.setAttribute(attributeName, new THREE.BufferAttribute(mergedArray, sourceAttribute.itemSize, sourceAttribute.normalized));\n        }\n        const vertexCount = sourceGeometry.getAttribute('position').count;\n        if (material instanceof PaletteBasicMaterial) {\n            mergedGeometry.setAttribute('vertexColorMult', new THREE.BufferAttribute(new Float32Array(vertexCount * maxInstances * 4), 4));\n        }\n        if ((material as any).palette) {\n            mergedGeometry.setAttribute('vertexPaletteOffset', new THREE.BufferAttribute(new Float32Array(vertexCount * maxInstances), 1));\n        }\n        for (const attribute of Object.values(mergedGeometry.attributes)) {\n            (attribute as THREE.BufferAttribute).setUsage(THREE.DynamicDrawUsage);\n        }\n        if (sourceGeometry.index) {\n            mergedGeometry.setIndex(new THREE.BufferAttribute(new Uint32Array(maxInstances * sourceGeometry.index.array.length), 1));\n            for (let i = 0; i < maxInstances; i++) {\n                const vertexOffset = i * vertexCount;\n                const indexArray = mergedGeometry.index!.array as Uint32Array;\n                const sourceIndexArray = sourceGeometry.index.array;\n                indexArray.set(Uint32Array.from(sourceIndexArray, (index: number) => index + vertexOffset), i * sourceIndexArray.length);\n            }\n        }\n        return mergedGeometry;\n    }\n    constructor(sourceGeometry: THREE.BufferGeometry, material: THREE.Material, maxInstances: number) {\n        super(MergedSpriteMesh.createMergedGeometry(sourceGeometry, maxInstances, material));\n        this.maxInstances = maxInstances;\n        this.material = this.decorateMaterial(material.clone());\n        this.verticesPerItem = sourceGeometry.getAttribute('position').count;\n        this.indicesPerItem = sourceGeometry.index?.count;\n        this.frustumCulled = false;\n    }\n    private decorateMaterial(material: THREE.Material): THREE.Material {\n        const mat = material as any;\n        if (!mat.defines) {\n            mat.defines = {};\n        }\n        if (mat.palette) {\n            mat.defines.VERTEX_PALETTE_OFFSET = '';\n        }\n        if (material instanceof PaletteBasicMaterial) {\n            (mat as any).useVertexColorMult = true;\n        }\n        return material;\n    }\n    public updateFromMeshes(meshes: any[]): void {\n        const attributes = this.geometry.attributes;\n        const positionAttr = attributes.position as THREE.BufferAttribute;\n        const uvAttr = attributes.uv as THREE.BufferAttribute;\n        const colorMultAttr = attributes.vertexColorMult as THREE.BufferAttribute;\n        const paletteOffsetAttr = attributes.vertexPaletteOffset as THREE.BufferAttribute;\n        const meshCount = meshes.length;\n        if (meshCount > this.maxInstances) {\n            throw new RangeError('Exceeded maximum number of instances');\n        }\n        for (let i = 0; i < meshCount; i++) {\n            const vertexOffset = i * this.verticesPerItem;\n            const mesh = meshes[i];\n            this.setGeometryAt(vertexOffset, mesh.geometry, tempVector3.setFromMatrixPosition(mesh.matrixWorld), positionAttr, uvAttr);\n            const extraLight = mesh.getExtraLight();\n            if (colorMultAttr) {\n                this.setColorMultAt(vertexOffset, tempVector4.set(1 + extraLight.x, 1 + extraLight.y, 1 + extraLight.z, mesh.getOpacity()), colorMultAttr);\n            }\n            if (paletteOffsetAttr) {\n                this.setPaletteIndexAt(vertexOffset, mesh.getPaletteIndex(), paletteOffsetAttr);\n            }\n        }\n        this.geometry.setDrawRange(0, meshCount * (this.geometry.index ? this.indicesPerItem! : this.verticesPerItem));\n        for (const attribute of Object.values(attributes)) {\n            if ((attribute as any).usage === THREE.DynamicDrawUsage) {\n                const bufferAttr = attribute as THREE.BufferAttribute;\n                if (bufferAttr.updateRanges && bufferAttr.updateRanges.length > 0) {\n                    bufferAttr.updateRanges[0].count =\n                        meshCount < this.maxInstances\n                            ? meshCount * this.verticesPerItem * bufferAttr.itemSize\n                            : -1;\n                }\n            }\n        }\n    }\n    private setGeometryAt(vertexOffset: number, sourceGeometry: THREE.BufferGeometry, worldPosition: THREE.Vector3, positionAttr: THREE.BufferAttribute, uvAttr: THREE.BufferAttribute): void {\n        const sourceAttributes = sourceGeometry.attributes;\n        const sourcePositions = sourceAttributes.position.array as Float32Array;\n        const targetPositions = positionAttr.array as Float32Array;\n        for (let i = 0; i < this.verticesPerItem; i++) {\n            const targetIndex = 3 * (vertexOffset + i);\n            const sourceIndex = 3 * i;\n            const x = Math.fround(sourcePositions[sourceIndex] + Math.fround(worldPosition.x));\n            const y = Math.fround(sourcePositions[sourceIndex + 1] + Math.fround(worldPosition.y));\n            const z = Math.fround(sourcePositions[sourceIndex + 2] + Math.fround(worldPosition.z));\n            if (x !== targetPositions[targetIndex] ||\n                y !== targetPositions[targetIndex + 1] ||\n                z !== targetPositions[targetIndex + 2]) {\n                targetPositions[targetIndex] = x;\n                targetPositions[targetIndex + 1] = y;\n                targetPositions[targetIndex + 2] = z;\n                positionAttr.needsUpdate = true;\n            }\n        }\n        const targetUVs = uvAttr.array as Float32Array;\n        const sourceUVs = sourceAttributes.uv.array as Float32Array;\n        const uvStartIndex = 2 * vertexOffset;\n        if (!arrayUtils.equals(Array.from(sourceUVs), Array.from(targetUVs.subarray(uvStartIndex, uvStartIndex + sourceUVs.length)))) {\n            targetUVs.set(sourceUVs, uvStartIndex);\n            uvAttr.needsUpdate = true;\n        }\n    }\n    private setColorMultAt(vertexOffset: number, colorMult: THREE.Vector4, colorMultAttr: THREE.BufferAttribute): void {\n        if (colorMultAttr.getX(vertexOffset) !== colorMult.x ||\n            colorMultAttr.getY(vertexOffset) !== colorMult.y ||\n            colorMultAttr.getZ(vertexOffset) !== colorMult.z ||\n            colorMultAttr.getW(vertexOffset) !== colorMult.w) {\n            colorMultAttr.needsUpdate = true;\n            for (let i = 0; i < this.verticesPerItem; i++) {\n                colorMultAttr.setXYZW(vertexOffset + i, colorMult.x, colorMult.y, colorMult.z, colorMult.w);\n            }\n        }\n    }\n    private setPaletteIndexAt(vertexOffset: number, paletteIndex: number, paletteOffsetAttr: THREE.BufferAttribute): void {\n        if (paletteOffsetAttr.getX(vertexOffset) !== paletteIndex) {\n            paletteOffsetAttr.needsUpdate = true;\n            for (let i = 0; i < this.verticesPerItem; i++) {\n                paletteOffsetAttr.setX(vertexOffset + i, paletteIndex);\n            }\n        }\n    }\n    public dispose(): void {\n        this.geometry.dispose();\n        (this.material as THREE.Material).dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/batch/MeshBatchManager.ts",
    "content": "import * as THREE from 'three';\nimport { BatchedMesh, BatchMode } from './BatchedMesh';\nimport { MeshInstancingBatch } from './MeshInstancingBatch';\nimport { RenderableContainer } from '../RenderableContainer';\nimport { MeshMergingBatch } from './MeshMergingBatch';\ninterface MeshBatch {\n    castShadow: boolean;\n    receiveShadow: boolean;\n    renderOrder: number;\n    clippingPlanes: THREE.Plane[];\n    setMeshes(meshes: BatchedMesh[]): void;\n    dispose(): void;\n}\nexport class MeshBatchManager extends RenderableContainer {\n    private renderableContainer: RenderableContainer;\n    private batches: Map<string, MeshBatch[]> = new Map();\n    constructor(renderableContainer: RenderableContainer) {\n        super();\n        this.renderableContainer = renderableContainer;\n    }\n    create3DObject(): void {\n        let container = this.get3DObject();\n        if (!container) {\n            container = new THREE.Object3D();\n            container.name = \"mesh_batch_manager\";\n            container.matrixAutoUpdate = false;\n            this.set3DObject(container);\n        }\n        super.create3DObject();\n    }\n    updateMeshes(): void {\n        const container = this.renderableContainer.get3DObject();\n        if (!container)\n            return;\n        const meshes = this.collectMeshes(container);\n        const groupedMeshes = this.groupMeshesByBatchKey(meshes);\n        const usedBatchCounts = this.fillBatches(groupedMeshes);\n        this.cleanUnusedBatches(usedBatchCounts);\n    }\n    private collectMeshes(container: THREE.Object3D): BatchedMesh[] {\n        const meshes: BatchedMesh[] = [];\n        container.traverseVisible((object) => {\n            if ((object as any).isBatchedMesh) {\n                meshes.push(object as BatchedMesh);\n            }\n        });\n        return meshes;\n    }\n    private fillBatches(groupedMeshes: Map<string, BatchedMesh[]>): Map<string, number> {\n        const usedBatchCounts = new Map<string, number>([...this.batches.keys()].map(key => [key, 0]));\n        for (const [batchKey, meshes] of groupedMeshes) {\n            let batchArray = this.batches.get(batchKey);\n            let batchIndex = 0;\n            while (meshes.length > 0) {\n                const isInstancing = meshes[0].batchMode === BatchMode.Instancing;\n                const maxInstances = isInstancing ? 1024 : 128;\n                const batchMeshes = meshes.splice(0, maxInstances);\n                let batch = batchArray?.[batchIndex];\n                if (!batch) {\n                    if (!batchArray) {\n                        batchArray = [];\n                        this.batches.set(batchKey, batchArray);\n                    }\n                    batch = new (isInstancing ? MeshInstancingBatch : MeshMergingBatch)(maxInstances);\n                    batch.castShadow = batchMeshes[0].castShadow;\n                    batch.receiveShadow = batchMeshes[0].receiveShadow;\n                    batch.renderOrder = batchMeshes[0].renderOrder;\n                    batch.clippingPlanes = batchMeshes[0].getClippingPlanes();\n                    batchArray.push(batch);\n                    this.add(batch as any);\n                    this.processRenderQueue();\n                }\n                batch.setMeshes(batchMeshes);\n                batchIndex++;\n            }\n            usedBatchCounts.set(batchKey, batchIndex);\n        }\n        return usedBatchCounts;\n    }\n    private cleanUnusedBatches(usedBatchCounts: Map<string, number>): void {\n        for (const [batchKey, usedCount] of usedBatchCounts) {\n            const batchArray = this.batches.get(batchKey);\n            if (batchArray) {\n                const unusedBatches = batchArray.splice(usedCount);\n                for (const batch of unusedBatches) {\n                    this.remove(batch as any);\n                    batch.dispose();\n                }\n                if (batchArray.length === 0) {\n                    this.batches.delete(batchKey);\n                }\n            }\n        }\n    }\n    private groupMeshesByBatchKey(meshes: BatchedMesh[]): Map<string, BatchedMesh[]> {\n        const groups = new Map<string, BatchedMesh[]>();\n        for (let i = 0, length = meshes.length; i < length; i++) {\n            const mesh = meshes[i];\n            const batchKey = this.getBatchKey(mesh);\n            let group = groups.get(batchKey);\n            if (!group) {\n                group = [];\n                groups.set(batchKey, group);\n            }\n            group.push(mesh);\n        }\n        return groups;\n    }\n    private getBatchKey(mesh: BatchedMesh): string {\n        const material = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;\n        return (mesh.batchMode +\n            \"_\" +\n            (mesh.batchMode === BatchMode.Instancing\n                ? mesh.geometry.uuid\n                : mesh.geometry.attributes.position.count) +\n            \"_\" +\n            material.uuid +\n            \"_\" +\n            Number(mesh.castShadow) +\n            \"_\" +\n            mesh.renderOrder +\n            \"_\" +\n            Number(mesh.receiveShadow) +\n            \"_\" +\n            mesh.getClippingPlanesHash());\n    }\n    dispose(): void {\n        this.batches.forEach(batchArray => batchArray.forEach(batch => batch.dispose()));\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/batch/MeshInstancingBatch.ts",
    "content": "import * as THREE from 'three';\nimport { InstancedMesh } from './InstancedMesh';\nexport class MeshInstancingBatch {\n    public maxInstances: number;\n    private target?: THREE.Object3D;\n    private instancedMesh?: InstancedMesh;\n    private _castShadow: boolean = false;\n    private _receiveShadow: boolean = false;\n    private _clippingPlanes: THREE.Plane[] = [];\n    private _renderOrder: number = 0;\n    constructor(maxInstances: number) {\n        this.maxInstances = maxInstances;\n    }\n    get castShadow(): boolean {\n        return this._castShadow;\n    }\n    set castShadow(value: boolean) {\n        this._castShadow = value;\n        if (this.instancedMesh) {\n            this.instancedMesh.castShadow = value;\n        }\n    }\n    get receiveShadow(): boolean {\n        return this._receiveShadow;\n    }\n    set receiveShadow(value: boolean) {\n        this._receiveShadow = value;\n        if (this.instancedMesh) {\n            this.instancedMesh.receiveShadow = value;\n        }\n    }\n    get clippingPlanes(): THREE.Plane[] {\n        return this._clippingPlanes;\n    }\n    set clippingPlanes(value: THREE.Plane[]) {\n        this._clippingPlanes = value;\n        if (this.instancedMesh) {\n            (this.instancedMesh.material as any).clippingPlanes = value;\n        }\n    }\n    get renderOrder(): number {\n        return this._renderOrder;\n    }\n    set renderOrder(value: number) {\n        this._renderOrder = value;\n        if (this.instancedMesh) {\n            this.instancedMesh.renderOrder = value;\n        }\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    create3DObject(): void {\n        if (!this.target) {\n            const object3D = new THREE.Object3D();\n            object3D.matrixAutoUpdate = false;\n            this.target = object3D;\n            if (this.instancedMesh) {\n                object3D.add(this.instancedMesh);\n            }\n        }\n    }\n    setMeshes(meshes: any[]): void {\n        if (meshes.length > this.maxInstances) {\n            throw new RangeError('Meshes array exceeds max number of instances');\n        }\n        if (meshes.length > 0) {\n            const hasPalette = !!meshes[0].material.palette;\n            if (!this.instancedMesh) {\n                this.instancedMesh = new InstancedMesh(meshes[0].geometry, meshes[0].material, this.maxInstances, true);\n                this.instancedMesh.castShadow = this._castShadow;\n                this.instancedMesh.renderOrder = this._renderOrder;\n                (this.instancedMesh.material as any).clippingPlanes = this._clippingPlanes;\n                if (hasPalette) {\n                    const geometry = this.instancedMesh.geometry as THREE.InstancedBufferGeometry;\n                    geometry.setAttribute('instancePaletteOffset', new THREE.InstancedBufferAttribute(new Float32Array(this.maxInstances), 1));\n                    geometry.setAttribute('instanceExtraLight', new THREE.InstancedBufferAttribute(new Float32Array(3 * this.maxInstances), 3));\n                }\n                if (this.target) {\n                    this.target.add(this.instancedMesh);\n                }\n            }\n            this.instancedMesh.updateFromMeshes(meshes);\n        }\n        else {\n            if (this.instancedMesh) {\n                if (this.target) {\n                    this.target.remove(this.instancedMesh);\n                }\n                this.instancedMesh.dispose();\n                this.instancedMesh = undefined;\n            }\n        }\n    }\n    update(): void {\n    }\n    dispose(): void {\n        if (this.instancedMesh) {\n            this.instancedMesh.dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/batch/MeshMergingBatch.ts",
    "content": "import * as THREE from 'three';\nimport { MergedSpriteMesh } from './MergedSpriteMesh';\nexport class MeshMergingBatch {\n    public maxInstances: number;\n    private target?: THREE.Object3D;\n    private mergedGeoMesh?: MergedSpriteMesh;\n    private _castShadow: boolean = false;\n    private _receiveShadow: boolean = false;\n    private _clippingPlanes: THREE.Plane[] = [];\n    private _renderOrder: number = 0;\n    constructor(maxInstances: number) {\n        this.maxInstances = maxInstances;\n    }\n    get castShadow(): boolean {\n        return this._castShadow;\n    }\n    set castShadow(value: boolean) {\n        this._castShadow = value;\n        if (this.mergedGeoMesh) {\n            this.mergedGeoMesh.castShadow = value;\n        }\n    }\n    get receiveShadow(): boolean {\n        return this._receiveShadow;\n    }\n    set receiveShadow(value: boolean) {\n        this._receiveShadow = value;\n        if (this.mergedGeoMesh) {\n            this.mergedGeoMesh.receiveShadow = value;\n        }\n    }\n    get clippingPlanes(): THREE.Plane[] {\n        return this._clippingPlanes;\n    }\n    set clippingPlanes(value: THREE.Plane[]) {\n        this._clippingPlanes = value;\n        if (this.mergedGeoMesh) {\n            (this.mergedGeoMesh.material as any).clippingPlanes = value;\n        }\n    }\n    get renderOrder(): number {\n        return this._renderOrder;\n    }\n    set renderOrder(value: number) {\n        this._renderOrder = value;\n        if (this.mergedGeoMesh) {\n            this.mergedGeoMesh.renderOrder = value;\n        }\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    create3DObject(): void {\n        if (!this.target) {\n            const object3D = new THREE.Object3D();\n            object3D.matrixAutoUpdate = false;\n            this.target = object3D;\n            if (this.mergedGeoMesh) {\n                object3D.add(this.mergedGeoMesh);\n            }\n        }\n    }\n    setMeshes(meshes: any[]): void {\n        if (meshes.length > this.maxInstances) {\n            throw new RangeError('Meshes array exceeds max number of instances');\n        }\n        if (meshes.length > 0) {\n            if (!this.mergedGeoMesh) {\n                this.mergedGeoMesh = new MergedSpriteMesh(meshes[0].geometry, meshes[0].material, this.maxInstances);\n                this.mergedGeoMesh.castShadow = this._castShadow;\n                this.mergedGeoMesh.receiveShadow = this._receiveShadow;\n                this.mergedGeoMesh.renderOrder = this._renderOrder;\n                (this.mergedGeoMesh.material as any).clippingPlanes = this._clippingPlanes;\n                if (this.target) {\n                    this.target.add(this.mergedGeoMesh);\n                }\n            }\n            this.mergedGeoMesh.updateFromMeshes(meshes);\n        }\n        else {\n            if (this.mergedGeoMesh) {\n                if (this.target) {\n                    this.target.remove(this.mergedGeoMesh);\n                }\n                this.mergedGeoMesh.dispose();\n                this.mergedGeoMesh = undefined;\n            }\n        }\n    }\n    update(): void {\n    }\n    dispose(): void {\n        if (this.mergedGeoMesh) {\n            this.mergedGeoMesh.dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/drawable/PalDrawable.ts",
    "content": "import { RgbaBitmap } from '../../../data/Bitmap';\nimport { Palette } from '../../../data/Palette';\nexport class PalDrawable {\n    private pal: Palette;\n    constructor(palette: Palette) {\n        this.pal = palette;\n    }\n    draw(): RgbaBitmap {\n        const size = this.pal.size;\n        const bitmap = new RgbaBitmap(size, 1);\n        let dataIndex = 0;\n        for (let i = 0; i < size; i++) {\n            const color = this.pal.getColor(i);\n            bitmap.data[dataIndex] = color.r;\n            bitmap.data[dataIndex + 1] = color.g;\n            bitmap.data[dataIndex + 2] = color.b;\n            bitmap.data[dataIndex + 3] = i ? 255 : 0;\n            dataIndex += 4;\n        }\n        return bitmap;\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/drawable/TmpDrawable.ts",
    "content": "import { IndexedBitmap } from '@/data/Bitmap';\ninterface TileData {\n    tileData: number[];\n    x: number;\n    y: number;\n    extraX?: number;\n    extraY?: number;\n    hasExtraData?: boolean;\n    extraWidth?: number;\n    extraHeight?: number;\n    extraData?: number[];\n}\nexport class TmpDrawable {\n    drawTileBlock(tile: TileData, bitmap: IndexedBitmap, width: number, height: number, offsetX: number, offsetY: number): void {\n        const data = bitmap.data;\n        const halfHeight = height / 2;\n        let pos = width / 2 - 2 + bitmap.width * offsetY + offsetX;\n        const totalPixels = bitmap.width * bitmap.height;\n        let tileIndex = 0;\n        let row = 0;\n        let rowWidth = 0;\n        for (; row < halfHeight; row++) {\n            rowWidth += 4;\n            for (let i = 0; i < rowWidth; i++) {\n                const pixel = tile.tileData[tileIndex];\n                if (pixel !== 0 && pos >= 0 && pos < totalPixels) {\n                    data[pos] = pixel;\n                }\n                pos++;\n                tileIndex++;\n            }\n            pos += bitmap.width - (rowWidth + 2);\n        }\n        pos += 4;\n        for (; row < height; row++) {\n            rowWidth -= 4;\n            for (let i = 0; i < rowWidth; i++) {\n                const pixel = tile.tileData[tileIndex];\n                if (pos >= 0 && pos < totalPixels) {\n                    data[pos] = pixel;\n                }\n                pos++;\n                tileIndex++;\n            }\n            pos += bitmap.width - (rowWidth - 2);\n        }\n    }\n    draw(tile: TileData, width: number, height: number): IndexedBitmap {\n        let finalWidth = width;\n        let finalHeight = height;\n        let offsetX = 0;\n        let offsetY = 0;\n        if (tile.hasExtraData) {\n            offsetX += Math.max(0, tile.x - (tile.extraX ?? 0));\n            offsetY += Math.max(0, tile.y - (tile.extraY ?? 0));\n            finalWidth += Math.max(0, tile.x - (tile.extraX ?? 0));\n            finalHeight += Math.max(0, tile.y - (tile.extraY ?? 0));\n        }\n        const bitmap = new IndexedBitmap(finalWidth, finalHeight);\n        this.drawTileBlock(tile, bitmap, width, height, offsetX, offsetY);\n        if (tile.hasExtraData) {\n            this.drawExtraData(tile, bitmap);\n        }\n        return bitmap;\n    }\n    drawExtraData(tile: TileData, bitmap: IndexedBitmap): void {\n        if (!tile.hasExtraData)\n            return;\n        const data = bitmap.data;\n        const width = bitmap.width;\n        const height = bitmap.height;\n        const extraOffsetX = Math.max(0, (tile.extraX ?? 0) - tile.x);\n        const stride = width;\n        const totalPixels = width * height;\n        let pos = stride * Math.max(0, (tile.extraY ?? 0) - tile.y) + extraOffsetX;\n        let extraIndex = 0;\n        for (let y = 0; y < (tile.extraHeight ?? 0); y++) {\n            for (let x = 0; x < (tile.extraWidth ?? 0); x++) {\n                const pixel = tile.extraData?.[extraIndex];\n                if (pixel !== 0 && pos >= 0 && pos < totalPixels) {\n                    data[pos] = pixel;\n                }\n                pos++;\n                extraIndex++;\n            }\n            pos += stride - (tile.extraWidth ?? 0);\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/geometry/BufferGeometrySerializer.ts",
    "content": "import { DataStream } from '../../../data/DataStream';\nimport * as THREE from 'three';\nexport class BufferGeometrySerializer {\n    serialize(geometry: THREE.BufferGeometry): ArrayBuffer {\n        if (Object.keys(geometry.morphAttributes).length) {\n            throw new Error('Morph attributes are not supported');\n        }\n        if (geometry.groups.length > 1) {\n            throw new Error('Groups are not supported');\n        }\n        const attributeNames = Object.keys(geometry.attributes);\n        const index = geometry.index;\n        const bufferSize = 1 +\n            22 * attributeNames.length +\n            Object.values(geometry.attributes)\n                .map(attr => this.getTypedArrayByteSize(attr.array))\n                .reduce((sum, size) => sum + size, 0) +\n            1 +\n            (index ? this.getTypedArrayByteSize(index.array) : 0);\n        const stream = new DataStream(new ArrayBuffer(bufferSize));\n        stream.writeUint8(attributeNames.length);\n        for (const name of attributeNames) {\n            const attribute = geometry.getAttribute(name);\n            stream.writeString(name, 'ASCII', 20);\n            stream.writeUint8(attribute.itemSize);\n            stream.writeUint8(Number(attribute.normalized));\n            this.writeTypedArray(stream, attribute.array);\n        }\n        stream.writeUint8(Number(Boolean(index)));\n        if (index) {\n            this.writeTypedArray(stream, index.array);\n        }\n        stream.seek(0);\n        stream.dynamicSize = false;\n        return stream.buffer;\n    }\n    unserialize(stream: DataStream): THREE.BufferGeometry {\n        const geometry = new THREE.BufferGeometry();\n        const attributeCount = stream.readUint8();\n        for (let i = 0; i < attributeCount; i++) {\n            const name = stream.readCString(20);\n            const itemSize = stream.readUint8();\n            const normalized = Boolean(stream.readUint8());\n            const array = this.readTypedArray(stream);\n            const attribute = new THREE.BufferAttribute(array, itemSize, normalized);\n            geometry.setAttribute(name, attribute);\n        }\n        if (Boolean(stream.readUint8())) {\n            const indexArray = this.readTypedArray(stream);\n            geometry.setIndex(new THREE.BufferAttribute(indexArray, 1));\n        }\n        return geometry;\n    }\n    writeTypedArray(stream: DataStream, array: ArrayLike<number> & {\n        length: number;\n    }): void {\n        stream.writeUint32(array.length);\n        if (array instanceof Float32Array) {\n            stream.writeUint8(0);\n            for (let i = 0; i < array.length; i++) {\n                stream.writeFloat32(array[i]);\n            }\n        }\n        else if (array instanceof Uint32Array) {\n            stream.writeUint8(1);\n            for (let i = 0; i < array.length; i++) {\n                stream.writeUint32(array[i]);\n            }\n        }\n        else if (array instanceof Uint16Array) {\n            stream.writeUint8(2);\n            for (let i = 0; i < array.length; i++) {\n                stream.writeUint16(array[i]);\n            }\n        }\n        else {\n            throw new Error(`Unsupported array type \"${(array as any).constructor.name}\"`);\n        }\n    }\n    readTypedArray(stream: DataStream): Float32Array | Uint32Array | Uint16Array {\n        const length = stream.readUint32();\n        const type = stream.readUint8();\n        switch (type) {\n            case 0:\n                return stream.readFloat32Array(length);\n            case 1:\n                return stream.readUint32Array(length);\n            case 2:\n                return stream.readUint16Array(length);\n            default:\n                throw new Error(`Unsupported array type \"${type}\"`);\n        }\n    }\n    getTypedArrayByteSize(array: ArrayLike<number> & {\n        BYTES_PER_ELEMENT: number;\n        length: number;\n    }): number {\n        return 5 + array.BYTES_PER_ELEMENT * array.length;\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/geometry/VxlGeometryCache.ts",
    "content": "import { DataStream } from '../../../data/DataStream';\nimport { VirtualFile } from '../../../data/vfs/VirtualFile';\nimport { BufferGeometrySerializer } from './BufferGeometrySerializer';\nimport { FileNotFoundError } from '../../../data/vfs/FileNotFoundError';\nimport * as THREE from 'three';\ninterface VirtualFileSystem {\n    openFile(filename: string): Promise<{\n        stream: DataStream;\n    }>;\n    writeFile(file: VirtualFile): Promise<void>;\n    getEntries(): AsyncIterable<string>;\n    deleteFile(filename: string): Promise<void>;\n}\ninterface VxlFile {\n    name: string;\n}\nexport class VxlGeometryCache {\n    static cacheFilePrefix = 'geocache_';\n    private cacheDir: VirtualFileSystem | null;\n    private activeMod: string | null;\n    private geometries: Map<VxlFile, THREE.BufferGeometry>;\n    constructor(cacheDir: VirtualFileSystem | null, activeMod: string | null) {\n        this.cacheDir = cacheDir;\n        this.activeMod = activeMod;\n        this.geometries = new Map();\n    }\n    async loadFromStorage(vxlFile: VxlFile, filename: string): Promise<THREE.BufferGeometry | undefined> {\n        let geometry = this.geometries.get(vxlFile);\n        if (!geometry) {\n            const cacheDir = this.cacheDir;\n            if (cacheDir) {\n                const cacheFileName = this.getCacheFileName(filename, vxlFile.name);\n                try {\n                    const file = await cacheDir.openFile(cacheFileName);\n                    geometry = new BufferGeometrySerializer().unserialize(file.stream);\n                    this.set(vxlFile, geometry);\n                }\n                catch (error) {\n                    if (!(error instanceof FileNotFoundError)) {\n                        console.error(`Failed to load buffer geometry from cache file \"${cacheFileName}\"`, error);\n                    }\n                }\n            }\n        }\n        return geometry;\n    }\n    async persistToStorage(vxlFile: VxlFile, filename: string, data: ArrayBuffer): Promise<void> {\n        if (!this.geometries.has(vxlFile)) {\n            this.set(vxlFile, new BufferGeometrySerializer().unserialize(new DataStream(data)));\n        }\n        await this.cacheDir?.writeFile(new VirtualFile(new DataStream(data), this.getCacheFileName(filename, vxlFile.name)));\n    }\n    async clearStorage(): Promise<void> {\n        await this.clearStorageFiles();\n    }\n    async clearOtherModStorage(): Promise<void> {\n        const prefix = VxlGeometryCache.cacheFilePrefix + this.getModPrefix();\n        await this.clearStorageFiles((filename) => !filename.startsWith(prefix));\n    }\n    async clearStorageFiles(filter: (filename: string) => boolean = () => true): Promise<void> {\n        const cacheDir = this.cacheDir;\n        if (cacheDir) {\n            for await (const entry of cacheDir.getEntries()) {\n                if (entry.startsWith(VxlGeometryCache.cacheFilePrefix) && filter(entry)) {\n                    await cacheDir.deleteFile(entry);\n                }\n            }\n        }\n    }\n    getCacheFileName(filename: string, vxlName: string): string {\n        const modPrefix = this.getModPrefix();\n        return VxlGeometryCache.cacheFilePrefix + modPrefix + filename.replace('.vxl', '') + '_' + vxlName;\n    }\n    getModPrefix(): string {\n        return this.activeMod ? this.activeMod + '#' : '#';\n    }\n    clear(): void {\n        this.geometries.forEach((geometry) => {\n            geometry.dispose();\n            for (const attributeName of Object.keys(geometry.attributes)) {\n                geometry.deleteAttribute(attributeName);\n            }\n        });\n        this.geometries.clear();\n    }\n    get(vxlFile: VxlFile): THREE.BufferGeometry | undefined {\n        return this.geometries.get(vxlFile);\n    }\n    set(vxlFile: VxlFile, geometry: THREE.BufferGeometry): void {\n        this.geometries.set(vxlFile, geometry);\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/lighting/LightingDirector.ts",
    "content": "import { LightingFx } from './LightingFx';\nimport { MapLighting } from '@/data/map/MapLighting';\nimport { Lighting } from '@/engine/Lighting';\nexport class LightingDirector {\n    private lighting: Lighting;\n    private renderer: {\n        onFrame: {\n            subscribe: (callback: (time: number) => void) => void;\n            unsubscribe: (callback: (time: number) => void) => void;\n        };\n    };\n    private gameSpeed: {\n        value: number;\n    };\n    private effects: LightingFx[];\n    private onFrame: (time: number) => void;\n    constructor(lighting: Lighting, renderer: {\n        onFrame: {\n            subscribe: (callback: (time: number) => void) => void;\n            unsubscribe: (callback: (time: number) => void) => void;\n        };\n    }, gameSpeed: {\n        value: number;\n    }) {\n        this.lighting = lighting;\n        this.renderer = renderer;\n        this.gameSpeed = gameSpeed;\n        this.effects = [];\n        this.onFrame = (time: number) => {\n            if (this.effects.length) {\n                let needsUpdate = false;\n                this.effects.slice().forEach((effect, index) => {\n                    if (!effect.isRunning) {\n                        effect.isRunning = true;\n                        effect.startTime = time;\n                        effect.mapLighting.copy(this.lighting.getBaseAmbient());\n                    }\n                    const result = effect.update(time, this.gameSpeed.value);\n                    if (result.done) {\n                        this.effects.splice(this.effects.indexOf(effect), 1);\n                        if (!index) {\n                            needsUpdate = true;\n                        }\n                    }\n                    if (!index && result.updated) {\n                        this.lighting.applyAmbientOverride(effect.mapLighting);\n                    }\n                });\n                if (this.effects.length) {\n                    if (needsUpdate) {\n                        this.lighting.applyAmbientOverride(this.effects[0].mapLighting);\n                    }\n                }\n                else {\n                    this.lighting.applyAmbientOverride(undefined);\n                }\n            }\n        };\n    }\n    init(): void {\n        this.renderer.onFrame.subscribe(this.onFrame);\n    }\n    addEffect(effect: LightingFx): void {\n        this.effects.push(effect);\n        this.effects.sort((a, b) => b.priority - a.priority);\n    }\n    dispose(): void {\n        this.renderer.onFrame.unsubscribe(this.onFrame);\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/lighting/LightingFx.ts",
    "content": "import { MapLighting } from '@/data/map/MapLighting';\nexport enum LightingFxPriority {\n    Normal = 0,\n    High = 1\n}\nexport class LightingFx {\n    priority: LightingFxPriority;\n    mapLighting: MapLighting;\n    isRunning: boolean;\n    startTime?: number;\n    constructor() {\n        this.priority = LightingFxPriority.Normal;\n        this.mapLighting = new MapLighting();\n        this.isRunning = false;\n    }\n    update(time: number, gameSpeed: number): {\n        done: boolean;\n        updated?: boolean;\n    } {\n        return { done: true };\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/lighting/LightningStormFx.ts",
    "content": "import { LightingFx } from './LightingFx';\nexport class LightningStormFx extends LightingFx {\n    private durationGameSeconds: number;\n    private ionLighting: any;\n    private cloudAnims: any[];\n    constructor(durationGameSeconds: number, ionLighting: any) {\n        super();\n        this.durationGameSeconds = durationGameSeconds;\n        this.ionLighting = ionLighting;\n        this.cloudAnims = [];\n    }\n    waitForCloudAnim(anim: any): void {\n        this.cloudAnims.push(anim);\n    }\n    update(time: number, gameSpeed: number): {\n        done: boolean;\n        updated: boolean;\n    } {\n        let updated = false;\n        let done = false;\n        if (time === this.startTime) {\n            this.mapLighting.copy(this.ionLighting);\n            updated = true;\n        }\n        if (((time - this.startTime) / 1000) * gameSpeed > this.durationGameSeconds &&\n            !this.cloudAnims.some(anim => !anim.isAnimFinished())) {\n            done = true;\n        }\n        return { done, updated };\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/lighting/NukeLightingFx.ts",
    "content": "import { LightingFx, LightingFxPriority } from './LightingFx';\nexport class NukeLightingFx extends LightingFx {\n    private initialAmbient?: number;\n    constructor() {\n        super();\n        this.priority = LightingFxPriority.High;\n    }\n    update(time: number, gameSpeed: number): {\n        done: boolean;\n        updated: boolean;\n    } {\n        let updated = false;\n        let done = false;\n        if (!this.initialAmbient) {\n            this.initialAmbient = this.mapLighting.ambient;\n        }\n        let newAmbient: number | undefined;\n        const elapsedSeconds = ((time - this.startTime!) / 1000) * gameSpeed;\n        let progress: number;\n        if (elapsedSeconds >= 3.3) {\n            const remainingTime = elapsedSeconds - 3.3;\n            progress = Math.min(1, remainingTime / 0.5);\n            newAmbient = this.initialAmbient + 1.5 * (1 - progress);\n            if (progress === 1) {\n                done = true;\n            }\n        }\n        else if (elapsedSeconds < 0.3) {\n            progress = elapsedSeconds / 0.3;\n            newAmbient = this.initialAmbient + 1.5 * progress;\n        }\n        if (newAmbient !== undefined && this.mapLighting.ambient !== newAmbient) {\n            updated = true;\n            this.mapLighting.ambient = newAmbient;\n        }\n        return { done, updated };\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/material/PaletteBasicMaterial.ts",
    "content": "import { paletteShaderLib } from \"./paletteShaderLib\";\nimport * as THREE from 'three';\nconst PaletteBasicShader = {\n    uniforms: THREE.UniformsUtils.merge([\n        THREE.ShaderLib.basic.uniforms,\n        paletteShaderLib.uniforms,\n    ]),\n    vertexShader: THREE.ShaderChunk.meshbasic_vert\n        .replace(\"#include <common>\", \"#include <common>\\n\" +\n        [\n            paletteShaderLib.instanceParsVertex,\n            paletteShaderLib.paletteColorParsVertex,\n            paletteShaderLib.vertexColorMultParsVertex,\n        ].join(\"\\n\"))\n        .replace(\"void main() {\", \"void main() {\\n\" +\n        [\n            paletteShaderLib.instanceVertex,\n            paletteShaderLib.paletteColorVertex,\n            paletteShaderLib.vertexColorMultVertex,\n        ].join(\"\\n\")),\n    fragmentShader: THREE.ShaderChunk.meshbasic_frag\n        .replace(\"#include <common>\", \"#include <common>\\n\" +\n        [\n            paletteShaderLib.paletteColorParsFrag,\n            paletteShaderLib.vertexColorMultParsFrag,\n        ].join(\"\\n\"))\n        .replace(\"#include <color_fragment>\", \"#include <color_fragment>\\n\" +\n        [\n            paletteShaderLib.paletteColorFrag,\n            paletteShaderLib.paletteBasicLightFragment,\n            paletteShaderLib.vertexColorMultFrag,\n        ].join(\"\\n\")),\n};\nexport class PaletteBasicMaterial extends THREE.MeshBasicMaterial {\n    uniforms: any;\n    vertexShader: string;\n    fragmentShader: string;\n    get palette() {\n        return this.uniforms.palette.value;\n    }\n    set palette(value) {\n        this.uniforms.palette.value = value;\n    }\n    get paletteOffset() {\n        return this.uniforms.paletteOffsetCount.value[0];\n    }\n    set paletteOffset(value) {\n        this.uniforms.paletteOffsetCount.value[0] = value;\n    }\n    get paletteCount() {\n        return this.uniforms.paletteOffsetCount.value[1];\n    }\n    set paletteCount(value) {\n        this.uniforms.paletteOffsetCount.value[1] = value;\n    }\n    get extraLight() {\n        return this.uniforms.extraLight.value;\n    }\n    set extraLight(value) {\n        this.uniforms.extraLight.value = value;\n    }\n    set useVertexColorMult(value) {\n        if (value) {\n            this.defines = this.defines || {};\n            this.defines.USE_VERTEX_COLOR_MULT = \"\";\n        }\n        else if (this.defines) {\n            delete this.defines.USE_VERTEX_COLOR_MULT;\n        }\n    }\n    constructor({ palette, paletteCount, paletteOffset, extraLight, useVertexColorMult, flatShading, useRedIndex, ...options }: any = {}) {\n        if (options.side === undefined) {\n            options.side = THREE.DoubleSide;\n        }\n        super(options);\n        this.uniforms = THREE.UniformsUtils.clone(PaletteBasicShader.uniforms);\n        if (palette) {\n            this.palette = palette;\n        }\n        if (paletteCount) {\n            this.paletteCount = paletteCount;\n        }\n        if (paletteOffset) {\n            this.paletteOffset = paletteOffset;\n        }\n        if (extraLight) {\n            this.extraLight.copy(extraLight);\n        }\n        if (useVertexColorMult) {\n            this.useVertexColorMult = useVertexColorMult;\n        }\n        this.vertexShader = PaletteBasicShader.vertexShader;\n        this.fragmentShader = PaletteBasicShader.fragmentShader;\n        if (useRedIndex) {\n            this.defines = this.defines || {};\n            this.defines.USE_RED_INDEX = '';\n        }\n        this.type = \"PaletteBasicMaterial\";\n        this.onBeforeCompile = (shader: any) => {\n            shader.uniforms = THREE.UniformsUtils.merge([shader.uniforms, this.uniforms]);\n            shader.vertexShader = this.vertexShader;\n            shader.fragmentShader = this.fragmentShader;\n            this.userData.lastCompiledShader = {\n                vertexShader: shader.vertexShader,\n                fragmentShader: shader.fragmentShader,\n                uniforms: Object.keys(shader.uniforms),\n            };\n            console.log('[PaletteBasicMaterial] compiled', {\n                type: this.type,\n                hasMap: !!this.map,\n                defines: this.defines,\n                hasColorFragmentInclude: shader.fragmentShader.includes('#include <color_fragment>'),\n                hasPaletteColorIndex: shader.fragmentShader.includes('paletteColorIndex'),\n            });\n        };\n        this.needsUpdate = true;\n    }\n    copy(source) {\n        super.copy(source);\n        this.fragmentShader = source.fragmentShader;\n        this.vertexShader = source.vertexShader;\n        this.uniforms = THREE.UniformsUtils.clone(source.uniforms);\n        this.palette = source.palette;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/material/PaletteLambertMaterial.ts",
    "content": "import { paletteShaderLib } from \"./paletteShaderLib\";\nimport * as THREE from 'three';\nconst PaletteLambertShader = {\n    uniforms: THREE.UniformsUtils.merge([\n        THREE.ShaderLib.lambert.uniforms,\n        paletteShaderLib.uniforms,\n    ]),\n    vertexShader: THREE.ShaderChunk.meshlambert_vert\n        .replace(\"#include <common>\", \"#include <common>\\n\" + paletteShaderLib.instanceParsVertex)\n        .replace(\"void main() {\", \"void main() {\\n\" + paletteShaderLib.instanceVertex),\n    fragmentShader: THREE.ShaderChunk.meshlambert_frag\n        .replace(\"#include <common>\", \"#include <common>\\n\" + paletteShaderLib.paletteColorParsFrag)\n        .replace(\"#include <color_fragment>\", \"#include <color_fragment>\\n\" + paletteShaderLib.paletteColorFrag)\n        .replace(\"#include <lights_fragment_end>\", \"#include <lights_fragment_end>\\n\" + paletteShaderLib.paletteFullLightFragment),\n};\nexport class PaletteLambertMaterial extends THREE.MeshLambertMaterial {\n    uniforms: any;\n    vertexShader: string;\n    fragmentShader: string;\n    get palette() {\n        return this.uniforms.palette.value;\n    }\n    set palette(value) {\n        this.uniforms.palette.value = value;\n    }\n    get paletteOffset() {\n        return this.uniforms.paletteOffsetCount.value[0];\n    }\n    set paletteOffset(value) {\n        this.uniforms.paletteOffsetCount.value[0] = value;\n    }\n    get paletteCount() {\n        return this.uniforms.paletteOffsetCount.value[1];\n    }\n    set paletteCount(value) {\n        this.uniforms.paletteOffsetCount.value[1] = value;\n    }\n    get extraLight() {\n        return this.uniforms?.extraLight.value;\n    }\n    set extraLight(value) {\n        this.uniforms.extraLight.value = value;\n    }\n    constructor({ palette, paletteCount, paletteOffset, extraLight, ...options } = {}) {\n        super(options);\n        this.uniforms = THREE.UniformsUtils.clone(PaletteLambertShader.uniforms);\n        if (palette)\n            this.palette = palette;\n        if (paletteCount)\n            this.paletteCount = paletteCount;\n        if (paletteOffset)\n            this.paletteOffset = paletteOffset;\n        if (extraLight)\n            this.extraLight.copy(extraLight);\n        this.vertexShader = PaletteLambertShader.vertexShader;\n        this.fragmentShader = PaletteLambertShader.fragmentShader;\n        this.type = \"PaletteLambertMaterial\";\n    }\n    copy(source: PaletteLambertMaterial): this {\n        super.copy(source);\n        this.fragmentShader = source.fragmentShader;\n        this.vertexShader = source.vertexShader;\n        this.uniforms = THREE.UniformsUtils.clone(source.uniforms);\n        this.palette = source.palette;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/material/PalettePhongMaterial.ts",
    "content": "import * as THREE from 'three';\nimport { paletteShaderLib } from '@/engine/gfx/material/paletteShaderLib';\ninterface PalettePhongMaterialParameters extends THREE.MeshPhongMaterialParameters {\n    palette?: THREE.Texture;\n    paletteCount?: number;\n    paletteOffset?: number;\n    extraLight?: THREE.Vector3;\n}\ninterface ShaderMaterial {\n    uniforms: {\n        [uniform: string]: THREE.IUniform;\n    };\n    vertexShader: string;\n    fragmentShader: string;\n}\nconst shaderMaterial: ShaderMaterial = {\n    uniforms: THREE.UniformsUtils.merge([\n        THREE.ShaderLib.phong.uniforms,\n        paletteShaderLib.uniforms,\n    ]),\n    vertexShader: THREE.ShaderChunk.meshphong_vert\n        .replace(\"#include <common>\", \"#include <common>\\n\" + paletteShaderLib.instanceParsVertex)\n        .replace(\"void main() {\", \"void main() {\\n\" + paletteShaderLib.instanceVertex),\n    fragmentShader: THREE.ShaderChunk.meshphong_frag\n        .replace(\"#include <common>\", \"#include <common>\\n\" + paletteShaderLib.paletteColorParsFrag)\n        .replace(\"#include <color_fragment>\", \"#include <color_fragment>\\n\" + paletteShaderLib.paletteColorFrag)\n        .replace(\"#include <lights_fragment_end>\", \"#include <lights_fragment_end>\\n\" + paletteShaderLib.paletteFullLightFragment),\n};\nexport class PalettePhongMaterial extends THREE.MeshPhongMaterial {\n    public uniforms: {\n        [uniform: string]: THREE.IUniform;\n    };\n    public vertexShader: string;\n    public fragmentShader: string;\n    constructor(parameters: PalettePhongMaterialParameters = {}) {\n        const { palette, paletteCount, paletteOffset, extraLight, ...materialParams } = parameters;\n        super(materialParams);\n        this.uniforms = THREE.UniformsUtils.clone(shaderMaterial.uniforms);\n        if (palette) {\n            this.palette = palette;\n        }\n        if (paletteCount !== undefined) {\n            this.paletteCount = paletteCount;\n        }\n        if (paletteOffset !== undefined) {\n            this.paletteOffset = paletteOffset;\n        }\n        if (extraLight) {\n            this.extraLight.copy(extraLight);\n        }\n        this.vertexShader = shaderMaterial.vertexShader;\n        this.fragmentShader = shaderMaterial.fragmentShader;\n        this.type = \"PalettePhongMaterial\";\n    }\n    get palette(): THREE.Texture {\n        return this.uniforms.palette.value;\n    }\n    set palette(value: THREE.Texture) {\n        this.uniforms.palette.value = value;\n    }\n    get paletteOffset(): number {\n        return this.uniforms.paletteOffsetCount.value[0];\n    }\n    set paletteOffset(value: number) {\n        this.uniforms.paletteOffsetCount.value[0] = value;\n    }\n    get paletteCount(): number {\n        return this.uniforms.paletteOffsetCount.value[1];\n    }\n    set paletteCount(value: number) {\n        this.uniforms.paletteOffsetCount.value[1] = value;\n    }\n    get extraLight(): THREE.Vector3 {\n        return this.uniforms?.extraLight.value;\n    }\n    set extraLight(value: THREE.Vector3) {\n        this.uniforms.extraLight.value = value;\n    }\n    copy(source: PalettePhongMaterial): this {\n        super.copy(source);\n        this.fragmentShader = source.fragmentShader;\n        this.vertexShader = source.vertexShader;\n        this.uniforms = THREE.UniformsUtils.clone(source.uniforms);\n        this.palette = source.palette;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/engine/gfx/material/paletteShaderLib.ts",
    "content": "import * as THREE from 'three';\nexport const paletteShaderLib = {\n    uniforms: {\n        palette: { type: \"t\", value: null },\n        paletteOffsetCount: { value: [0, 1] },\n        extraLight: { value: new THREE.Vector3(0, 0, 0) },\n    },\n    instanceParsVertex: `\n#ifdef INSTANCE_TRANSFORM\n    attribute float instancePaletteOffset;\n    varying float vInstancePaletteOffset;\n    attribute vec3 instanceExtraLight;\n    varying vec3 vInstanceExtraLight;\n#endif\n`,\n    instanceVertex: `\n  #ifdef INSTANCE_TRANSFORM\n    vInstancePaletteOffset = instancePaletteOffset;\n    vInstanceExtraLight = instanceExtraLight;\n  #endif\n`,\n    paletteColorParsVertex: `\n#ifdef VERTEX_PALETTE_OFFSET\n    attribute float vertexPaletteOffset;\n    varying float vVertexPaletteOffset;\n#endif\n`,\n    paletteColorVertex: `\n  #ifdef VERTEX_PALETTE_OFFSET\n    vVertexPaletteOffset = vertexPaletteOffset;\n  #endif\n`,\n    paletteColorParsFrag: `\nuniform sampler2D palette;\n#ifdef VERTEX_PALETTE_OFFSET\n    varying float vVertexPaletteOffset;\n#endif\nuniform vec2 paletteOffsetCount;\nuniform vec3 extraLight;\n\n#ifdef INSTANCE_TRANSFORM\nvarying float vInstancePaletteOffset;\nvarying vec3 vInstanceExtraLight;\n#endif\n`,\n    paletteColorFrag: `\n  float paletteColorIndex;\n\n  #ifdef USE_MAP\n  #ifdef USE_RED_INDEX\n  paletteColorIndex = sampledDiffuseColor.r;\n  #else\n  paletteColorIndex = sampledDiffuseColor.a;\n  #endif\n  #endif\n\n  #ifdef USE_COLOR\n  paletteColorIndex = vColor.r;\n  #endif\n\n  #ifdef INSTANCE_TRANSFORM\n  diffuseColor = texture2D(palette, vec2(paletteColorIndex, (vInstancePaletteOffset + 0.5) / paletteOffsetCount.y));\n  #elif defined(VERTEX_PALETTE_OFFSET)\n  diffuseColor = texture2D(palette, vec2(paletteColorIndex, (vVertexPaletteOffset + 0.5) / paletteOffsetCount.y));\n  #else\n  diffuseColor = texture2D(palette, vec2(paletteColorIndex, (paletteOffsetCount.x + 0.5) / paletteOffsetCount.y));\n  #endif\n\n  #ifdef INSTANCE_OPACITY\n  diffuseColor.a *= vInstanceOpacity * opacity;\n  #else\n  diffuseColor.a *= opacity;\n  #endif\n  diffuseColor = clamp(diffuseColor, 0.0, 1.0);\n`,\n    paletteBasicLightFragment: `\n  #ifdef INSTANCE_TRANSFORM\n  diffuseColor.rgb += vInstanceExtraLight.rgb * diffuseColor.rgb;\n  #else\n  diffuseColor.rgb += extraLight.rgb * diffuseColor.rgb;\n  #endif\n\n  diffuseColor = clamp(diffuseColor, 0.0, 1.0);\n`,\n    paletteFullLightFragment: `\n  #ifdef INSTANCE_TRANSFORM\n  vec3 extraIrradiance = vInstanceExtraLight.rgb;\n  #else\n  vec3 extraIrradiance = extraLight.rgb;\n  #endif\n\n  #if ( NUM_DIR_LIGHTS > 0 )\n    #pragma unroll_loop_start\n    for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n      vec3 lightDirection = normalize( directionalLights[ i ].direction );\n      float dotNL = saturate( dot( geometryNormal, lightDirection ) );\n      vec3 customIrradiance = dotNL * directionalLights[ i ].color * extraIrradiance;\n      \n      reflectedLight.directDiffuse += customIrradiance * BRDF_Lambert( material.diffuseColor );\n      #ifdef USE_PHONG\n        reflectedLight.directSpecular += customIrradiance * BRDF_BlinnPhong( lightDirection, geometryViewDir, geometryNormal, material.specularColor, material.specularShininess ) * material.specularStrength;\n      #endif\n    }\n    #pragma unroll_loop_end\n  #endif\n\n  vec3 ambientIrradiance = getAmbientLightIrradiance( ambientLightColor );\n  ambientIrradiance *= extraIrradiance;\n  reflectedLight.indirectDiffuse += ambientIrradiance * BRDF_Lambert( material.diffuseColor );\n`,\n    vertexColorMultParsVertex: `\n#ifdef USE_VERTEX_COLOR_MULT\nattribute vec4 vertexColorMult;\nvarying vec4 vVertexColorMult;\n#endif\n`,\n    vertexColorMultVertex: `\n  #ifdef USE_VERTEX_COLOR_MULT\n  vVertexColorMult = vertexColorMult;\n  #endif\n`,\n    vertexColorMultParsFrag: `\n#ifdef USE_VERTEX_COLOR_MULT\nvarying vec4 vVertexColorMult;\n#endif\n`,\n    vertexColorMultFrag: `\n  #ifdef USE_VERTEX_COLOR_MULT\n  diffuseColor.rgba *= vVertexColorMult.rgba;\n  #endif\n`,\n};\n"
  },
  {
    "path": "src/engine/mixDatabase.ts",
    "content": "export const mixDatabase = new Map<string, string[]>()\n    .set(\"cameo.mix\", [\n    \"adogicon.shp\", \"adoguico.shp\", \"aengicon.shp\", \"aenguico.shp\", \"agapgen.shp\",\n    \"agisicon.shp\", \"ahrvicon.shp\", \"ahrvuico.shp\", \"aparicon.shp\", \"apchicon.shp\",\n    \"apcicon.shp\", \"artyicon.shp\", \"asaticon.shp\", \"ayaricon.shp\", \"batricon.shp\",\n    \"beagicon.shp\", \"bggyicon.shp\", \"bolticon.shp\", \"brrkicon.shp\", \"carricon.shp\",\n    \"ccomicon.shp\", \"ccomuico.shp\", \"chemicon.shp\", \"chroicon.shp\", \"clckicon.shp\",\n    \"clegicon.shp\", \"cleguico.shp\", \"clonicon.shp\", \"cnsticon.shp\", \"crryicon.shp\",\n    \"csphicon.shp\", \"darken.shp\", \"desoicon.shp\", \"desouico.shp\", \"desticon.shp\",\n    \"detnicon.shp\", \"dlphicon.shp\", \"dlphuico.shp\", \"dogicon.shp\", \"doguico.shp\",\n    \"dredicon.shp\", \"dronicon.shp\", \"e1icon.shp\", \"e1uico.shp\", \"e2icon.shp\",\n    \"e2uico.shp\", \"e4icon.shp\", \"empicon.shp\", \"engnicon.shp\", \"facticon.shp\",\n    \"falcicon.shp\", \"fixicon.shp\", \"flakicon.shp\", \"flkticon.shp\", \"flktuico.shp\",\n    \"forticon.shp\", \"fsdicon.shp\", \"fspicon.shp\", \"fstdicon.shp\", \"fvicon.shp\",\n    \"fvuico.shp\", \"gapicon.shp\", \"gat2icon.shp\", \"gateicon.shp\", \"gbayicon.shp\",\n    \"gcanicon.shp\", \"giicon.shp\", \"giuico.shp\", \"gorep.shp\", \"gtnkicon.shp\",\n    \"gtnkuico.shp\", \"gwepicon.shp\", \"handicon.shp\", \"harvicon.shp\", \"harvuico.shp\",\n    \"heliicon.shp\", \"hindicon.shp\", \"hmecicon.shp\", \"hovricon.shp\", \"htkicon.shp\",\n    \"htkuico.shp\", \"htnkicon.shp\", \"htnkuico.shp\", \"ioncicon.shp\", \"ircricon.shp\",\n    \"ironicon.shp\", \"ivanicon.shp\", \"ivanuico.shp\", \"ivncicon.shp\", \"ivncuico.shp\",\n    \"jjeticon.shp\", \"jjetuico.shp\", \"landicon.shp\", \"lasricon.shp\", \"liteicon.shp\",\n    \"lpsticon.shp\", \"mcvicon.shp\", \"mcvuico.shp\", \"metricon.shp\", \"mltiicon.shp\",\n    \"mmchicon.shp\", \"msslicon.shp\", \"mtnkicon.shp\", \"mtnkuico.shp\", \"mutcicon.shp\",\n    \"nga2icon.shp\", \"ngaticon.shp\", \"nhpdicon.shp\", \"npsiicon.shp\", \"npwricon.shp\",\n    \"nradicon.shp\", \"nrcticon.shp\", \"nreficon.shp\", \"ntchicon.shp\", \"nukeicon.shp\",\n    \"nwalicon.shp\", \"nwepicon.shp\", \"obliicon.shp\", \"obmbicon.shp\", \"orcaicon.shp\",\n    \"otrnicon.shp\", \"paraicon.shp\", \"pillicon.shp\", \"plticon.shp\", \"plugicon.shp\",\n    \"podsicon.shp\", \"powricon.shp\", \"prisicon.shp\", \"proicon.shp\", \"psicicon.shp\",\n    \"psicuico.shp\", \"psisicon.shp\", \"psiticon.shp\", \"psituico.shp\", \"rad1icon.shp\",\n    \"rad2icon.shp\", \"rad3icon.shp\", \"radricon.shp\", \"rboticon.shp\", \"reficon.shp\",\n    \"rfixicon.shp\", \"rtnkicon.shp\", \"rtnkuico.shp\", \"samicon.shp\", \"sapcicon.shp\",\n    \"sapicon.shp\", \"sbagicon.shp\", \"sealicon.shp\", \"sealuico.shp\", \"seekicon.shp\",\n    \"shadicon.shp\", \"shaduico.shp\", \"shkicon.shp\", \"shkuico.shp\", \"smchicon.shp\",\n    \"smcvicon.shp\", \"smcvuico.shp\", \"snipicon.shp\", \"snipuico.shp\", \"soniicon.shp\",\n    \"spoticon.shp\", \"spyicon.shp\", \"spyuico.shp\", \"sqdicon.shp\", \"sreficon.shp\",\n    \"srefuico.shp\", \"stnkicon.shp\", \"subicon.shp\", \"subticon.shp\", \"tanyicon.shp\",\n    \"tanyuico.shp\", \"techicon.shp\", \"tempicon.shp\", \"teslaicon.shp\", \"tickicon.shp\",\n    \"tnkdicon.shp\", \"tnkduico.shp\", \"towricon.shp\", \"trkaicon.shp\", \"trsticon.shp\",\n    \"trstuico.shp\", \"tslaicon.shp\", \"ttnkicon.shp\", \"ttnkuico.shp\", \"turbicon.shp\",\n    \"twr1icon.shp\", \"twr2icon.shp\", \"twr3icon.shp\", \"v3icon.shp\", \"v3uico.shp\",\n    \"wallicon.shp\", \"weapicon.shp\", \"weaticon.shp\", \"weedicon.shp\", \"wethicon.shp\",\n    \"xxicon.shp\", \"yardicon.shp\", \"yuriicon.shp\", \"yuriuico.shp\", \"yurpicon.shp\",\n    \"yurpuico.shp\", \"zepicon.shp\", \"zepuico.shp\",\n])\n    .set(\"theme.mix\", [\n    \"200meter.wav\", \"blowitup.wav\", \"burn.wav\", \"destroy.wav\", \"eaglehun.wav\",\n    \"fortific.wav\", \"grinder.wav\", \"hm2.wav\", \"indeep.wav\", \"industro.wav\",\n    \"jank.wav\", \"motorize.wav\", \"power.wav\", \"ra2-opt.wav\", \"ra2-sco.wav\",\n    \"tension.wav\",\n]);\nconst sideBarFiles = [\n    \"addon.shp\", \"bkgdlg.shp\", \"bkgdmd.shp\", \"bkgdsm.shp\", \"bttnbkgd.shp\",\n    \"button00.shp\", \"button01.shp\", \"button02.shp\", \"button03.shp\", \"button04.shp\",\n    \"button05.shp\", \"button06.shp\", \"button07.shp\", \"button08.shp\", \"button09.shp\",\n    \"button10.shp\", \"button11.shp\", \"credits.shp\", \"diplobtn.shp\", \"gclock2.shp\",\n    \"key.ini\", \"lendcap.shp\", \"lspacer.shp\", \"optbtn.shp\", \"pbeacon.shp\",\n    \"power.shp\", \"powerp.shp\", \"pwrlvl.shp\", \"radar.shp\", \"radar01.shp\",\n    \"radar02.shp\", \"r-dn.shp\", \"rdrbeacn.shp\", \"rendcap.shp\", \"repair.shp\",\n    \"r-up.shp\", \"sell.shp\", \"side1.shp\", \"side2.shp\", \"side2b.shp\",\n    \"side3.shp\", \"sidebar.pal\", \"sidebttn.shp\", \"tab00.shp\", \"tab01.shp\",\n    \"tab02.shp\", \"tab03.shp\", \"tabs.shp\", \"top.shp\", \"uibkgd.pal\",\n    \"wayp.shp\",\n];\nmixDatabase.set(\"sidec01.mix\", sideBarFiles);\nmixDatabase.set(\"sidec02.mix\", sideBarFiles);\nconst sideBarCdFiles = [\"reportbug.shp\"];\nmixDatabase.set(\"sidec01cd.mix\", sideBarCdFiles);\nmixDatabase.set(\"sidec02cd.mix\", sideBarCdFiles);\n"
  },
  {
    "path": "src/engine/renderable/AlphaRenderable.ts",
    "content": "import { Palette } from \"@/data/Palette\";\nimport { ShpBuilder } from \"@/engine/renderable/builder/ShpBuilder\";\nimport { Color } from \"@/util/Color\";\nimport { Coords } from \"@/game/Coords\";\nimport * as THREE from \"three\";\nexport class AlphaRenderable {\n    private static alphaPalette?: Palette;\n    private shpFile: any;\n    private camera: THREE.Camera;\n    private visible: boolean;\n    private drawOffset: any;\n    private shpSize?: number;\n    private builder?: ShpBuilder;\n    private object3d?: THREE.Object3D;\n    static getOrCreateAlphaPalette(): Palette {\n        let palette = AlphaRenderable.alphaPalette;\n        if (!palette) {\n            palette = new Palette(new Array(768).fill(0));\n            const colors: Color[] = [];\n            for (let i = 0; i < 256; i++) {\n                const value = i > 127 ? 2 * (i - 127) : 0;\n                colors.push(new Color(value, value, value));\n            }\n            palette.setColors(colors);\n            AlphaRenderable.alphaPalette = palette;\n        }\n        return palette;\n    }\n    constructor(shpFile: any, camera: THREE.Camera, drawOffset: any) {\n        this.shpFile = shpFile;\n        this.camera = camera;\n        this.visible = true;\n        this.drawOffset = { ...drawOffset };\n    }\n    setVisible(visible: boolean): void {\n        this.visible = visible;\n        if (this.object3d) {\n            this.object3d.visible = visible;\n        }\n    }\n    setSize(size: number): void {\n        this.shpSize = size;\n        this.builder?.setSize(size);\n    }\n    create3DObject(): void {\n        if (!this.object3d) {\n            const palette = AlphaRenderable.getOrCreateAlphaPalette();\n            const builder = new ShpBuilder(this.shpFile, palette, this.camera, Coords.ISO_WORLD_SCALE);\n            if (this.shpSize) {\n                builder.setSize(this.shpSize);\n            }\n            builder.setFrame(0);\n            builder.setOffset(this.drawOffset);\n            const object = builder.build();\n            object.visible = this.visible;\n            object.renderOrder = 999995;\n            const material = object.material as THREE.Material;\n            material.depthTest = false;\n            material.depthWrite = true;\n            material.transparent = true;\n            material.blending = THREE.CustomBlending;\n            material.blendEquation = THREE.AddEquation;\n            material.blendSrc = THREE.DstColorFactor;\n            material.blendDst = THREE.OneFactor;\n            this.builder = builder;\n            this.object3d = object;\n        }\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.object3d;\n    }\n    update(delta: number): void { }\n    dispose(): void {\n        this.builder?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/CameraPan.ts",
    "content": "import { clamp } from '../../util/math';\nimport { BoxedVar } from '../../util/BoxedVar';\nexport class CameraPan {\n    private freeCamera: BoxedVar<boolean>;\n    private pan: {\n        x: number;\n        y: number;\n    };\n    private panLimits?: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    constructor(freeCamera: BoxedVar<boolean>) {\n        this.freeCamera = freeCamera;\n        this.pan = { x: 0, y: 0 };\n    }\n    setPanLimits(limits: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }): void {\n        this.panLimits = limits;\n        this.setPan({ x: this.pan.x, y: this.pan.y });\n    }\n    getPanLimits(): {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    } {\n        return { ...this.panLimits! };\n    }\n    getPan(): {\n        x: number;\n        y: number;\n    } {\n        return { ...this.pan };\n    }\n    setPan(pan: {\n        x: number;\n        y: number;\n    }): void {\n        if (this.panLimits && !this.freeCamera.value) {\n            pan.x = clamp(pan.x, this.panLimits.x, this.panLimits.x + this.panLimits.width);\n            pan.y = clamp(pan.y, this.panLimits.y, this.panLimits.y + this.panLimits.height);\n        }\n        this.pan = { x: pan.x, y: pan.y };\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/CameraZoom.ts",
    "content": "import { BoxedVar } from '../../util/BoxedVar';\nexport class CameraZoom {\n    private freeCamera: BoxedVar<boolean>;\n    private zoom: number;\n    constructor(freeCamera: BoxedVar<boolean>) {\n        this.freeCamera = freeCamera;\n        this.zoom = 1;\n    }\n    getZoom(): number {\n        return this.zoom;\n    }\n    applyStep(step: number): void {\n        if (this.freeCamera.value) {\n            this.zoom = Math.max(0.1, this.zoom + step);\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/DebugRenderable.ts",
    "content": "import { Palette } from \"@/data/Palette\";\nimport { DebugUtils } from \"@/engine/gfx/DebugUtils\";\nimport { TextureUtils } from \"@/engine/gfx/TextureUtils\";\nimport { PaletteBasicMaterial } from \"@/engine/gfx/material/PaletteBasicMaterial\";\nimport { Mesh, Texture, BufferGeometry } from \"three\";\nimport { BatchedMesh, BatchMode } from \"@/engine/gfx/batch/BatchedMesh\";\ninterface Foundation {\n    width: number;\n    height: number;\n}\ninterface DebugRenderableOptions {\n    centerFoundation?: boolean;\n}\ninterface MaterialCacheEntry {\n    material: PaletteBasicMaterial;\n    usages: number;\n}\nexport class DebugRenderable {\n    private static checkerboardTex?: Texture;\n    private static geometryCache = new Map<string, BufferGeometry>();\n    private static materialCache = new Map<string, MaterialCacheEntry>();\n    private foundation: Foundation;\n    private height: number;\n    private palette: Palette;\n    private options?: DebugRenderableOptions;\n    private batchPalettes: Palette[] = [];\n    private useMeshBatching: boolean = false;\n    private opacity: number = 1;\n    private mesh?: Mesh | BatchedMesh;\n    private materialCacheKey?: string;\n    static getOrCreateTexture(): Texture {\n        let texture = DebugRenderable.checkerboardTex;\n        if (!texture) {\n            texture = DebugUtils.createIndexedCheckerTex(Palette.REMAP_START_IDX - 1, Palette.REMAP_START_IDX);\n            DebugRenderable.checkerboardTex = texture;\n        }\n        return texture;\n    }\n    static clearCaches(): void {\n        DebugRenderable.checkerboardTex?.dispose();\n        DebugRenderable.geometryCache.forEach((geometry) => geometry.dispose());\n        DebugRenderable.geometryCache.clear();\n    }\n    constructor(foundation: Foundation, height: number, palette: Palette, options?: DebugRenderableOptions) {\n        this.foundation = foundation;\n        this.height = height;\n        this.palette = palette;\n        this.options = options;\n    }\n    private useMaterial(paletteTexture: Texture): PaletteBasicMaterial {\n        this.materialCacheKey = paletteTexture.uuid;\n        let cacheEntry = DebugRenderable.materialCache.get(this.materialCacheKey);\n        let material: PaletteBasicMaterial;\n        if (cacheEntry) {\n            material = cacheEntry.material;\n            cacheEntry.usages++;\n        }\n        else {\n            material = new PaletteBasicMaterial({\n                map: DebugRenderable.getOrCreateTexture(),\n                palette: paletteTexture,\n                alphaTest: 0.05,\n                paletteCount: this.batchPalettes.length,\n                flatShading: true,\n                transparent: true,\n            });\n            cacheEntry = { material, usages: 1 };\n            DebugRenderable.materialCache.set(this.materialCacheKey, cacheEntry);\n        }\n        return material;\n    }\n    private freeMaterial(): void {\n        if (!this.materialCacheKey) {\n            throw new Error(\"Material cache key not set\");\n        }\n        const cacheEntry = DebugRenderable.materialCache.get(this.materialCacheKey);\n        if (cacheEntry) {\n            if (cacheEntry.usages === 1) {\n                DebugRenderable.materialCache.delete(this.materialCacheKey);\n                cacheEntry.material.dispose();\n            }\n            else {\n                cacheEntry.usages--;\n            }\n        }\n    }\n    private getGeometryCacheKey(): string {\n        return `${this.foundation.width}_${this.foundation.height}_${this.height}`;\n    }\n    setBatched(useBatching: boolean): void {\n        if (this.mesh) {\n            throw new Error(\"Batching can only be set before calling build()\");\n        }\n        this.useMeshBatching = useBatching;\n    }\n    private getBatchPaletteIndex(palette: Palette): number {\n        const index = this.batchPalettes.findIndex((p) => p.hash === palette.hash);\n        if (index === -1) {\n            throw new Error(\"Provided palette not found in the list of batch palettes. Call setBatchPalettes first.\");\n        }\n        return index;\n    }\n    setPalette(palette: Palette): void {\n        this.palette = palette;\n        if (this.mesh) {\n            if (this.useMeshBatching) {\n                const paletteIndex = this.getBatchPaletteIndex(palette);\n                (this.mesh as BatchedMesh).setPaletteIndex(paletteIndex);\n            }\n            else {\n                const paletteTexture = TextureUtils.textureFromPalette(palette);\n                const material = (this.mesh as Mesh).material as PaletteBasicMaterial;\n                material.palette = paletteTexture;\n            }\n        }\n    }\n    setBatchPalettes(palettes: Palette[]): void {\n        if (!this.useMeshBatching) {\n            throw new Error(\"Can't use multiple palettes when not batching\");\n        }\n        if (this.mesh) {\n            throw new Error(\"Palettes must be set before creating 3DObject\");\n        }\n        this.batchPalettes = palettes;\n    }\n    setOpacity(opacity: number): void {\n        if (this.opacity !== opacity) {\n            this.opacity = opacity;\n            this.updateOpacity();\n        }\n    }\n    private updateOpacity(): void {\n        if (this.mesh) {\n            if (this.useMeshBatching) {\n                (this.mesh as BatchedMesh).setOpacity(this.opacity);\n            }\n            else {\n                ((this.mesh as Mesh).material as PaletteBasicMaterial).opacity = this.opacity;\n            }\n        }\n    }\n    create3DObject(): void {\n        if (!this.mesh) {\n            const cacheKey = this.getGeometryCacheKey();\n            const geometryCache = DebugRenderable.geometryCache;\n            let geometry = geometryCache.get(cacheKey);\n            if (!geometry) {\n                geometry = DebugUtils.createBoxGeometry(this.foundation, this.height, this.options?.centerFoundation);\n                geometryCache.set(cacheKey, geometry);\n            }\n            let mesh: Mesh | BatchedMesh;\n            if (this.useMeshBatching) {\n                const paletteTexture = TextureUtils.textureFromPalettes(this.batchPalettes);\n                const material = this.useMaterial(paletteTexture);\n                mesh = new BatchedMesh(geometry, material, BatchMode.Merging);\n                mesh.castShadow = false;\n            }\n            else {\n                const paletteTexture = TextureUtils.textureFromPalette(this.palette);\n                const checkerTexture = DebugRenderable.getOrCreateTexture();\n                const material = new PaletteBasicMaterial({\n                    palette: paletteTexture,\n                    map: checkerTexture,\n                    alphaTest: 0.05,\n                    transparent: true,\n                });\n                mesh = new Mesh(geometry, material);\n            }\n            mesh.matrixAutoUpdate = false;\n            this.mesh = mesh;\n            this.setPalette(this.palette);\n            this.updateOpacity();\n        }\n    }\n    get3DObject(): Mesh | BatchedMesh | undefined {\n        return this.mesh;\n    }\n    update(deltaTime: number): void {\n    }\n    dispose(): void {\n        if (this.mesh) {\n            if (this.useMeshBatching) {\n                this.freeMaterial();\n            }\n            else {\n                ((this.mesh as Mesh).material as PaletteBasicMaterial).dispose();\n            }\n            this.mesh = undefined;\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/Entity.ts",
    "content": "import { WithPosition } from \"./WithPosition\";\nimport { WithVisibility } from \"./WithVisibility\";\nimport * as THREE from \"three\";\nexport class Entity {\n    private position: WithPosition;\n    private visibility: WithVisibility;\n    private target?: THREE.Object3D;\n    constructor() {\n        this.position = new WithPosition();\n        this.visibility = new WithVisibility();\n        this.position.applyTo(this);\n        this.visibility.applyTo(this);\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    set3DObject(object: THREE.Object3D): void {\n        this.target = object;\n        this.position.updatePosition();\n        this.visibility.updateVisibility();\n    }\n    setPosition(x: number, y: number, z: number): void {\n        this.position.setPosition(x, y, z);\n    }\n    getPosition(): THREE.Vector3 {\n        return this.position.getPosition();\n    }\n    setVisible(visible: boolean): void {\n        this.visibility.setVisible(visible);\n    }\n    isVisible(): boolean {\n        return this.visibility.isVisible();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/MapSpriteTranslation.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { IsoCoords } from \"@/engine/IsoCoords\";\nimport * as THREE from \"three\";\nexport class MapSpriteTranslation {\n    private rx: number;\n    private ry: number;\n    constructor(rx: number, ry: number) {\n        this.rx = rx;\n        this.ry = ry;\n    }\n    compute(): {\n        spriteOffset: THREE.Vector2;\n        anchorPointWorld: THREE.Vector3;\n    } {\n        let worldPos = Coords.tileToWorld(this.rx, this.ry);\n        let screenPos = IsoCoords.worldToScreen(worldPos.x, worldPos.y);\n        let originScreen = IsoCoords.worldToScreen(0, 0);\n        let spriteOffset = new THREE.Vector2(originScreen.x - screenPos.x, originScreen.y - screenPos.y);\n        let yRemainder = spriteOffset.y - Math.floor(spriteOffset.y);\n        if (yRemainder !== 0) {\n            spriteOffset.y -= yRemainder;\n            originScreen = new THREE.Vector2(originScreen.x - spriteOffset.x, originScreen.y - spriteOffset.y);\n            worldPos = IsoCoords.screenToWorld(originScreen.x, originScreen.y);\n        }\n        return {\n            spriteOffset,\n            anchorPointWorld: worldPos as any\n        };\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/Renderable.ts",
    "content": "export { Renderable } from '@/engine/gfx/Renderable';\n"
  },
  {
    "path": "src/engine/renderable/RenderablePlugin.ts",
    "content": "export class RenderablePlugin {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/ShadowRenderable.ts",
    "content": "import { Palette } from \"@/data/Palette\";\nimport { ShpBuilder } from \"@/engine/renderable/builder/ShpBuilder\";\nimport { Coords } from \"@/game/Coords\";\nimport { IsoCoords } from \"@/engine/IsoCoords\";\nimport { MAGIC_OFFSET } from \"@/engine/renderable/entity/map/MapSurface\";\nimport * as THREE from \"three\";\nexport class ShadowRenderable {\n    private static shadowPalette: Palette;\n    private shpFile: any;\n    private camera: any;\n    private shadowHeightTileAdjust: number;\n    private baseFrameNo: number;\n    private frameOffset: number;\n    private visible: boolean;\n    private useBatching: boolean;\n    private drawOffset: {\n        x: number;\n        y: number;\n    };\n    private builder?: ShpBuilder;\n    private object3d?: THREE.Object3D;\n    private shpSize?: number;\n    static getOrCreateShadowPalette(): Palette {\n        let palette = ShadowRenderable.shadowPalette;\n        if (!palette) {\n            palette = new Palette(new Array(768).fill(0));\n            ShadowRenderable.shadowPalette = palette;\n        }\n        return palette;\n    }\n    constructor(shpFile: any, camera: any, drawOffset: {\n        x: number;\n        y: number;\n    }, shadowHeightTileAdjust: number = 0) {\n        this.shpFile = shpFile;\n        this.camera = camera;\n        this.shadowHeightTileAdjust = shadowHeightTileAdjust;\n        this.baseFrameNo = 0;\n        this.frameOffset = 0;\n        this.visible = true;\n        this.useBatching = false;\n        this.drawOffset = { ...drawOffset };\n    }\n    setVisible(visible: boolean): void {\n        this.visible = visible;\n        if (this.object3d) {\n            const frameNo = this.computeShadowFrameNo(this.baseFrameNo);\n            this.object3d.visible = visible && this.frameHasShadowData(frameNo);\n        }\n    }\n    setSize(size: number): void {\n        this.shpSize = size;\n        this.builder?.setSize(size);\n    }\n    setBatched(batched: boolean): void {\n        this.useBatching = batched;\n        this.builder?.setBatched(batched);\n    }\n    setBaseFrame(frameNo: number): void {\n        this.baseFrameNo = frameNo;\n        if (this.builder) {\n            const shadowFrameNo = this.computeShadowFrameNo(frameNo);\n            this.builder.setFrame(shadowFrameNo);\n            this.object3d!.visible = this.visible && this.frameHasShadowData(shadowFrameNo);\n        }\n    }\n    setFrameOffset(offset: number): void {\n        this.frameOffset = offset;\n        this.builder?.setFrameOffset(offset);\n    }\n    computeShadowFrameNo(frameNo: number): number {\n        return frameNo < this.shpFile.numImages ? this.shpFile.numImages / 2 + frameNo : 1;\n    }\n    create3DObject(): void {\n        if (!this.object3d) {\n            const palette = ShadowRenderable.getOrCreateShadowPalette();\n            const builder = new ShpBuilder(this.shpFile, palette, this.camera, Coords.ISO_WORLD_SCALE);\n            if (this.shpSize) {\n                builder.setSize(this.shpSize);\n            }\n            builder.setFrameOffset(this.frameOffset);\n            builder.setBatched(this.useBatching);\n            if (this.useBatching) {\n                builder.setBatchPalettes([palette]);\n            }\n            builder.flat = true;\n            const shadowFrameNo = this.computeShadowFrameNo(this.baseFrameNo);\n            builder.setFrame(shadowFrameNo);\n            if (this.shadowHeightTileAdjust) {\n                const heightAdjust = IsoCoords.tileHeightToScreen(this.shadowHeightTileAdjust);\n                this.drawOffset.y += -heightAdjust;\n            }\n            builder.setOffset(this.drawOffset);\n            builder.setOpacity(0.5);\n            const object = builder.build();\n            if (this.shadowHeightTileAdjust) {\n                object.position.y += Coords.tileHeightToWorld(-this.shadowHeightTileAdjust);\n                object.updateMatrix();\n            }\n            object.visible = this.visible && this.frameHasShadowData(shadowFrameNo);\n            object.position.y += MAGIC_OFFSET / 5;\n            object.updateMatrix();\n            this.builder = builder;\n            this.object3d = object;\n        }\n    }\n    frameHasShadowData(frameNo: number): boolean {\n        return !!this.shpFile.getImage(this.frameOffset + frameNo).imageData.length;\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.object3d;\n    }\n    update(delta: number): void { }\n    dispose(): void {\n        this.builder?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/ShpRenderable.ts",
    "content": "import { ShpBuilder } from \"./builder/ShpBuilder\";\nimport { ShadowRenderable } from \"./ShadowRenderable\";\nimport { Coords } from \"@/game/Coords\";\nimport * as THREE from \"three\";\nexport class ShpRenderable {\n    private builder: ShpBuilder;\n    private shadowRenderable?: ShadowRenderable;\n    private zShapeFixBuilder?: ShpBuilder;\n    private target?: THREE.Object3D;\n    private shapeMesh?: THREE.Object3D;\n    private shadowMesh?: THREE.Object3D;\n    static factory(shpFile: any, palette: any, camera: any, drawOffset: {\n        x: number;\n        y: number;\n    }, hasShadow: boolean = false, shadowHeightTileAdjust: number = 0, useBatching: boolean = false, frameOffset: number = 0, hasZShapeFix: boolean = false): ShpRenderable {\n        const shadowRenderable = hasShadow\n            ? new ShadowRenderable(shpFile, camera, drawOffset, shadowHeightTileAdjust)\n            : undefined;\n        const isoWorldScale = Coords.ISO_WORLD_SCALE;\n        let builder = new ShpBuilder(shpFile, palette, camera, isoWorldScale, useBatching, frameOffset);\n        builder.setOffset(drawOffset);\n        let zShapeFixBuilder: ShpBuilder | undefined;\n        if (hasZShapeFix) {\n            zShapeFixBuilder = new ShpBuilder(shpFile, palette, camera, isoWorldScale, useBatching, frameOffset);\n            zShapeFixBuilder.setOffset(drawOffset);\n            zShapeFixBuilder.flat = true;\n        }\n        return new ShpRenderable(builder, shadowRenderable, zShapeFixBuilder);\n    }\n    constructor(builder: ShpBuilder, shadowRenderable?: ShadowRenderable, zShapeFixBuilder?: ShpBuilder) {\n        this.builder = builder;\n        this.shadowRenderable = shadowRenderable;\n        this.zShapeFixBuilder = zShapeFixBuilder;\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    setBatched(batched: boolean): void {\n        this.builder.setBatched(batched);\n        this.zShapeFixBuilder?.setBatched(batched);\n        this.shadowRenderable?.setBatched(batched);\n    }\n    setBatchPalettes(palettes: any[]): void {\n        this.builder.setBatchPalettes(palettes);\n        this.zShapeFixBuilder?.setBatchPalettes(palettes);\n    }\n    setSize(size: number): void {\n        this.builder.setSize(size);\n        this.zShapeFixBuilder?.setSize(size);\n        this.shadowRenderable?.setSize(size);\n    }\n    getFlat(): boolean {\n        return this.builder.flat;\n    }\n    setFlat(flat: boolean): void {\n        this.builder.flat = flat;\n    }\n    setFrame(frame: number): void {\n        if (this.builder.getFrame() !== frame) {\n            this.builder.setFrame(frame);\n            this.zShapeFixBuilder?.setFrame(frame);\n            this.shadowRenderable?.setBaseFrame(frame);\n        }\n    }\n    setFrameOffset(offset: number): void {\n        this.builder.setFrameOffset(offset);\n        this.zShapeFixBuilder?.setFrameOffset(offset);\n        this.shadowRenderable?.setFrameOffset(offset);\n    }\n    setPalette(palette: any): void {\n        this.builder.setPalette(palette);\n        this.zShapeFixBuilder?.setPalette(palette);\n    }\n    setExtraLight(light: any): void {\n        this.builder.setExtraLight(light);\n        this.zShapeFixBuilder?.setExtraLight(light);\n    }\n    setOpacity(opacity: number): void {\n        this.builder.setOpacity(opacity);\n        this.zShapeFixBuilder?.setOpacity(opacity);\n    }\n    setForceTransparent(transparent: boolean): void {\n        this.builder.setForceTransparent(transparent);\n        this.zShapeFixBuilder?.setForceTransparent(transparent);\n    }\n    get frameCount(): number {\n        return this.shadowRenderable\n            ? this.builder.frameCount / 2\n            : this.builder.frameCount;\n    }\n    getShapeMesh(): THREE.Object3D | undefined {\n        return this.shapeMesh;\n    }\n    getShadowMesh(): THREE.Object3D | undefined {\n        return this.shadowMesh;\n    }\n    setShadowVisible(visible: boolean): void {\n        this.shadowRenderable?.setVisible(visible);\n    }\n    create3DObject(): void {\n        if (!this.target) {\n            this.shapeMesh = this.builder.build();\n            if (this.shadowRenderable || this.zShapeFixBuilder) {\n                const container = new THREE.Object3D();\n                container.matrixAutoUpdate = false;\n                container.add(this.shapeMesh);\n                if (this.shadowRenderable) {\n                    this.shadowRenderable.create3DObject();\n                    this.shadowMesh = this.shadowRenderable.get3DObject();\n                    container.add(this.shadowMesh);\n                }\n                if (this.zShapeFixBuilder) {\n                    const zShapeFixMesh = this.zShapeFixBuilder.build();\n                    container.add(zShapeFixMesh);\n                }\n                this.target = container;\n            }\n            else {\n                this.target = this.shapeMesh;\n            }\n        }\n    }\n    update(delta: number): void { }\n    dispose(): void {\n        this.builder.dispose();\n        this.zShapeFixBuilder?.dispose();\n        this.shadowRenderable?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/WithPosition.ts",
    "content": "import * as THREE from \"three\";\nexport class WithPosition {\n    public matrixUpdate: boolean = false;\n    private position: THREE.Vector3;\n    private target?: any;\n    constructor() {\n        this.position = new THREE.Vector3();\n    }\n    setPosition(x: number, y: number, z: number): void {\n        this.position.x = x;\n        this.position.y = y;\n        this.position.z = z;\n        this.updatePosition();\n    }\n    getPosition(): THREE.Vector3 {\n        return this.position;\n    }\n    updatePosition(): void {\n        if (this.target) {\n            const object = this.target.get3DObject();\n            if (object) {\n                object.position.set(this.position.x, this.position.y, this.position.z);\n                if (this.matrixUpdate) {\n                    object.matrix.setPosition(object.position);\n                    object.matrixWorldNeedsUpdate = true;\n                }\n            }\n        }\n    }\n    applyTo(target: any): void {\n        this.target = target;\n        this.updatePosition();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/WithVisibility.ts",
    "content": "export class WithVisibility {\n    private visible: boolean = true;\n    private target?: any;\n    constructor() {\n        this.visible = true;\n    }\n    setVisible(visible: boolean): void {\n        this.visible = visible;\n        this.updateVisibility();\n    }\n    isVisible(): boolean {\n        return this.visible;\n    }\n    updateVisibility(): void {\n        if (this.target) {\n            const object = this.target.get3DObject();\n            if (object) {\n                object.visible = this.visible;\n            }\n        }\n    }\n    applyTo(target: any): void {\n        this.target = target;\n        this.updateVisibility();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/WorldScene.ts",
    "content": "import { pointEquals } from '../../util/geometry';\nimport { RenderableContainer } from '../gfx/RenderableContainer';\nimport { CameraPan } from './CameraPan';\nimport { CameraZoom } from './CameraZoom';\nimport { LightingType } from '../type/LightingType';\nimport { Coords } from '../../game/Coords';\nimport { EventDispatcher } from '../../util/event';\nimport { ShadowQuality } from './entity/unit/ShadowQuality';\nimport { MeshBatchManager } from '../gfx/batch/MeshBatchManager';\nimport { BoxedVar } from '../../util/BoxedVar';\nimport { setMeshLineViewportResolution } from './fx/MeshLineResolution';\nimport * as THREE from 'three';\nconst AMBIENT_LIGHT_INTENSITY = 0.8;\nconst CAMERA_FAR = 16000;\nconst SHADOW_QUALITY_MAP = new Map([\n    [ShadowQuality.High, 8],\n    [ShadowQuality.Medium, 4],\n    [ShadowQuality.Low, 2]\n]);\nexport class WorldScene extends RenderableContainer {\n    public scene: THREE.Scene;\n    public camera: THREE.OrthographicCamera;\n    public viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    public cameraPan: CameraPan;\n    public cameraZoom: CameraZoom;\n    public shadowQuality: BoxedVar<ShadowQuality>;\n    private initialized: boolean = false;\n    private ambientLight: THREE.AmbientLight;\n    private directionalLight: THREE.DirectionalLight;\n    private _onBeforeCameraUpdate = new EventDispatcher<WorldScene, number>();\n    private _onCameraUpdate = new EventDispatcher<WorldScene, number>();\n    private lastCameraPan?: {\n        x: number;\n        y: number;\n    };\n    private lastCameraZoom?: number;\n    private meshBatchManager?: MeshBatchManager;\n    private shadowQualityListener?: () => void;\n    private lightFocusPoint?: {\n        x: number;\n        y: number;\n    };\n    get onBeforeCameraUpdate() {\n        return this._onBeforeCameraUpdate.asEvent();\n    }\n    get onCameraUpdate() {\n        return this._onCameraUpdate.asEvent();\n    }\n    static factory(viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }, enableLighting: BoxedVar<boolean>, shadowQuality: BoxedVar<ShadowQuality>): WorldScene {\n        let scene = new THREE.Scene();\n        scene.matrixAutoUpdate = false;\n        const camera = WorldScene.createCamera(viewport);\n        const cameraPan = new CameraPan(enableLighting);\n        const cameraZoom = new CameraZoom(enableLighting);\n        return new WorldScene(scene, camera, viewport, cameraPan, cameraZoom, shadowQuality);\n    }\n    static getCameraParams(viewport: {\n        width: number;\n        height: number;\n    }) {\n        const alpha = Coords.ISO_CAMERA_ALPHA;\n        const beta = Coords.ISO_CAMERA_BETA;\n        const worldScale = Coords.ISO_WORLD_SCALE;\n        return {\n            alpha,\n            beta,\n            d: (viewport.height / 2) * Coords.COS_ISO_CAMERA_BETA * worldScale,\n            aspect: viewport.width / viewport.height,\n            far: CAMERA_FAR * worldScale\n        };\n    }\n    static createCamera(viewport: {\n        width: number;\n        height: number;\n    }): THREE.OrthographicCamera {\n        const { alpha, beta, d, aspect, far } = this.getCameraParams(viewport);\n        const camera = new THREE.OrthographicCamera(-d * aspect, d * aspect, d, -d, 0, far);\n        camera.rotation.order = 'YXZ';\n        camera.rotation.y = +beta;\n        camera.rotation.x = -alpha;\n        setMeshLineViewportResolution(camera, viewport.width, viewport.height);\n        return camera;\n    }\n    constructor(scene: THREE.Scene, camera: THREE.OrthographicCamera, viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }, cameraPan: CameraPan, cameraZoom: CameraZoom, shadowQuality: BoxedVar<ShadowQuality>) {\n        super(scene);\n        this.scene = scene;\n        this.camera = camera;\n        this.viewport = viewport;\n        this.cameraPan = cameraPan;\n        this.cameraZoom = cameraZoom;\n        this.shadowQuality = shadowQuality;\n        this.ambientLight = new THREE.AmbientLight(0xFFFFFF, AMBIENT_LIGHT_INTENSITY);\n        this.directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1);\n        this._onBeforeCameraUpdate = new EventDispatcher<WorldScene, number>();\n        this._onCameraUpdate = new EventDispatcher<WorldScene, number>();\n    }\n    updateViewport(viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }): void {\n        this.viewport = viewport;\n        const { d, aspect } = WorldScene.getCameraParams(viewport);\n        const camera = this.camera;\n        camera.left = -d * aspect;\n        camera.right = d * aspect;\n        camera.top = d;\n        camera.bottom = -d;\n        setMeshLineViewportResolution(camera, viewport.width, viewport.height);\n        camera.updateProjectionMatrix();\n    }\n    updateCamera(pan: {\n        x: number;\n        y: number;\n    }, zoom: number): void {\n        const camera = this.camera;\n        camera.updateMatrix();\n        const elements = camera.matrix.elements;\n        const translation = new THREE.Vector3();\n        camera.position.set(0, 0, 0);\n        camera.translateZ(CAMERA_FAR * Coords.ISO_WORLD_SCALE);\n        translation.set(elements[0], elements[1], elements[2]);\n        translation.multiplyScalar((pan.x * (camera.right - camera.left)) / this.viewport.width / camera.zoom);\n        camera.position.add(translation);\n        translation.set(elements[4], elements[5], elements[6]);\n        translation.multiplyScalar((-pan.y * (camera.top - camera.bottom)) / this.viewport.height / camera.zoom);\n        camera.position.add(translation);\n        camera.zoom = zoom;\n        camera.updateProjectionMatrix();\n        camera.updateMatrixWorld(false);\n    }\n    create3DObject(): void {\n        super.create3DObject();\n        if (!this.initialized) {\n            this.initialized = true;\n            this.scene.position.x -= 0.1 * Coords.ISO_WORLD_SCALE;\n            this.scene.position.z -= 0.1 * Coords.ISO_WORLD_SCALE;\n            this.scene.updateMatrix();\n            const axesHelper = new THREE.AxesHelper(Coords.LEPTONS_PER_TILE);\n            this.scene.add(axesHelper);\n            this.scene.add(this.ambientLight);\n            const light = this.directionalLight;\n            light.position.set(-87.012, 204.338, 195.409);\n            if (this.lightFocusPoint) {\n                light.position.x += this.lightFocusPoint.x;\n                light.position.z += this.lightFocusPoint.y;\n                light.target.position.set(this.lightFocusPoint.x, 0, this.lightFocusPoint.y);\n                light.target.updateMatrixWorld(undefined);\n            }\n            this.updateShadowQuality(light, this.shadowQuality.value);\n            this.shadowQualityListener = () => this.updateShadowQuality(light, this.shadowQuality.value);\n            this.shadowQuality.onChange.subscribe(this.shadowQualityListener);\n            this.scene.add(light);\n            this.scene.add(this.camera);\n            this.meshBatchManager = new MeshBatchManager(this);\n            this.add(this.meshBatchManager);\n            (this.scene as any).autoUpdate = false;\n        }\n    }\n    updateShadowQuality(light: THREE.DirectionalLight, quality: ShadowQuality): void {\n        const enableShadows = quality !== ShadowQuality.Off;\n        light.castShadow = enableShadows;\n        if (enableShadows) {\n            const worldScale = Coords.ISO_WORLD_SCALE;\n            const shadowSize = 3500 * worldScale;\n            const shadowCamera = light.shadow.camera as THREE.OrthographicCamera;\n            shadowCamera.right = shadowSize;\n            shadowCamera.left = -shadowSize;\n            shadowCamera.top = shadowSize;\n            shadowCamera.bottom = -shadowSize;\n            shadowCamera.near = -4000 * worldScale;\n            shadowCamera.far = 3000 * worldScale;\n            const shadowMapMultiplier = SHADOW_QUALITY_MAP.get(quality);\n            if (!shadowMapMultiplier) {\n                throw new Error(`Unsupported shadow quality \"${quality}\"`);\n            }\n            light.shadow.mapSize.width = 1024 * shadowMapMultiplier;\n            light.shadow.mapSize.height = 1024 * shadowMapMultiplier;\n        }\n    }\n    setLightFocusPoint(x: number, y: number): void {\n        this.lightFocusPoint = { x, y };\n    }\n    applyLighting(lighting: {\n        computeTint: (type: LightingType) => THREE.Vector3;\n        getAmbientIntensity: () => number;\n    }): void {\n        const tint = lighting.computeTint(LightingType.Ambient);\n        this.ambientLight.color.setRGB(tint.x, tint.y, tint.z);\n        this.directionalLight.color.setRGB(tint.x, tint.y, tint.z);\n        const ambientIntensity = lighting.getAmbientIntensity();\n        this.ambientLight.intensity = ambientIntensity * AMBIENT_LIGHT_INTENSITY;\n        this.directionalLight.intensity = ambientIntensity;\n    }\n    update(deltaTime: number, time?: number): void {\n        super.update(deltaTime);\n        this._onBeforeCameraUpdate.dispatch(this, deltaTime);\n        const zoom = this.cameraZoom.getZoom();\n        const pan = this.cameraPan.getPan();\n        if (!pointEquals(pan, this.lastCameraPan) || this.lastCameraZoom !== zoom) {\n            this.updateCamera(pan, zoom);\n            this.lastCameraZoom = zoom;\n            this.lastCameraPan = pan;\n        }\n        this._onCameraUpdate.dispatch(this, deltaTime);\n        this.scene.updateMatrixWorld(false);\n        this.meshBatchManager?.updateMeshes();\n    }\n    dispose(): void {\n        if (this.shadowQualityListener) {\n            this.shadowQuality.onChange.unsubscribe(this.shadowQualityListener);\n            this.shadowQualityListener = undefined;\n        }\n        (this.directionalLight.shadow.map as any)?.dispose();\n        if (this.meshBatchManager) {\n            this.meshBatchManager.dispose();\n            this.remove(this.meshBatchManager);\n            this.meshBatchManager = undefined;\n        }\n        this.scene.remove(this.ambientLight);\n        this.scene.remove(this.directionalLight);\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/BatchShpBuilder.ts",
    "content": "import * as THREE from 'three';\nimport { ShpTextureAtlas } from './ShpTextureAtlas';\nimport { SpriteUtils } from '../../gfx/SpriteUtils';\nimport { TextureUtils } from '../../gfx/TextureUtils';\nimport { PaletteBasicMaterial } from '../../gfx/material/PaletteBasicMaterial';\nimport { ShpFile } from '../../../data/ShpFile';\ninterface BatchItem {\n    [key: string]: any;\n    position: THREE.Vector3;\n    shpFile: ShpFile;\n    depth: boolean;\n    flat: boolean;\n    frameNo: number;\n    offset: {\n        x: number;\n        y: number;\n    };\n    lightMult?: THREE.Vector3;\n}\nexport class BatchShpBuilder {\n    private shpFile: ShpFile;\n    private palette: any;\n    private camera: THREE.Camera;\n    private textureCache: Map<ShpFile, ShpTextureAtlas>;\n    private opacity: number;\n    private transparent: boolean;\n    private batchSize: number;\n    private scale: number;\n    private specIndexes: Map<BatchItem, number | undefined>;\n    private atlas?: ShpTextureAtlas;\n    private mesh?: THREE.Mesh;\n    private positionAttribute?: THREE.BufferAttribute;\n    private colorMultAttribute?: THREE.BufferAttribute;\n    private firstFreeSpriteIdx: number = -1;\n    get verticesPerSprite(): number {\n        return SpriteUtils.VERTICES_PER_SPRITE;\n    }\n    get trianglesPerSprite(): number {\n        return SpriteUtils.TRIANGLES_PER_SPRITE;\n    }\n    constructor(shpFile: ShpFile, palette: any, camera: THREE.Camera, textureCache: Map<any, any>, opacity: number = 1, transparent: boolean = false, batchSize: number = 10000, scale: number = 1) {\n        this.shpFile = shpFile;\n        this.palette = palette;\n        this.camera = camera;\n        this.textureCache = textureCache;\n        this.opacity = opacity;\n        this.transparent = transparent;\n        this.batchSize = batchSize;\n        this.scale = scale;\n        this.specIndexes = new Map();\n    }\n    private initTexture(): void {\n        if (this.textureCache.has(this.shpFile)) {\n            this.atlas = this.textureCache.get(this.shpFile)!;\n        }\n        else {\n            const atlas = new ShpTextureAtlas().fromShpFile(this.shpFile);\n            this.textureCache.set(this.shpFile, atlas);\n            this.atlas = atlas;\n        }\n    }\n    private getSpriteGeometryOptions(item: BatchItem) {\n        const image = this.shpFile.getImage(item.frameNo);\n        const offset = {\n            x: image.x - item.shpFile.width / 2 + item.offset.x,\n            y: image.y - item.shpFile.height / 2 + item.offset.y,\n        };\n        return {\n            texture: this.atlas!.getTexture(),\n            textureArea: this.atlas!.getTextureArea(item.frameNo),\n            flat: item.flat,\n            depth: item.depth,\n            align: { x: 1, y: -1 },\n            offset: offset,\n            camera: this.camera,\n            scale: this.scale,\n        };\n    }\n    setPalette(palette: any): void {\n        this.palette = palette;\n        if (this.mesh) {\n            const paletteTexture = TextureUtils.textureFromPalette(palette);\n            let material = this.mesh.material as PaletteBasicMaterial;\n            material.palette = paletteTexture;\n        }\n    }\n    build(): THREE.Mesh {\n        if (this.mesh) {\n            return this.mesh;\n        }\n        this.initTexture();\n        const paletteTexture = TextureUtils.textureFromPalette(this.palette);\n        let geometry = new THREE.BufferGeometry();\n        const vertexCount = this.batchSize * this.verticesPerSprite;\n        const positionAttribute = new THREE.BufferAttribute(new Float32Array(3 * vertexCount), 3);\n        geometry.setAttribute(\"position\", positionAttribute);\n        this.positionAttribute = positionAttribute;\n        geometry.setAttribute(\"uv\", new THREE.BufferAttribute(new Float32Array(2 * vertexCount), 2));\n        if (SpriteUtils.USE_INDEXED_GEOMETRY) {\n            const indexCount = this.batchSize * this.trianglesPerSprite * 3;\n            geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(1 * indexCount), 1));\n        }\n        const colorMultAttribute = new THREE.BufferAttribute(new Float32Array(4 * vertexCount), 4);\n        geometry.setAttribute(\"vertexColorMult\", colorMultAttribute);\n        this.colorMultAttribute = colorMultAttribute;\n        let spriteIndex = 0;\n        for (const item of this.specIndexes.keys()) {\n            this.specIndexes.set(item, spriteIndex);\n            this.setSpecGeometry(item, geometry, spriteIndex);\n            spriteIndex++;\n        }\n        this.firstFreeSpriteIdx = spriteIndex < this.batchSize ? spriteIndex : -1;\n        if (spriteIndex < this.batchSize) {\n            let posArray = positionAttribute.array as Float32Array;\n            for (let i = spriteIndex; i < this.batchSize - 1; i++) {\n                posArray[i * this.verticesPerSprite * 3] = i + 1;\n            }\n            posArray[(this.batchSize - 1) * this.verticesPerSprite * 3] = -1;\n        }\n        const material = new PaletteBasicMaterial({\n            map: this.atlas!.getTexture(),\n            palette: paletteTexture,\n            alphaTest: this.transparent ? 0.05 : 0.5,\n            flatShading: true,\n            transparent: this.transparent,\n            opacity: this.opacity,\n            useVertexColorMult: true,\n        });\n        let mesh = new THREE.Mesh(geometry, material);\n        mesh.matrixAutoUpdate = false;\n        mesh.frustumCulled = false;\n        this.mesh = mesh;\n        return mesh;\n    }\n    add(item: BatchItem): void {\n        if (!this.specIndexes.has(item)) {\n            if (this.isFull()) {\n                throw new Error(\"Batch is full\");\n            }\n            const geometry = this.mesh?.geometry;\n            if (geometry) {\n                const spriteIndex = this.firstFreeSpriteIdx;\n                if (spriteIndex === -1) {\n                    throw new Error(\"No free sprite index found\");\n                }\n                this.specIndexes.set(item, spriteIndex);\n                const nextFreeIndex = (this.positionAttribute?.array as Float32Array)[spriteIndex * this.verticesPerSprite * 3];\n                this.setSpecGeometry(item, geometry, spriteIndex);\n                this.firstFreeSpriteIdx = nextFreeIndex;\n            }\n            else {\n                this.specIndexes.set(item, undefined);\n            }\n        }\n    }\n    private setSpecGeometry(item: BatchItem, geometry: THREE.BufferGeometry, spriteIndex: number): void {\n        const options = this.getSpriteGeometryOptions(item);\n        let spriteGeometry = SpriteUtils.createSpriteGeometry(options);\n        const position = item.position;\n        spriteGeometry.applyMatrix4(new THREE.Matrix4().makeTranslation(position.x, position.y, position.z));\n        const posAttr = geometry.getAttribute(\"position\") as THREE.BufferAttribute;\n        const uvAttr = geometry.getAttribute(\"uv\") as THREE.BufferAttribute;\n        const dstPosArray = posAttr.array as Float32Array;\n        const dstUvArray = uvAttr.array as Float32Array;\n        const srcPosAttr = spriteGeometry.getAttribute(\"position\") as THREE.BufferAttribute;\n        const srcUvAttr = spriteGeometry.getAttribute(\"uv\") as THREE.BufferAttribute;\n        if (srcPosAttr.count !== this.verticesPerSprite) {\n            throw new Error(\"Vertex count mismatch\");\n        }\n        for (let i = 0; i < this.verticesPerSprite; i++) {\n            const dstBase = (spriteIndex * this.verticesPerSprite + i) * 3;\n            dstPosArray[dstBase + 0] = srcPosAttr.getX(i);\n            dstPosArray[dstBase + 1] = srcPosAttr.getY(i);\n            dstPosArray[dstBase + 2] = srcPosAttr.getZ(i);\n        }\n        if (srcUvAttr) {\n            for (let i = 0; i < this.verticesPerSprite; i++) {\n                const dstBase = (spriteIndex * this.verticesPerSprite + i) * 2;\n                dstUvArray[dstBase + 0] = srcUvAttr.getX(i);\n                dstUvArray[dstBase + 1] = srcUvAttr.getY(i);\n            }\n        }\n        if (SpriteUtils.USE_INDEXED_GEOMETRY && spriteGeometry.index) {\n            const dstIndex = geometry.getIndex();\n            if (dstIndex) {\n                const dstIndexArray = dstIndex.array as Uint32Array;\n                const srcIndexArray = spriteGeometry.index.array as Uint16Array | Uint32Array;\n                const base = spriteIndex * this.verticesPerSprite;\n                const offset = spriteIndex * spriteGeometry.index.count;\n                for (let i = 0; i < spriteGeometry.index.count; i++) {\n                    dstIndexArray[offset + i] = base + (srcIndexArray as any)[i];\n                }\n                dstIndex.needsUpdate = true;\n            }\n        }\n        const lightMult = item.lightMult ?? new THREE.Vector3(1, 1, 1);\n        this.setLightingAt(spriteIndex, lightMult, this.colorMultAttribute!.array as Float32Array);\n        this.setVisibilityAt(spriteIndex, true, this.colorMultAttribute!.array as Float32Array);\n        posAttr.needsUpdate = true;\n        uvAttr.needsUpdate = true;\n    }\n    has(item: BatchItem): boolean {\n        return this.specIndexes.has(item);\n    }\n    remove(item: BatchItem): void {\n        if (this.specIndexes.has(item)) {\n            if (this.mesh) {\n                const spriteIndex = this.specIndexes.get(item)!;\n                this.setVisibilityAt(spriteIndex, false, this.colorMultAttribute!.array as Float32Array);\n                this.colorMultAttribute!.needsUpdate = true;\n                let posArray = this.positionAttribute!.array as Float32Array;\n                posArray[spriteIndex * this.verticesPerSprite * 3] = this.firstFreeSpriteIdx;\n                this.firstFreeSpriteIdx = spriteIndex;\n            }\n            this.specIndexes.delete(item);\n        }\n    }\n    update(item: BatchItem): void {\n        if (!this.specIndexes.has(item)) {\n            return;\n        }\n        const geometry = this.mesh?.geometry;\n        if (geometry) {\n            this.setSpecGeometry(item, geometry, this.specIndexes.get(item)!);\n        }\n    }\n    isFull(): boolean {\n        return this.specIndexes.size === this.batchSize;\n    }\n    isEmpty(): boolean {\n        return this.specIndexes.size === 0;\n    }\n    private setLightingAt(spriteIndex: number, lightMult: THREE.Vector3, array: Float32Array): void {\n        for (let i = 0; i < this.verticesPerSprite; i++) {\n            array[spriteIndex * this.verticesPerSprite * 4 + 4 * i] = lightMult.x;\n            array[spriteIndex * this.verticesPerSprite * 4 + 4 * i + 1] = lightMult.y;\n            array[spriteIndex * this.verticesPerSprite * 4 + 4 * i + 2] = lightMult.z;\n        }\n    }\n    private setVisibilityAt(spriteIndex: number, visible: boolean, array: Float32Array): void {\n        for (let i = 0; i < this.verticesPerSprite; i++) {\n            array[spriteIndex * this.verticesPerSprite * 4 + 4 * i + 3] = visible ? 1 : 0;\n        }\n    }\n    updateLighting(): void {\n        if (this.mesh) {\n            let colorArray = this.colorMultAttribute!.array as Float32Array;\n            this.specIndexes.forEach((spriteIndex, item) => {\n                const lightMult = item.lightMult ?? new THREE.Vector3(1, 1, 1);\n                this.setLightingAt(spriteIndex!, lightMult, colorArray);\n            });\n            this.colorMultAttribute!.needsUpdate = true;\n        }\n    }\n    dispose(): void {\n        if (this.mesh) {\n            this.mesh.geometry.dispose();\n            const { material } = this.mesh;\n            if (Array.isArray(material)) {\n                material.forEach((entry) => entry.dispose());\n            }\n            else {\n                material.dispose();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/CanvasSpriteBuilder.ts",
    "content": "import * as THREE from 'three';\nimport { SpriteUtils } from '../../gfx/SpriteUtils';\nimport { CanvasTextureAtlas } from './CanvasTextureAtlas';\nexport class CanvasSpriteBuilder {\n    private static textureCache = new Map<HTMLImageElement[], CanvasTextureAtlas>();\n    private images: HTMLImageElement[];\n    private camera: THREE.Camera;\n    private offset: {\n        x: number;\n        y: number;\n    };\n    private align: {\n        x: number;\n        y: number;\n    };\n    private opacity: number;\n    private forceTransparent: boolean;\n    private frustumCulled: boolean;\n    private frameGeometries: Map<number, THREE.BufferGeometry>;\n    private frameNo: number;\n    private atlas?: CanvasTextureAtlas;\n    private mesh?: THREE.Mesh;\n    static clearCaches(): void {\n        CanvasSpriteBuilder.textureCache.clear();\n    }\n    constructor(images: HTMLImageElement[], camera: THREE.Camera) {\n        this.images = images;\n        this.camera = camera;\n        this.offset = { x: 0, y: 0 };\n        this.align = { x: 0, y: 0 };\n        this.opacity = 1;\n        this.forceTransparent = false;\n        this.frustumCulled = false;\n        this.frameGeometries = new Map();\n        this.frameNo = 0;\n        this.setFrame(0);\n    }\n    setOffset(offset: {\n        x: number;\n        y: number;\n    }): void {\n        this.offset = offset;\n    }\n    setAlign(x: number, y: number): void {\n        this.align = { x: x, y: y };\n        if (this.mesh) {\n            this.frameGeometries.get(this.frameNo)?.dispose();\n            const geometry = SpriteUtils.createSpriteGeometry(this.getSpriteGeometryOptions());\n            this.frameGeometries.set(this.frameNo, geometry);\n            this.mesh.geometry = geometry;\n        }\n    }\n    private initTexture(): void {\n        if (CanvasSpriteBuilder.textureCache.has(this.images)) {\n            this.atlas = CanvasSpriteBuilder.textureCache.get(this.images)!;\n        }\n        else {\n            let atlas = new CanvasTextureAtlas();\n            atlas.pack(this.images);\n            CanvasSpriteBuilder.textureCache.set(this.images, atlas);\n            this.atlas = atlas;\n        }\n    }\n    private getSpriteGeometryOptions() {\n        const image = this.images[this.frameNo];\n        const offset = {\n            x: -image.width / 2 - this.align.x * (image.width / 2) + this.offset.x,\n            y: -image.height / 2 - this.align.y * (image.height / 2) + this.offset.y,\n        };\n        return {\n            texture: this.atlas!.getTexture(),\n            textureArea: this.atlas!.getImageRect(image),\n            align: { x: 1, y: -1 },\n            offset: offset,\n            camera: this.camera,\n        };\n    }\n    setFrame(frameNo: number): void {\n        if (this.frameNo !== frameNo) {\n            this.frameNo = frameNo;\n            if (this.mesh) {\n                let geometry = this.frameGeometries.get(frameNo);\n                if (!geometry) {\n                    geometry = SpriteUtils.createSpriteGeometry(this.getSpriteGeometryOptions());\n                    this.frameGeometries.set(frameNo, geometry);\n                }\n                this.mesh.geometry = geometry;\n            }\n        }\n    }\n    getFrame(): number {\n        return this.frameNo;\n    }\n    getSize(): {\n        width: number;\n        height: number;\n    } {\n        return {\n            width: this.images[this.frameNo].width,\n            height: this.images[this.frameNo].height,\n        };\n    }\n    get frameCount(): number {\n        return this.images.length;\n    }\n    setOpacity(opacity: number): void {\n        const oldOpacity = this.opacity;\n        if (oldOpacity !== opacity) {\n            this.opacity = opacity;\n            if (this.mesh) {\n                (this.mesh.material as THREE.MeshBasicMaterial).opacity = opacity;\n            }\n            if (Math.floor(oldOpacity) === Math.floor(opacity) || this.forceTransparent) {\n            }\n            else {\n                this.updateTransparency();\n            }\n        }\n    }\n    setForceTransparent(forceTransparent: boolean): void {\n        if (this.forceTransparent !== forceTransparent) {\n            this.forceTransparent = forceTransparent;\n            this.updateTransparency();\n        }\n    }\n    private updateTransparency(): void {\n        if (this.mesh) {\n            (this.mesh.material as THREE.MeshBasicMaterial).transparent =\n                this.forceTransparent || this.opacity < 1;\n        }\n    }\n    setExtraLight(extraLight: any): void {\n        throw new Error(\"Not implemented\");\n    }\n    setFrustumCulled(frustumCulled: boolean): void {\n        this.frustumCulled = frustumCulled;\n        if (this.mesh) {\n            this.mesh.frustumCulled = frustumCulled;\n        }\n    }\n    build(): THREE.Mesh {\n        if (this.mesh) {\n            return this.mesh;\n        }\n        this.initTexture();\n        const geometry = SpriteUtils.createSpriteGeometry(this.getSpriteGeometryOptions());\n        this.frameGeometries.set(this.frameNo, geometry);\n        const material = new THREE.MeshBasicMaterial({\n            map: this.atlas!.getTexture(),\n            opacity: this.opacity,\n            transparent: this.opacity < 1 || this.forceTransparent,\n        });\n        let mesh = new THREE.Mesh(geometry, material);\n        mesh.matrixAutoUpdate = false;\n        mesh.frustumCulled = this.frustumCulled;\n        this.mesh = mesh;\n        return mesh;\n    }\n    dispose(): void {\n        this.frameGeometries.forEach((geometry) => geometry.dispose());\n        if (this.mesh?.material) {\n            (this.mesh.material as THREE.MeshBasicMaterial).dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/CanvasTextureAtlas.ts",
    "content": "import * as THREE from 'three';\nimport { GrowingPacker, type GrowingPackerBlock } from '@/engine/gfx/GrowingPacker';\ntype CanvasTextureAtlasBlock = GrowingPackerBlock & {\n    image: HTMLImageElement;\n};\nexport class CanvasTextureAtlas {\n    private texture?: THREE.Texture;\n    private imageRects?: Map<HTMLImageElement, {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }>;\n    getTexture(): THREE.Texture {\n        if (!this.texture) {\n            throw new Error(\"Texture atlas not initialized\");\n        }\n        return this.texture;\n    }\n    getImageRect(image: HTMLImageElement): {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    } {\n        if (!this.imageRects) {\n            throw new Error(\"Texture atlas not initialized\");\n        }\n        const rect = this.imageRects.get(image);\n        if (!rect) {\n            throw new Error(\"Image not found in atlas\");\n        }\n        return rect;\n    }\n    pack(images: HTMLImageElement[]): void {\n        const blocks: CanvasTextureAtlasBlock[] = [];\n        images.forEach((image) => {\n            blocks.push({ w: image.width, h: image.height, image: image });\n        });\n        blocks.sort((a, b) => 1000 * (b.w - a.w) + b.h - a.h);\n        const packer = new GrowingPacker();\n        packer.fit(blocks);\n        const atlasWidth = packer.root.w;\n        const atlasHeight = packer.root.h;\n        let canvas = document.createElement(\"canvas\");\n        let context = canvas.getContext(\"2d\", { alpha: true })!;\n        canvas.width = atlasWidth;\n        canvas.height = atlasHeight;\n        let imageRects = new Map<HTMLImageElement, {\n            x: number;\n            y: number;\n            width: number;\n            height: number;\n        }>();\n        blocks.forEach((block) => {\n            if (!block.fit) {\n                throw new Error(\"Couldn't fit all images in a single texture\");\n            }\n            const image = block.image;\n            const x = block.fit.x;\n            const y = block.fit.y;\n            imageRects.set(image, { x: x, y: y, width: block.w, height: block.h });\n            context.drawImage(image, x, y);\n        });\n        let texture = new THREE.Texture(canvas);\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        texture.needsUpdate = true;\n        this.texture = texture;\n        this.imageRects = imageRects;\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/ObjectBuilder.ts",
    "content": "export class ObjectBuilder {\n    constructor() { }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/ShpAggregator.ts",
    "content": "import { ShpFile } from \"@/data/ShpFile\";\nimport { ShpImage } from \"@/data/ShpImage\";\nexport class ShpAggregator {\n    static getShpFrameInfo(file: ShpFile, hasShadow: boolean) {\n        return {\n            file,\n            hasShadow,\n            frameCount: Math.floor(file.numImages * (hasShadow ? 0.5 : 1)),\n        };\n    }\n    aggregate(frames: Array<{\n        file: ShpFile;\n        hasShadow: boolean;\n        frameCount: number;\n    }>, filename: string) {\n        const shpFile = new ShpFile();\n        shpFile.filename = filename;\n        const shadowImages: ShpImage[] = [];\n        const imageIndexes = new Map<ShpFile, number>();\n        let currentIndex = 0;\n        for (const { file, hasShadow, frameCount } of frames) {\n            if (!imageIndexes.has(file)) {\n                imageIndexes.set(file, currentIndex);\n                for (let i = 0; i < frameCount; i++) {\n                    shpFile.addImage(file.getImage(i));\n                    shadowImages.push(hasShadow ? file.getImage(frameCount + i) : new ShpImage());\n                    currentIndex++;\n                }\n            }\n        }\n        shadowImages.forEach(image => shpFile.addImage(image));\n        return {\n            file: shpFile,\n            imageIndexes\n        };\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/ShpBuilder.ts",
    "content": "import * as TextureUtils from \"../../gfx/TextureUtils\";\nimport * as SpriteUtils from \"../../gfx/SpriteUtils\";\nimport { ShpTextureAtlas } from \"./ShpTextureAtlas\";\nimport { PaletteBasicMaterial } from \"../../gfx/material/PaletteBasicMaterial\";\nimport { BatchedMesh, BatchMode } from \"../../gfx/batch/BatchedMesh\";\nimport * as THREE from 'three';\nexport class ShpBuilder {\n    static textureCache = new Map();\n    static geometryCache = new Map();\n    static materialCache = new Map();\n    private scale!: number;\n    private depth!: boolean;\n    private depthOffset!: number;\n    private batchPalettes!: any[];\n    private useMeshBatching!: boolean;\n    private opacity!: number;\n    private forceTransparent!: boolean;\n    private offset!: {\n        x: number;\n        y: number;\n    };\n    private frameOffset!: number;\n    public flat!: boolean;\n    private uiAnchorCompensation!: boolean;\n    private shpFile: any;\n    private palette: any;\n    private camera: any;\n    private shpSize!: {\n        width: number;\n        height: number;\n    };\n    private mesh?: any;\n    private extraLight?: any;\n    private materialCacheKey?: string;\n    private atlas: any;\n    private frameNo?: number;\n    private align?: {\n        x: number;\n        y: number;\n    };\n    static prepareTexture(shpFile) {\n        if (!ShpBuilder.textureCache.has(shpFile)) {\n            const atlas = new ShpTextureAtlas().fromShpFile(shpFile);\n            ShpBuilder.textureCache.set(shpFile, atlas);\n        }\n    }\n    static clearCaches() {\n        ShpBuilder.textureCache.forEach((texture) => texture.dispose());\n        ShpBuilder.textureCache.clear();\n        ShpBuilder.geometryCache.forEach((cache) => cache.forEach((geometry) => geometry.dispose()));\n        ShpBuilder.geometryCache.clear();\n    }\n    constructor(shpFile, palette, camera, scale = 1, depth = false, depthOffset = 0) {\n        this.scale = scale;\n        this.depth = depth;\n        this.depthOffset = depthOffset;\n        this.batchPalettes = [];\n        this.useMeshBatching = false;\n        this.opacity = 1;\n        this.forceTransparent = false;\n        this.offset = { x: 0, y: 0 };\n        this.frameOffset = 0;\n        this.flat = false;\n        this.uiAnchorCompensation = false;\n        this.shpFile = shpFile;\n        this.palette = palette;\n        this.camera = camera;\n        this.shpSize = { width: shpFile.width, height: shpFile.height };\n        this.setFrame(0);\n    }\n    setUiAnchorCompensation(enabled) {\n        if (this.mesh) {\n            throw new Error(\"UI anchor compensation can only be set before calling build()\");\n        }\n        this.uiAnchorCompensation = enabled;\n    }\n    useMaterial(texture, palette, transparent) {\n        if (texture.format !== THREE.RGBAFormat) {\n            throw new Error(\"Texture must have format THREE.RGBAFormat\");\n        }\n        this.materialCacheKey = texture.uuid + \"_\" + palette.uuid + \"_\" + Number(transparent);\n        let cached = ShpBuilder.materialCache.get(this.materialCacheKey);\n        let material;\n        if (cached) {\n            material = cached.material;\n            cached.usages++;\n        }\n        else {\n            material = new PaletteBasicMaterial({\n                map: texture,\n                palette: palette,\n                alphaTest: 0.05,\n                paletteCount: this.batchPalettes.length,\n                flatShading: true,\n                transparent: transparent,\n            });\n            cached = { material: material, usages: 1 };\n            ShpBuilder.materialCache.set(this.materialCacheKey, cached);\n        }\n        return material;\n    }\n    freeMaterial() {\n        if (!this.materialCacheKey) {\n            throw new Error(\"Material cache key not set\");\n        }\n        let cached = ShpBuilder.materialCache.get(this.materialCacheKey);\n        if (cached) {\n            if (cached.usages === 1) {\n                ShpBuilder.materialCache.delete(this.materialCacheKey);\n                cached.material.dispose();\n            }\n            else {\n                cached.usages--;\n            }\n        }\n    }\n    setBatched(batched) {\n        if (this.mesh) {\n            throw new Error(\"Batching can only be set before calling build()\");\n        }\n        this.useMeshBatching = batched;\n    }\n    setOffset(offset) {\n        if (this.mesh) {\n            throw new Error(\"Offset can only be set before calling build()\");\n        }\n        this.offset = offset;\n    }\n    setAlign(x, y) {\n        if (this.mesh) {\n            throw new Error(\"Align can only be set before calling build()\");\n        }\n        this.align = { x, y };\n    }\n    setFrameOffset(frameOffset) {\n        if (this.mesh) {\n            throw new Error(\"frameOffset can only be set before calling build()\");\n        }\n        this.frameOffset = frameOffset;\n    }\n    initTexture() {\n        ShpBuilder.prepareTexture(this.shpFile);\n        this.atlas = ShpBuilder.textureCache.get(this.shpFile);\n    }\n    getSpriteGeometryOptions(frameNo) {\n        frameNo += this.frameOffset;\n        const image = this.shpFile.getImage(frameNo);\n        const offset = {\n            x: image.x - Math.floor(this.shpSize.width / 2) + Math.floor(this.offset.x),\n            y: image.y - Math.floor(this.shpSize.height / 2) + Math.floor(this.offset.y),\n        };\n        const align = this.align ? this.align : (this.uiAnchorCompensation ? { x: 0, y: -1 } : { x: 1, y: -1 });\n        return {\n            texture: this.atlas.getTexture(),\n            textureArea: this.atlas.getTextureArea(frameNo),\n            flat: this.flat,\n            align: align,\n            offset: offset,\n            camera: this.camera,\n            depth: this.depth,\n            depthOffset: this.depthOffset,\n            scale: this.scale,\n        };\n    }\n    getGeometryCacheKey(frameNo) {\n        return (frameNo +\n            this.frameOffset +\n            \"_\" +\n            this.shpSize.width +\n            \"_\" +\n            this.shpSize.height +\n            \"_\" +\n            this.offset.x +\n            \"_\" +\n            this.offset.y +\n            \"_\" +\n            this.flat +\n            \"_\" +\n            this.depth +\n            \"_\" +\n            this.depthOffset);\n    }\n    setFrame(frameNo) {\n        if (this.frameNo !== frameNo) {\n            this.frameNo = frameNo;\n            if (this.mesh) {\n                let geometryCache = this.getGeometryCache();\n                const cacheKey = this.getGeometryCacheKey(frameNo);\n                let geometry = geometryCache.get(cacheKey);\n                if (!geometry) {\n                    geometry = SpriteUtils.createSpriteGeometry(this.getSpriteGeometryOptions(frameNo));\n                    geometryCache.set(cacheKey, geometry);\n                }\n                this.mesh.geometry = geometry;\n            }\n        }\n    }\n    getGeometryCache() {\n        let cache = ShpBuilder.geometryCache.get(this.shpFile);\n        if (!cache) {\n            cache = new Map();\n            ShpBuilder.geometryCache.set(this.shpFile, cache);\n        }\n        return cache;\n    }\n    getFrame() {\n        return this.frameNo;\n    }\n    setSize(size) {\n        this.shpSize = { width: size.width, height: size.height };\n    }\n    getSize() {\n        return this.shpSize;\n    }\n    get frameCount() {\n        return this.shpFile.numImages;\n    }\n    getBatchPaletteIndex(palette) {\n        const index = this.batchPalettes.findIndex((p) => p.hash === palette.hash);\n        if (index === -1) {\n            throw new Error(\"Provided palette not found in the list of batch palettes. Call setBatchPalettes first.\");\n        }\n        return index;\n    }\n    setPalette(palette) {\n        this.palette = palette;\n        if (this.mesh) {\n            if (this.useMeshBatching) {\n                const paletteIndex = this.getBatchPaletteIndex(palette);\n                this.mesh.setPaletteIndex(paletteIndex);\n            }\n            else {\n                const paletteTexture = TextureUtils.textureFromPalette(palette);\n                let material = this.mesh.material;\n                material.palette = paletteTexture;\n            }\n        }\n    }\n    setBatchPalettes(palettes) {\n        if (!this.useMeshBatching) {\n            throw new Error(\"Can't use multiple palettes when not batching\");\n        }\n        if (this.mesh) {\n            throw new Error(\"Palettes must be set before creating 3DObject\");\n        }\n        this.batchPalettes = palettes;\n    }\n    setExtraLight(extraLight) {\n        this.extraLight = extraLight;\n        if (this.mesh) {\n            if (this.useMeshBatching) {\n                this.mesh.setExtraLight(extraLight);\n            }\n            else {\n                let material = this.mesh.material;\n                material.extraLight = extraLight;\n            }\n        }\n    }\n    setOpacity(opacity) {\n        const oldOpacity = this.opacity;\n        if (oldOpacity !== opacity) {\n            this.opacity = opacity;\n            this.updateOpacity();\n        }\n        if (Math.floor(oldOpacity) !== Math.floor(opacity) && !this.forceTransparent) {\n            this.updateTransparency();\n        }\n    }\n    setForceTransparent(forceTransparent) {\n        if (forceTransparent !== this.forceTransparent) {\n            this.forceTransparent = forceTransparent;\n            this.updateTransparency();\n        }\n    }\n    updateOpacity() {\n        if (this.mesh) {\n            if (this.useMeshBatching) {\n                this.mesh.setOpacity(this.opacity);\n            }\n            else {\n                this.mesh.material.opacity = this.opacity;\n            }\n        }\n    }\n    updateTransparency() {\n        if (this.mesh) {\n            const transparent = this.forceTransparent || this.opacity < 1;\n            if (this.useMeshBatching) {\n                const texture = this.mesh.material.map;\n                const palette = this.mesh.material.palette;\n                this.freeMaterial();\n                this.mesh.material = this.useMaterial(texture, palette, transparent);\n            }\n            else {\n                this.mesh.material.transparent = transparent;\n            }\n        }\n    }\n    build() {\n        if (this.mesh) {\n            return this.mesh;\n        }\n        this.initTexture();\n        const texture = this.atlas.getTexture();\n        const cacheKey = this.getGeometryCacheKey(this.frameNo);\n        let geometryCache = this.getGeometryCache();\n        let geometry = geometryCache.get(cacheKey);\n        if (!geometry) {\n            const options = this.getSpriteGeometryOptions(this.frameNo);\n            geometry = SpriteUtils.createSpriteGeometry(options);\n            geometryCache.set(cacheKey, geometry);\n        }\n        else {\n        }\n        let mesh;\n        const transparent = this.opacity < 1 || this.forceTransparent;\n        if (this.useMeshBatching) {\n            const paletteTexture = TextureUtils.textureFromPalettes(this.batchPalettes);\n            const material = this.useMaterial(texture, paletteTexture, transparent);\n            mesh = new BatchedMesh(geometry, material, BatchMode.Merging);\n            mesh.castShadow = false;\n        }\n        else {\n            const paletteTexture = TextureUtils.textureFromPalette(this.palette);\n            const material = new PaletteBasicMaterial({\n                map: texture,\n                palette: paletteTexture,\n                alphaTest: 0.5,\n                flatShading: true,\n                transparent: transparent,\n            });\n            mesh = new THREE.Mesh(geometry, material);\n        }\n        mesh.matrixAutoUpdate = false;\n        this.mesh = mesh;\n        this.setPalette(this.palette);\n        this.updateOpacity();\n        if (this.extraLight) {\n            this.setExtraLight(this.extraLight);\n        }\n        return mesh;\n    }\n    dispose() {\n        if (this.mesh) {\n            if (this.useMeshBatching) {\n                this.freeMaterial();\n            }\n            else {\n                this.mesh.material.dispose();\n            }\n            this.mesh = undefined;\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/ShpTextureAtlas.ts",
    "content": "import { IndexedBitmap } from \"../../../data/Bitmap\";\nimport { TextureAtlas } from \"../../gfx/TextureAtlas\";\nexport class ShpTextureAtlas {\n    private images: IndexedBitmap[];\n    private atlas: TextureAtlas;\n    fromShpFile(shpFile: any): ShpTextureAtlas {\n        const bitmaps: IndexedBitmap[] = [];\n        for (let i = 0; i < shpFile.numImages; i++) {\n            const image = shpFile.getImage(i);\n            bitmaps.push(new IndexedBitmap(image.width, image.height, image.imageData));\n        }\n        const atlas = new TextureAtlas();\n        atlas.pack(bitmaps);\n        this.images = bitmaps;\n        this.atlas = atlas;\n        return this;\n    }\n    getTextureArea(imageIndex: number): any {\n        return this.atlas.getImageRect(this.images[imageIndex]);\n    }\n    getTexture(): any {\n        return this.atlas.getTexture();\n    }\n    dispose(): void {\n        this.atlas.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/SpriteBuilder.ts",
    "content": "export class SpriteBuilder {\n    constructor() { }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/VxlBatchedBuilder.ts",
    "content": "import { TextureUtils } from \"@/engine/gfx/TextureUtils\";\nimport { BatchedMesh } from \"@/engine/gfx/batch/BatchedMesh\";\nimport { VxlBuilder } from \"@/engine/renderable/builder/VxlBuilder\";\nimport { PalettePhongMaterial } from \"@/engine/gfx/material/PalettePhongMaterial\";\nimport { VxlFile } from \"@/data/VxlFile\";\nimport { HvaFile } from \"@/data/HvaFile\";\nimport { Palette } from \"@/data/Palette\";\nimport * as THREE from \"three\";\nexport class VxlBatchedBuilder extends VxlBuilder {\n    private static materialCache = new Map<THREE.Texture, {\n        material: PalettePhongMaterial;\n        usages: number;\n    }>();\n    private vxlFile: VxlFile;\n    private hvaFile?: HvaFile;\n    private palettes: Palette[];\n    private palette: Palette;\n    private vxlGeometryPool: any;\n    private clippingPlanes: THREE.Plane[] = [];\n    private opacity: number = 1;\n    private castShadow: boolean = true;\n    private materialCacheKey?: THREE.Texture;\n    private extraLight: any;\n    constructor(vxlFile: VxlFile, hvaFile: HvaFile | undefined, palettes: Palette[], palette: Palette, vxlGeometryPool: any, camera: any) {\n        super(camera);\n        this.vxlFile = vxlFile;\n        this.hvaFile = hvaFile;\n        this.palettes = palettes;\n        this.palette = palette;\n        this.vxlGeometryPool = vxlGeometryPool;\n    }\n    createVxlMeshes(): Map<string, BatchedMesh> {\n        const texture = TextureUtils.textureFromPalettes(this.palettes);\n        const material = this.useMaterial(texture);\n        this.materialCacheKey = texture;\n        const paletteIndex = this.getPaletteIndex(this.palette);\n        const sections = this.vxlFile.sections;\n        const meshes = new Map<string, BatchedMesh>();\n        sections.forEach((section: any, index: number) => {\n            const geometry = this.vxlGeometryPool.get(section);\n            const mesh = new BatchedMesh(geometry, material);\n            let matrix = section.transfMatrix;\n            const hvaSection = this.hvaFile?.sections[index];\n            if (hvaSection) {\n                matrix = section.scaleHvaMatrix(hvaSection.getMatrix(0));\n            }\n            mesh.applyMatrix4(matrix);\n            meshes.set(section.name, mesh);\n            mesh.castShadow = this.castShadow;\n            mesh.setPaletteIndex(paletteIndex);\n            if (this.extraLight) {\n                mesh.setExtraLight(this.extraLight);\n            }\n            mesh.setOpacity(this.opacity);\n            mesh.setClippingPlanes(this.clippingPlanes);\n        });\n        return meshes;\n    }\n    private useMaterial(texture: THREE.Texture): PalettePhongMaterial {\n        let cached = VxlBatchedBuilder.materialCache.get(texture);\n        let material: PalettePhongMaterial;\n        if (cached) {\n            material = cached.material;\n            cached.usages++;\n        }\n        else {\n            material = new PalettePhongMaterial({\n                palette: texture,\n                paletteCount: this.palettes.length,\n                vertexColors: true,\n                transparent: true\n            });\n            cached = { material, usages: 1 };\n            VxlBatchedBuilder.materialCache.set(texture, cached);\n        }\n        return material;\n    }\n    private freeMaterial(): void {\n        const cached = VxlBatchedBuilder.materialCache.get(this.materialCacheKey);\n        if (cached) {\n            if (cached.usages === 1) {\n                VxlBatchedBuilder.materialCache.delete(this.materialCacheKey);\n                cached.material.dispose();\n            }\n            else {\n                cached.usages--;\n            }\n        }\n    }\n    private getPaletteIndex(palette: Palette): number {\n        const index = this.palettes.findIndex((p) => p.hash === palette.hash);\n        if (index === -1) {\n            throw new Error(\"Provided palette not found in the list of available palettes\");\n        }\n        return index;\n    }\n    setPalette(palette: Palette): void {\n        this.palette = palette;\n        if (this.object && this.sections) {\n            const index = this.getPaletteIndex(palette);\n            this.sections.forEach((section: any) => section.setPaletteIndex(index));\n        }\n    }\n    setExtraLight(light: any): void {\n        this.extraLight = light;\n        if (this.object && this.sections) {\n            this.sections.forEach((section: any) => section.setExtraLight(light));\n        }\n    }\n    setShadow(castShadow: boolean): void {\n        this.castShadow = castShadow;\n        this.sections?.forEach((section: any) => {\n            section.castShadow = castShadow;\n        });\n    }\n    setClippingPlanes(planes: any[]): void {\n        this.clippingPlanes = planes;\n        if (this.object && this.sections) {\n            this.sections.forEach((section: any) => section.setClippingPlanes(planes));\n        }\n    }\n    setOpacity(opacity: number): void {\n        this.opacity = opacity;\n        if (this.object && this.sections) {\n            this.sections.forEach((section: any) => section.setOpacity(opacity));\n        }\n    }\n    dispose(): void {\n        if (this.object) {\n            this.freeMaterial();\n            this.object = undefined;\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/VxlBuilder.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport * as THREE from 'three';\ninterface Camera {\n    rotation: {\n        y: number;\n    };\n}\nexport abstract class VxlBuilder {\n    protected camera: Camera;\n    protected object?: THREE.Object3D;\n    protected sections?: Map<string, THREE.Mesh>;\n    protected localBoundingBox?: THREE.Box3;\n    constructor(camera: Camera) {\n        this.camera = camera;\n    }\n    build(): THREE.Object3D {\n        if (this.object) {\n            return this.object;\n        }\n        const rootObject = this.object = new THREE.Object3D();\n        const scale = Math.cos(this.camera.rotation.y) * Coords.ISO_WORLD_SCALE;\n        rootObject.scale.set(scale, scale, scale);\n        const rotationContainer = new THREE.Object3D();\n        rotationContainer.rotation.x = -Math.PI / 2;\n        rotationContainer.rotation.z = +Math.PI / 2;\n        rotationContainer.matrixAutoUpdate = false;\n        rotationContainer.updateMatrix();\n        rootObject.add(rotationContainer);\n        const meshes = this.sections = this.createVxlMeshes();\n        meshes.forEach((mesh) => {\n            mesh.matrixAutoUpdate = false;\n            rotationContainer.add(mesh);\n            if (!this.localBoundingBox) {\n                if (!mesh.geometry.boundingBox) {\n                    mesh.geometry.computeBoundingBox();\n                }\n                if (mesh.geometry.boundingBox) {\n                    this.localBoundingBox = new THREE.Box3(mesh.geometry.boundingBox.min.clone().multiplyScalar(scale), mesh.geometry.boundingBox.max.clone().multiplyScalar(scale));\n                    const tempMinX = this.localBoundingBox.min.x;\n                    this.localBoundingBox.min.x = this.localBoundingBox.min.y;\n                    this.localBoundingBox.min.y = tempMinX;\n                    const tempMaxX = this.localBoundingBox.max.x;\n                    this.localBoundingBox.max.x = this.localBoundingBox.max.y;\n                    this.localBoundingBox.max.y = tempMaxX;\n                }\n            }\n        });\n        rootObject.matrixAutoUpdate = false;\n        rootObject.updateMatrix();\n        return rootObject;\n    }\n    getSection(sectionName: string): THREE.Mesh | undefined {\n        if (!this.sections) {\n            throw new Error(\"Vxl object must be built first\");\n        }\n        return this.sections.get(sectionName);\n    }\n    getLocalBoundingBox(): THREE.Box3 | undefined {\n        return this.localBoundingBox;\n    }\n    abstract createVxlMeshes(): Map<string, THREE.Mesh>;\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/VxlBuilderFactory.ts",
    "content": "import { VxlBatchedBuilder } from \"./VxlBatchedBuilder\";\nimport { VxlNonBatchedBuilder } from \"./VxlNonBatchedBuilder\";\nimport { VxlGeometryPool } from \"./vxlGeometry/VxlGeometryPool\";\nimport { Camera } from \"three\";\nimport { VxlFile } from \"@/data/VxlFile\";\nimport { HvaFile } from \"@/data/HvaFile\";\nimport { Palette } from \"@/data/Palette\";\nimport { VxlBuilder } from \"./VxlBuilder\";\nexport class VxlBuilderFactory {\n    constructor(private vxlGeometryPool: VxlGeometryPool, private useBatching: boolean, private camera: Camera) { }\n    create(vxlData: VxlFile, hvaData: HvaFile | undefined, palettes: Palette[], palette: Palette): VxlBuilder {\n        return this.useBatching\n            ? new VxlBatchedBuilder(vxlData, hvaData, palettes, palette, this.vxlGeometryPool, this.camera)\n            : new VxlNonBatchedBuilder(vxlData, palette, hvaData ?? null, this.vxlGeometryPool, this.camera);\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/VxlNonBatchedBuilder.ts",
    "content": "import { TextureUtils } from '@/engine/gfx/TextureUtils';\nimport { VxlBuilder } from '@/engine/renderable/builder/VxlBuilder';\nimport { PalettePhongMaterial } from '@/engine/gfx/material/PalettePhongMaterial';\nimport { Palette } from '@/data/Palette';\nimport * as THREE from 'three';\ninterface VxlSection {\n    name: string;\n    transfMatrix: THREE.Matrix4;\n    scaleHvaMatrix(matrix: THREE.Matrix4): THREE.Matrix4;\n}\ninterface VxlFile {\n    sections: VxlSection[];\n}\ninterface HvaSection {\n    getMatrix(index: number): THREE.Matrix4;\n}\ninterface HvaFile {\n    sections: HvaSection[];\n}\ninterface VxlGeometryPool {\n    get(section: VxlSection): THREE.BufferGeometry;\n}\nexport class VxlNonBatchedBuilder extends VxlBuilder {\n    private vxlFile: VxlFile;\n    private hvaFile: HvaFile | null;\n    private palette: Palette;\n    private vxlGeometryPool: VxlGeometryPool;\n    private clippingPlanes: THREE.Plane[];\n    private castShadow: boolean;\n    private material?: PalettePhongMaterial;\n    private extraLight?: any;\n    constructor(vxlFile: VxlFile, palette: Palette, hvaFile: HvaFile | null, vxlGeometryPool: VxlGeometryPool, parent: THREE.Camera) {\n        super(parent);\n        this.vxlFile = vxlFile;\n        this.hvaFile = hvaFile;\n        this.palette = palette;\n        this.vxlGeometryPool = vxlGeometryPool;\n        this.clippingPlanes = [];\n        this.castShadow = true;\n    }\n    createVxlMeshes(): Map<string, THREE.Mesh> {\n        const paletteTexture = TextureUtils.textureFromPalette(this.palette);\n        const material = this.material = new PalettePhongMaterial({\n            palette: paletteTexture,\n            vertexColors: true,\n        });\n        if (this.extraLight) {\n            material.extraLight = this.extraLight;\n        }\n        material.clippingPlanes = this.clippingPlanes;\n        const sections = this.vxlFile.sections;\n        const meshMap = new Map<string, THREE.Mesh>();\n        sections.forEach((section, index) => {\n            const geometry = this.vxlGeometryPool.get(section);\n            const mesh = new THREE.Mesh(geometry, material);\n            let transformMatrix = section.transfMatrix;\n            const hvaSection = this.hvaFile?.sections[index];\n            if (hvaSection) {\n                transformMatrix = section.scaleHvaMatrix(hvaSection.getMatrix(0));\n            }\n            mesh.applyMatrix4(transformMatrix);\n            meshMap.set(section.name, mesh);\n            mesh.castShadow = this.castShadow;\n        });\n        this.sections = meshMap;\n        return meshMap;\n    }\n    setPalette(palette: Palette): void {\n        this.palette = palette;\n        if (this.object && this.material) {\n            const paletteTexture = TextureUtils.textureFromPalette(palette);\n            this.material.palette = paletteTexture;\n        }\n    }\n    setExtraLight(extraLight: any): void {\n        this.extraLight = extraLight;\n        if (this.object && this.material) {\n            this.material.extraLight = extraLight;\n        }\n    }\n    setShadow(castShadow: boolean): void {\n        this.castShadow = castShadow;\n        if (this.sections) {\n            this.sections.forEach((mesh) => {\n                mesh.castShadow = castShadow;\n            });\n        }\n    }\n    setClippingPlanes(clippingPlanes: THREE.Plane[]): void {\n        this.clippingPlanes = clippingPlanes;\n        if (this.object && this.material) {\n            this.material.clippingPlanes = clippingPlanes;\n        }\n    }\n    setOpacity(opacity: number): void {\n        if (this.material) {\n            this.material.transparent = opacity < 1;\n            this.material.opacity = opacity;\n        }\n    }\n    dispose(): void {\n        if (this.object && this.material) {\n            this.material.dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/vxlGeometry/VxlGeometryCulledBuilder.ts",
    "content": "import { BufferGeometryUtils } from \"@/engine/gfx/BufferGeometryUtils\";\nimport * as THREE from 'three';\nexport class VxlGeometryCulledBuilder {\n    build(e: any) {\n        let { voxels: a, voxelField: n } = e.getAllVoxels();\n        const t = new THREE.BoxBufferGeometry(1, 1, 1);\n        const o = t.getAttribute(\"position\").array;\n        const l = o.length / 3;\n        const c = t.getAttribute(\"normal\").array;\n        const h = t.getIndex().array;\n        const u: number[] = [];\n        const d: number[] = [];\n        const g: number[] = [];\n        const p: number[] = [];\n        const m = e.minBounds;\n        const f = e.scale;\n        const y = e.getNormals();\n        let T = 0;\n        for (let E = 0, r = a.length; E < r; E++) {\n            const v = a[E];\n            const b = y[Math.min(a[E].normalIndex, y.length - 1)];\n            const e = new Array(l);\n            for (let t = 0, i = 3 * l; t < i; t += 3) {\n                if (!n.get(v.x + c[t], v.y + c[t + 1], v.z + c[t + 2])) {\n                    e[t / 3] = T;\n                    u.push(m.x + v.x * f.x + o[t], m.y + v.y * f.y + o[t + 1], m.z + v.z * f.z + o[t + 2]);\n                    d.push(b.x, b.y, b.z);\n                    g.push(v.colorIndex / 255, 0, 0);\n                    T++;\n                }\n            }\n            for (let r = 0, s = h.length; r < s; r += 3) {\n                const S = e[h[r]];\n                const w = e[h[r + 1]];\n                const C = e[h[r + 2]];\n                if (S !== undefined && w !== undefined && C !== undefined) {\n                    p.push(S, w, C);\n                }\n            }\n        }\n        let i = new THREE.BufferGeometry();\n        i.setIndex(new THREE.BufferAttribute(new Uint32Array(p), 1));\n        i.setAttribute(\"position\", new THREE.BufferAttribute(new Float32Array(3 * t), 3));\n        i.setAttribute(\"normal\", new THREE.BufferAttribute(new Float32Array(3 * t), 3));\n        i.setAttribute(\"color\", new THREE.BufferAttribute(new Float32Array(4 * t), 4));\n        i = BufferGeometryUtils.mergeVertices(i);\n        i.computeBoundingBox();\n        return i;\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/vxlGeometry/VxlGeometryMonotoneBuilder.ts",
    "content": "import { BufferGeometryUtils } from \"@/engine/gfx/BufferGeometryUtils\";\nimport * as THREE from 'three';\nclass VxlGeometryMonotoneBuilder {\n    build(e, t = false) {\n        let s = e.getAllVoxels()[\"voxelField\"];\n        var { vertices: i, faces: r } = (function (e, t) {\n            for (var i = [], r = [], s = 0; s < 3; ++s) {\n                var a = (s + 1) % 3, n = (s + 2) % 3, o = new Int32Array(3), l = new Int32Array(3), c = new Int32Array(2 * (t[a] + 1)), h = new Int32Array(t[a]), u = new Int32Array(t[a]), d = new Int32Array(2 * t[n]), g = new Int32Array(2 * t[n]), p = new Int32Array(24 * t[n]), m = [\n                    [0, 0],\n                    [0, 0],\n                ];\n                for (l[s] = 1, o[s] = -1; o[s] < t[s];) {\n                    var f = [], y = 0;\n                    for (o[n] = 0; o[n] < t[n]; ++o[n]) {\n                        var T = 0, v = 0, b = 0;\n                        for (o[a] = 0; o[a] < t[a]; ++o[a], v = b) {\n                            var S = 0 <= o[s] ? e(o[0], o[1], o[2]) : 0, w = o[s] < t[s] - 1\n                                ? e(o[0] + l[0], o[1] + l[1], o[2] + l[2])\n                                : 0;\n                            !(b = S) == !w ? (b = 0) : S || (b = -w),\n                                v !== b && ((c[T++] = o[a]), (c[T++] = b));\n                        }\n                        c[T++] = t[a];\n                        for (var C = (c[T++] = 0), E = 0, x = 0; E < y && x < T - 2;) {\n                            let e = f[h[E]];\n                            var O = e.left[e.left.length - 1][0], M = e.right[e.right.length - 1][0], A = e.color, R = c[x], P = c[x + 2], I = c[x + 1];\n                            O < P && R < M && I === A\n                                ? (e.merge_run(o[n], R, P),\n                                    (u[C++] = h[E]),\n                                    ++E,\n                                    (x += 2))\n                                : (P <= M &&\n                                    (I &&\n                                        ((k = new VxlRun(I, o[n], R, P)),\n                                            (u[C++] = f.length),\n                                            f.push(k)),\n                                        (x += 2)),\n                                    M <= P && (e.close_off(o[n]), ++E));\n                        }\n                        for (; E < y; ++E)\n                            f[h[E]].close_off(o[n]);\n                        for (; x < T - 2; x += 2) {\n                            var k, R = c[x], P = c[x + 2];\n                            (I = c[x + 1]) &&\n                                ((k = new VxlRun(I, o[n], R, P)),\n                                    (u[C++] = f.length),\n                                    f.push(k));\n                        }\n                        var B = u, u = h, h = B, y = C;\n                    }\n                    for (E = 0; E < y; ++E) {\n                        let e = f[h[E]];\n                        e.close_off(t[n]);\n                    }\n                    o[s]++;\n                    for (E = 0; E < f.length; ++E) {\n                        var N = f[E], j = !1;\n                        (b = N.color) < 0 && ((j = !0), (b = -b));\n                        for (x = 0; x < N.left.length; ++x) {\n                            d[x] = i.length;\n                            var L = [0, 0, 0], D = N.left[x];\n                            (L[s] = o[s]),\n                                (L[a] = D[0]),\n                                (L[n] = D[1]),\n                                i.push({ position: L, value: b });\n                        }\n                        for (x = 0; x < N.right.length; ++x) {\n                            g[x] = i.length;\n                            (L = [0, 0, 0]), (D = N.right[x]);\n                            (L[s] = o[s]),\n                                (L[a] = D[0]),\n                                (L[n] = D[1]),\n                                i.push({ position: L, value: b });\n                        }\n                        var F = 0, _ = 0, U = 1, H = 1, G = !0;\n                        for (p[_++] = d[0],\n                            p[_++] = N.left[0][0],\n                            p[_++] = N.left[0][1],\n                            p[_++] = g[0],\n                            p[_++] = N.right[0][0],\n                            p[_++] = N.right[0][1]; U < N.left.length || H < N.right.length;) {\n                            var V, W, z = !1;\n                            U === N.left.length\n                                ? (z = !0)\n                                : H !== N.right.length &&\n                                    ((V = N.left[U]),\n                                        (W = N.right[H]),\n                                        (z = V[1] > W[1]));\n                            var K = z ? g[H] : d[U], q = z ? N.right[H] : N.left[U];\n                            if (z !== G)\n                                for (; F + 3 < _;)\n                                    j === z\n                                        ? r.push([p[F], p[F + 3], K])\n                                        : r.push([p[F + 3], p[F], K]),\n                                        (F += 3);\n                            else\n                                for (; F + 3 < _;) {\n                                    for (x = 0; x < 2; ++x)\n                                        for (var $ = 0; $ < 2; ++$)\n                                            m[x][$] = p[_ - 3 * (x + 1) + $ + 1] - q[$];\n                                    var Q = m[0][0] * m[1][1] - m[1][0] * m[0][1];\n                                    if (z === 0 < Q)\n                                        break;\n                                    0 != Q &&\n                                        (j === z\n                                            ? r.push([p[_ - 3], p[_ - 6], K])\n                                            : r.push([p[_ - 6], p[_ - 3], K])),\n                                        (_ -= 3);\n                                }\n                            (p[_++] = K),\n                                (p[_++] = q[0]),\n                                (p[_++] = q[1]),\n                                z ? ++H : ++U,\n                                (G = z);\n                        }\n                    }\n                }\n            }\n            return { vertices: i, faces: r };\n        })(t\n            ? (e, t, i) => {\n                var r = s.get(e, t, i);\n                return r ? r.colorIndex : 0;\n            }\n            : (e, t, i) => {\n                var r = s.get(e, t, i);\n                return r ? r.normalIndex + 256 * r.colorIndex : 0;\n            }, [e.sizeX, e.sizeY, e.sizeZ]), a = e.minBounds, n = e.scale, o = e.getNormals();\n        let l = new Float32Array(3 * i.length), c = new Float32Array(3 * i.length), h = new Float32Array(3 * i.length), u = 0, d = 0, g = 0;\n        for (let b = 0, S = i.length; b < S; b++) {\n            var p = i[b], m = t ? p.value : (p.value / 256) | 0;\n            (l[u++] = a.x + p.position[0] * n.x),\n                (l[u++] = a.y + p.position[1] * n.y),\n                (l[u++] = a.z + p.position[2] * n.z),\n                (h[g++] = m / 255),\n                (h[g++] = 0),\n                (h[g++] = 0),\n                t ||\n                    ((p = p.value % 256),\n                        (p = o[Math.min(p, o.length - 1)]),\n                        (c[d++] = p.x),\n                        (c[d++] = p.y),\n                        (c[d++] = p.z));\n        }\n        let f = new Uint32Array(3 * r.length), y = 0;\n        for (let w = 0, C = r.length; w < C; w++) {\n            var T = r[w];\n            (f[y++] = T[0]), (f[y++] = T[1]), (f[y++] = T[2]);\n        }\n        let v = new THREE.BufferGeometry();\n        return (v.setAttribute(\"position\", new THREE.BufferAttribute(l, 3)),\n            t ||\n                v.setAttribute(\"normal\", new THREE.BufferAttribute(c, 3)),\n            v.setAttribute(\"color\", new THREE.BufferAttribute(h, 3)),\n            v.setIndex(new THREE.BufferAttribute(f, 1)),\n            (v = BufferGeometryUtils.mergeVertices(v)),\n            v.computeBoundingBox(),\n            t && v.computeVertexNormals(),\n            v);\n    }\n}\nclass VxlRun {\n    color: any;\n    left: any[][];\n    right: any[][];\n    constructor(e, t, i, r) {\n        this.color = e;\n        this.left = [[i, t]];\n        this.right = [[r, t]];\n    }\n    close_off(e) {\n        this.left.push([this.left[this.left.length - 1][0], e]);\n        this.right.push([this.right[this.right.length - 1][0], e]);\n    }\n    merge_run(e, t, i) {\n        var r = this.left[this.left.length - 1][0], s = this.right[this.right.length - 1][0];\n        r !== t && (this.left.push([r, e]), this.left.push([t, e]));\n        s !== i && (this.right.push([s, e]), this.right.push([i, e]));\n    }\n}\nexport { VxlGeometryMonotoneBuilder };\n"
  },
  {
    "path": "src/engine/renderable/builder/vxlGeometry/VxlGeometryNaiveBuilder.ts",
    "content": "import * as THREE from 'three';\nimport { BufferGeometryUtils } from '@/engine/gfx/BufferGeometryUtils';\nexport class VxlGeometryNaiveBuilder {\n    build(vxl: any): THREE.BufferGeometry {\n        const { voxels, voxelField } = vxl.getAllVoxels();\n        const boxGeometry = new THREE.BoxBufferGeometry(1, 1, 1);\n        const vertexCount = boxGeometry.getAttribute(\"position\").array.length / 3;\n        const normalArray = boxGeometry.getAttribute(\"normal\").array;\n        let geometry = new THREE.BufferGeometry();\n        geometry.setIndex(this.createIndexAttr(voxels, boxGeometry, vertexCount));\n        geometry.setAttribute(\"position\", this.createPositionAttr(vxl, voxels, boxGeometry));\n        geometry.setAttribute(\"normal\", this.createNormalAttr(vxl, voxels, vertexCount));\n        geometry.setAttribute(\"color\", this.createColorAttr(voxels, vertexCount, normalArray, voxelField));\n        geometry = BufferGeometryUtils.mergeVertices(geometry);\n        geometry.computeBoundingBox();\n        return geometry;\n    }\n    private createPositionAttr(vxl: any, voxels: any[], boxGeometry: THREE.BoxBufferGeometry): THREE.BufferAttribute {\n        const positionArray = boxGeometry.getAttribute(\"position\").array;\n        const arrayLength = positionArray.length;\n        const positions = new Float32Array(arrayLength * voxels.length);\n        const minBounds = vxl.minBounds;\n        const scale = vxl.scale;\n        for (let i = 0; i < voxels.length; i++) {\n            const offset = i * arrayLength;\n            const voxel = voxels[i];\n            for (let j = 0; j < positionArray.length; j += 3) {\n                positions[offset + j] = minBounds.x + voxel.x * scale.x + positionArray[j];\n                positions[offset + j + 1] = minBounds.y + voxel.y * scale.y + positionArray[j + 1];\n                positions[offset + j + 2] = minBounds.z + voxel.z * scale.z + positionArray[j + 2];\n            }\n        }\n        return new THREE.BufferAttribute(positions, 3);\n    }\n    private createNormalAttr(vxl: any, voxels: any[], vertexCount: number): THREE.BufferAttribute {\n        const normals = new Float32Array(vertexCount * voxels.length * 3);\n        const normalTable = vxl.getNormals();\n        for (let i = 0; i < voxels.length; i++) {\n            const offset = i * vertexCount * 3;\n            const normal = normalTable[Math.min(voxels[i].normalIndex, normalTable.length - 1)];\n            for (let j = 0; j < 3 * vertexCount; j += 3) {\n                normals[offset + j] = normal.x;\n                normals[offset + j + 1] = normal.y;\n                normals[offset + j + 2] = normal.z;\n            }\n        }\n        return new THREE.BufferAttribute(normals, 3);\n    }\n    private createColorAttr(voxels: any[], vertexCount: number, normalArray: Float32Array, voxelField: any): THREE.BufferAttribute {\n        const colors = new Float32Array(vertexCount * voxels.length * 3);\n        for (let i = 0; i < voxels.length; i++) {\n            const offset = i * vertexCount * 3;\n            const voxel = voxels[i];\n            for (let j = 0; j < 3 * vertexCount; j += 3) {\n                const hasVoxel = voxelField.get(voxel.x + normalArray[j], voxel.y + normalArray[j + 1], voxel.z + normalArray[j + 2]);\n                colors[offset + j] = hasVoxel ? 0 : voxel.colorIndex / 255;\n                colors[offset + j + 1] = 0;\n                colors[offset + j + 2] = 0;\n            }\n        }\n        return new THREE.BufferAttribute(colors, 3);\n    }\n    private createIndexAttr(voxels: any[], boxGeometry: THREE.BoxBufferGeometry, vertexCount: number): THREE.BufferAttribute {\n        const indexArray = boxGeometry.getIndex().array;\n        const indices = new Uint32Array(voxels.length * indexArray.length);\n        for (let i = 0; i < voxels.length; i++) {\n            for (let j = 0; j < indexArray.length; j++) {\n                indices[i * indexArray.length + j] = i * vertexCount + indexArray[j];\n            }\n        }\n        return new THREE.BufferAttribute(indices, 1);\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/builder/vxlGeometry/VxlGeometryPool.ts",
    "content": "import { ModelQuality } from \"@/engine/renderable/entity/unit/ModelQuality\";\nimport { isNotNullOrUndefined } from \"@/util/typeGuard\";\nimport { VxlGeometryMonotoneBuilder } from \"@/engine/renderable/builder/vxlGeometry/VxlGeometryMonotoneBuilder\";\nexport class VxlGeometryPool {\n    cache: any;\n    modelQuality: ModelQuality;\n    constructor(cache, modelQuality = ModelQuality.High) {\n        this.cache = cache;\n        this.modelQuality = modelQuality;\n    }\n    setModelQuality(modelQuality) {\n        this.modelQuality = modelQuality;\n    }\n    getModelQuality() {\n        return this.modelQuality;\n    }\n    async loadFromStorage(data, param) {\n        let results = await Promise.all(data.sections.map((section) => this.cache.loadFromStorage(section, param)));\n        return results.every(isNotNullOrUndefined);\n    }\n    async persistToStorage(data, param, results) {\n        for (let i = 0; i < data.sections.length; i++) {\n            const section = data.sections[i];\n            await this.cache.persistToStorage(section, param, results[i]);\n        }\n    }\n    clear() {\n        this.cache.clear();\n    }\n    async clearStorage() {\n        await this.cache.clearStorage();\n    }\n    async clearOtherModStorage() {\n        await this.cache.clearOtherModStorage();\n    }\n    get(key) {\n        let geometry = this.cache.get(key);\n        if (!geometry) {\n            geometry = new VxlGeometryMonotoneBuilder().build(key);\n            this.cache.set(key, geometry);\n        }\n        return geometry;\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/Aircraft.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { WithPosition } from \"@/engine/renderable/WithPosition\";\nimport { DebugUtils } from \"@/engine/gfx/DebugUtils\";\nimport { getRandomInt } from \"@/util/math\";\nimport { VeteranLevel } from \"@/game/gameobject/unit/VeteranLevel\";\nimport { HighlightAnimRunner } from \"@/engine/renderable/entity/HighlightAnimRunner\";\nimport { AnimationState } from \"@/engine/Animation\";\nimport { DeathType } from \"@/game/gameobject/common/DeathType\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { InvulnerableAnimRunner } from \"@/engine/renderable/entity/InvulnerableAnimRunner\";\nimport { BoxIntersectObject3D } from \"@/engine/renderable/entity/BoxIntersectObject3D\";\nimport { RotorHelper } from \"@/engine/renderable/entity/unit/RotorHelper\";\nimport { ExtraLightHelper } from \"@/engine/renderable/entity/unit/ExtraLightHelper\";\nimport { DebugRenderable } from \"@/engine/renderable/DebugRenderable\";\nimport * as THREE from 'three';\ninterface GameObject {\n    id: string;\n    name: string;\n    rules: any;\n    art: any;\n    owner: {\n        color: any;\n    };\n    tile: any;\n    veteranLevel: VeteranLevel;\n    invulnerableTrait: {\n        isActive(): boolean;\n    };\n    warpedOutTrait: {\n        isActive(): boolean;\n    };\n    cloakableTrait?: {\n        isCloaked(): boolean;\n    };\n    zone: ZoneType;\n    pitch: number;\n    yaw: number;\n    roll: number;\n    isDestroyed: boolean;\n    deathType: DeathType;\n    moveTrait: {\n        velocity: THREE.Vector3;\n    };\n    getUiName(): string;\n}\ninterface Rules {\n    colors: Map<string, any>;\n    audioVisual: {\n        extraAircraftLight: number;\n    };\n    general: {\n        getMissileRules(name: string): {\n            bodyLength: number;\n        };\n    };\n}\ninterface Palette {\n    clone(): Palette;\n    remap(color: any): Palette;\n}\ninterface VoxelAnims {\n    get(key: string): any;\n}\ninterface Voxels {\n    get(key: string): any;\n}\ninterface Lighting {\n    computeNoAmbient(lightingType: any, tile: any): number;\n    getAmbientIntensity(): number;\n}\ninterface GameSpeed {\n}\ninterface SelectionModel {\n}\ninterface VxlBuilderFactory {\n    create(voxel: any, anim: any, palettes: Palette[], palette: Palette): VxlBuilder;\n}\ninterface VxlBuilder {\n    build(): THREE.Object3D;\n    setExtraLight(light: THREE.Vector3): void;\n    setOpacity(opacity: number): void;\n    setPalette(palette: Palette): void;\n    dispose(): void;\n    getSection(name: string): THREE.Object3D | undefined;\n}\ninterface PipOverlay {\n    create3DObject(): void;\n    get3DObject(): THREE.Object3D;\n    update(deltaTime: number): void;\n    dispose(): void;\n}\ninterface Plugin {\n    updateLighting?(): void;\n    getUiNameOverride?(): string;\n    shouldDisableHighlight?(): boolean;\n    update(deltaTime: number): void;\n    onCreate(renderableManager: RenderableManager): void;\n    onRemove(renderableManager: RenderableManager): void;\n    dispose(): void;\n}\ninterface RenderableManager {\n    createTransientAnim(name: string, callback: (anim: any) => void): void;\n}\ninterface DebugFrame {\n    value: boolean;\n}\nexport class Aircraft {\n    private gameObject: GameObject;\n    private rules: Rules;\n    private voxels: Voxels;\n    private voxelAnims: VoxelAnims;\n    private palette: Palette;\n    private lighting: Lighting;\n    private debugFrame: DebugFrame;\n    private gameSpeed: GameSpeed;\n    private selectionModel: SelectionModel;\n    private vxlBuilderFactory: VxlBuilderFactory;\n    private useSpriteBatching: boolean;\n    private pipOverlay?: PipOverlay;\n    private rotorSpeeds: number[] = [];\n    private vxlBuilders: VxlBuilder[] = [];\n    private highlightAnimRunner: HighlightAnimRunner;\n    private invulnAnimRunner: InvulnerableAnimRunner;\n    private plugins: Plugin[] = [];\n    private objectRules: any;\n    private objectArt: any;\n    private label: string;\n    private paletteRemaps: Palette[] = [];\n    private lastOwnerColor: any;\n    private withPosition: WithPosition;\n    private baseExtraLight: THREE.Vector3;\n    private extraLight: THREE.Vector3;\n    private target?: THREE.Object3D;\n    private lastVeteranLevel?: VeteranLevel;\n    private lastInvulnerable: boolean = false;\n    private lastWarpedOut: boolean = false;\n    private lastCloaked: boolean = false;\n    private lastZone?: ZoneType;\n    private tiltObj: THREE.Object3D;\n    private posObj: THREE.Object3D;\n    private rotors?: THREE.Object3D[];\n    private placeholder?: DebugRenderable;\n    private renderableManager?: RenderableManager;\n    constructor(gameObject: GameObject, rules: Rules, voxels: Voxels, voxelAnims: VoxelAnims, palette: Palette, lighting: Lighting, debugFrame: DebugFrame, gameSpeed: GameSpeed, selectionModel: SelectionModel, vxlBuilderFactory: VxlBuilderFactory, useSpriteBatching: boolean, pipOverlay?: PipOverlay) {\n        this.gameObject = gameObject;\n        this.rules = rules;\n        this.voxels = voxels;\n        this.voxelAnims = voxelAnims;\n        this.palette = palette;\n        this.lighting = lighting;\n        this.debugFrame = debugFrame;\n        this.gameSpeed = gameSpeed;\n        this.selectionModel = selectionModel;\n        this.vxlBuilderFactory = vxlBuilderFactory;\n        this.useSpriteBatching = useSpriteBatching;\n        this.pipOverlay = pipOverlay;\n        this.highlightAnimRunner = new HighlightAnimRunner(this.gameSpeed as any);\n        this.invulnAnimRunner = new InvulnerableAnimRunner(this.gameSpeed as any);\n        this.objectRules = gameObject.rules;\n        this.objectArt = gameObject.art;\n        this.label = \"aircraft_\" + this.objectRules.name;\n        this.init();\n    }\n    private init(): void {\n        this.paletteRemaps = [...this.rules.colors.values()].map((color) => this.palette.clone().remap(color));\n        this.palette.remap(this.gameObject.owner.color);\n        this.lastOwnerColor = this.gameObject.owner.color;\n        this.withPosition = new WithPosition();\n        this.updateBaseLight();\n        this.extraLight = new THREE.Vector3().copy(this.baseExtraLight);\n    }\n    private updateBaseLight(): void {\n        this.baseExtraLight = new THREE.Vector3().setScalar(Math.PI * 1.5 + this.lighting.computeNoAmbient(this.objectArt.lightingType as any, this.gameObject.tile) + this.rules.audioVisual.extraAircraftLight);\n    }\n    updateLighting(): void {\n        this.plugins.forEach((plugin) => plugin.updateLighting?.());\n        this.updateBaseLight();\n        this.extraLight.copy(this.baseExtraLight);\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    getIntersectTarget(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    getUiName(): string {\n        const override = this.plugins.reduce((result, plugin) => plugin.getUiNameOverride?.() ?? result, undefined as string | undefined);\n        return override !== undefined ? override : this.gameObject.getUiName();\n    }\n    create3DObject(): void {\n        let obj = this.get3DObject();\n        if (!obj) {\n            obj = new BoxIntersectObject3D(new THREE.Vector3(1, 1 / 3, 1).multiplyScalar(Coords.LEPTONS_PER_TILE *\n                (this.gameObject.rules.spawned ? 0.5 : 1)));\n            obj.name = this.label;\n            obj.userData.id = this.gameObject.id;\n            this.target = obj;\n            obj.matrixAutoUpdate = false;\n            this.withPosition.matrixUpdate = true;\n            this.withPosition.applyTo(this);\n            this.createObjects(obj);\n            this.vxlBuilders.forEach((builder) => builder.setExtraLight(this.extraLight));\n            if (this.pipOverlay) {\n                this.pipOverlay.create3DObject();\n                this.posObj?.add(this.pipOverlay.get3DObject());\n            }\n        }\n    }\n    setPosition(position: {\n        x: number;\n        y: number;\n        z: number;\n    }): void {\n        this.withPosition.setPosition(position.x, position.y, position.z);\n    }\n    getPosition(): THREE.Vector3 {\n        return this.withPosition.getPosition();\n    }\n    registerPlugin(plugin: Plugin): void {\n        this.plugins.push(plugin);\n    }\n    highlight(): void {\n        if (!this.plugins.some((plugin) => plugin.shouldDisableHighlight?.())) {\n            if (this.highlightAnimRunner.animation.getState() !== AnimationState.RUNNING) {\n                this.highlightAnimRunner.animate(2);\n            }\n        }\n    }\n    update(deltaTime: number): void {\n        this.plugins.forEach((plugin) => plugin.update(deltaTime));\n        this.pipOverlay?.update(deltaTime);\n        if (this.gameObject.veteranLevel !== this.lastVeteranLevel) {\n            if (this.gameObject.veteranLevel === VeteranLevel.Elite &&\n                this.lastVeteranLevel !== undefined) {\n                this.highlightAnimRunner.animate(30);\n            }\n            this.lastVeteranLevel = this.gameObject.veteranLevel;\n        }\n        const shouldUpdateHighlight = this.highlightAnimRunner.shouldUpdate();\n        const isInvulnerable = this.gameObject.invulnerableTrait.isActive();\n        const invulnChanged = isInvulnerable !== this.lastInvulnerable;\n        this.lastInvulnerable = isInvulnerable;\n        if (isInvulnerable && invulnChanged) {\n            this.invulnAnimRunner.animate();\n        }\n        if (this.invulnAnimRunner.shouldUpdate()) {\n            this.invulnAnimRunner.tick(deltaTime);\n        }\n        if (shouldUpdateHighlight || invulnChanged || isInvulnerable) {\n            if (shouldUpdateHighlight) {\n                this.highlightAnimRunner.tick(deltaTime);\n            }\n            const invulnValue = isInvulnerable ? this.invulnAnimRunner.getValue() : 0;\n            const highlightValue = (shouldUpdateHighlight ? this.highlightAnimRunner.getValue() : 0) || invulnValue;\n            const ambientIntensity = this.lighting.getAmbientIntensity();\n            ExtraLightHelper.multiplyVxl(this.extraLight as any, this.baseExtraLight as any, ambientIntensity, highlightValue);\n        }\n        const isWarpedOut = this.gameObject.warpedOutTrait.isActive();\n        const warpedOutChanged = isWarpedOut !== this.lastWarpedOut;\n        this.lastWarpedOut = isWarpedOut;\n        const isCloaked = this.gameObject.cloakableTrait?.isCloaked();\n        const cloakedChanged = isCloaked !== this.lastCloaked;\n        this.lastCloaked = isCloaked;\n        if (warpedOutChanged || cloakedChanged) {\n            const opacity = isWarpedOut || isCloaked ? 0.5 : 1;\n            this.vxlBuilders.forEach((builder) => builder.setOpacity(opacity));\n            this.placeholder?.setOpacity(opacity);\n        }\n        const ownerColor = this.gameObject.owner.color;\n        if (this.lastOwnerColor !== ownerColor) {\n            this.palette.remap(ownerColor);\n            this.lastOwnerColor = ownerColor;\n            this.vxlBuilders.forEach((builder) => builder.setPalette(this.palette as any));\n            this.placeholder?.setPalette(this.palette as any);\n        }\n        const zone = this.gameObject.zone;\n        if (zone !== this.lastZone) {\n            if (this.gameObject.rules.missileSpawn &&\n                zone === ZoneType.Air &&\n                this.lastZone !== ZoneType.Air) {\n                this.renderableManager?.createTransientAnim(\"V3TAKOFF\", (anim) => anim.setPosition(this.withPosition.getPosition()));\n            }\n            this.lastZone = zone;\n        }\n        this.updateVxlRotation();\n    }\n    private updateVxlRotation(): void {\n        const { pitch, yaw, roll } = this.gameObject;\n        this.tiltObj.rotation.z = THREE.MathUtils.degToRad(roll);\n        this.tiltObj.rotation.x = THREE.MathUtils.degToRad(pitch);\n        this.tiltObj.rotation.y = THREE.MathUtils.degToRad(yaw);\n        if (this.rotors) {\n            this.rotors.forEach((rotor, index) => {\n                this.rotorSpeeds[index] = RotorHelper.computeRotationStep(this.gameObject, this.rotorSpeeds[index] ?? 0, this.objectArt.rotors[index]);\n                if (this.rotorSpeeds[index]) {\n                    rotor.rotateOnAxis(this.objectArt.rotors[index].axis, this.rotorSpeeds[index]);\n                    rotor.updateMatrix();\n                }\n            });\n        }\n    }\n    private createObjects(target: THREE.Object3D): void {\n        if (this.debugFrame.value) {\n            const wireframe = DebugUtils.createWireframe({ width: 1, height: 1 }, 1);\n            wireframe.translateX(-Coords.getWorldTileSize() / 2);\n            wireframe.translateZ(-Coords.getWorldTileSize() / 2);\n            target.add(wireframe);\n        }\n        const tiltObj = this.tiltObj = new THREE.Object3D();\n        tiltObj.rotation.order = \"YXZ\";\n        const mainObject = this.createMainObject();\n        tiltObj.add(mainObject);\n        const posObj = this.posObj = new THREE.Object3D();\n        posObj.matrixAutoUpdate = false;\n        posObj.add(tiltObj);\n        target.add(posObj);\n    }\n    private createMainObject(): THREE.Object3D {\n        const imageName = this.objectArt.imageName.toLowerCase();\n        const vxlFile = imageName + \".vxl\";\n        const voxel = this.voxels.get(vxlFile);\n        if (!voxel) {\n            console.warn(`VXL missing for aircraft ${this.objectRules.name}. Vxl file ${vxlFile} not found. `);\n            this.placeholder = new DebugRenderable({ width: 0.5, height: 0.5 }, this.objectArt.height, this.palette as any, { centerFoundation: true });\n            this.placeholder.setBatched(this.useSpriteBatching);\n            if (this.useSpriteBatching) {\n                this.placeholder.setBatchPalettes(this.paletteRemaps as any);\n            }\n            this.placeholder.create3DObject();\n            return this.placeholder.get3DObject();\n        }\n        const hvaFile = this.objectArt.noHva\n            ? undefined\n            : this.voxelAnims.get(imageName + \".hva\");\n        const palettes = [...this.rules.colors.values()].map((color) => this.palette.clone().remap(color));\n        const vxlBuilder = this.vxlBuilderFactory.create(voxel, hvaFile, palettes, this.palette);\n        this.vxlBuilders.push(vxlBuilder);\n        const builtObject = vxlBuilder.build();\n        if (this.objectArt.rotors) {\n            this.rotors = this.objectArt.rotors.map((rotorConfig: any) => {\n                const section = vxlBuilder.getSection(rotorConfig.name);\n                if (!section) {\n                    throw new Error(`Aircraft \"${this.objectRules.name}\" VXL section \"${rotorConfig.name}\" not found`);\n                }\n                return section;\n            });\n        }\n        return builtObject;\n    }\n    onCreate(renderableManager: RenderableManager): void {\n        this.renderableManager = renderableManager;\n        this.plugins.forEach((plugin) => plugin.onCreate(renderableManager));\n    }\n    onRemove(renderableManager: RenderableManager): void {\n        this.renderableManager = undefined;\n        this.plugins.forEach((plugin) => plugin.onRemove(renderableManager));\n        if (this.gameObject.isDestroyed &&\n            this.objectRules.explosion.length &&\n            this.gameObject.deathType !== DeathType.Temporal &&\n            this.gameObject.deathType !== DeathType.None) {\n            const explosions = this.objectRules.explosion;\n            const explosion = explosions[getRandomInt(0, explosions.length - 1)];\n            renderableManager.createTransientAnim(explosion, (anim) => {\n                let position = this.withPosition.getPosition();\n                if (this.gameObject.rules.missileSpawn) {\n                    position = position\n                        .clone()\n                        .add(this.gameObject.moveTrait.velocity\n                        .clone()\n                        .setLength(this.rules.general.getMissileRules(this.gameObject.name).bodyLength));\n                }\n                anim.setPosition(position);\n            });\n        }\n    }\n    dispose(): void {\n        this.plugins.forEach((plugin) => plugin.dispose());\n        this.pipOverlay?.dispose();\n        this.vxlBuilders.forEach((builder) => builder.dispose());\n        this.placeholder?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/Anim.ts",
    "content": "import { WithPosition } from \"@/engine/renderable/WithPosition\";\nimport { AnimProps } from \"@/engine/AnimProps\";\nimport { Animation, AnimationState } from \"@/engine/Animation\";\nimport { ShpRenderable } from \"@/engine/renderable/ShpRenderable\";\nimport { ImageFinder } from \"@/engine/ImageFinder\";\nimport { DebugUtils } from \"@/engine/gfx/DebugUtils\";\nimport { MapSpriteTranslation } from \"@/engine/renderable/MapSpriteTranslation\";\nimport { SimpleRunner } from \"@/engine/animation/SimpleRunner\";\nimport { MathUtils } from \"@/engine/gfx/MathUtils\";\nimport { Coords } from \"@/game/Coords\";\nimport * as THREE from 'three';\ninterface ObjectArt {\n    paletteType: string;\n    customPaletteName?: string;\n    startSound?: string;\n    report?: string;\n    translucent: boolean;\n    translucency: number;\n    zAdjust?: number;\n    flat: boolean;\n    art: any;\n    getDrawOffset(): {\n        x: number;\n        y: number;\n    };\n}\ninterface Theater {\n    getPalette(paletteType: string, customPaletteName?: string): any;\n}\ninterface Camera {\n}\ninterface DebugFrame {\n    value: boolean;\n}\ninterface WorldSound {\n    playEffect(sound: string, position: THREE.Vector3, param3?: any, param4?: number, param5?: number): SoundHandle;\n}\ninterface SoundHandle {\n    isLoop: boolean;\n    stop(): void;\n}\ninterface Palette {\n    clone(): Palette;\n    remap(colorMap: any): void;\n}\nexport class Anim {\n    public objectArt: ObjectArt;\n    public extraOffset: {\n        x: number;\n        y: number;\n    };\n    public imageFinder: ImageFinder;\n    public theater: Theater;\n    public camera: Camera;\n    public debugFrame: DebugFrame;\n    public gameSpeed: number;\n    public useSpriteBatching: boolean;\n    public extraLight: THREE.Vector3;\n    public worldSound?: WorldSound;\n    public renderOrder: number = 0;\n    public name: string;\n    public palette: Palette;\n    public withPosition: WithPosition;\n    private target?: THREE.Object3D;\n    private mainObj?: ShpRenderable;\n    private animation?: Animation;\n    private animationRunner?: SimpleRunner;\n    private shpFile?: any;\n    private soundHandle?: SoundHandle;\n    constructor(name: string, objectArt: ObjectArt, extraOffset: {\n        x: number;\n        y: number;\n    }, imageFinder: ImageFinder, theater: Theater, camera: Camera, debugFrame: DebugFrame, gameSpeed: number, useSpriteBatching: boolean, extraLight: THREE.Vector3 = new THREE.Vector3(0, 0, 0), worldSound?: WorldSound, palette?: Palette) {\n        this.objectArt = objectArt;\n        this.extraOffset = extraOffset;\n        this.imageFinder = imageFinder;\n        this.theater = theater;\n        this.camera = camera;\n        this.debugFrame = debugFrame;\n        this.gameSpeed = gameSpeed;\n        this.useSpriteBatching = useSpriteBatching;\n        this.extraLight = extraLight;\n        this.worldSound = worldSound;\n        this.name = name;\n        this.palette = palette ?? this.theater.getPalette(this.objectArt.paletteType, this.objectArt.customPaletteName);\n        this.withPosition = new WithPosition();\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    create3DObject(): void {\n        let obj = this.get3DObject();\n        if (!obj) {\n            obj = new THREE.Object3D();\n            obj.name = \"anim_\" + this.name;\n            this.target = obj;\n            obj.matrixAutoUpdate = false;\n            this.withPosition.matrixUpdate = true;\n            this.withPosition.applyTo(this);\n            this.createObjects(obj);\n        }\n    }\n    setPosition(position: {\n        x: number;\n        y: number;\n        z: number;\n    }): void {\n        this.withPosition.setPosition(position.x, position.y, position.z);\n    }\n    getPosition(): THREE.Vector3 {\n        return this.withPosition.getPosition();\n    }\n    update(deltaTime: number): void {\n        if (this.animationRunner && this.animation && this.mainObj) {\n            const sound = this.objectArt.startSound ?? this.objectArt.report;\n            if (sound && !this.soundHandle &&\n                this.animation.getState() === AnimationState.NOT_STARTED) {\n                this.soundHandle = this.worldSound?.playEffect(sound, this.withPosition.getPosition(), undefined, 1, 0.25);\n            }\n            this.animationRunner.tick(deltaTime);\n            const obj = this.mainObj.get3DObject();\n            if (obj) {\n                obj.visible = this.animation.getState() !== AnimationState.DELAYED;\n            }\n            this.mainObj.setFrame(this.animationRunner.getCurrentFrame());\n            const isTranslucent = this.objectArt.translucent;\n            const translucency = this.objectArt.translucency;\n            if (isTranslucent || translucency > 0) {\n                let opacity: number;\n                if (isTranslucent) {\n                    const props = this.animation.props;\n                    opacity = 1 - this.animationRunner.getCurrentFrame() / (props.end - props.start);\n                }\n                else {\n                    opacity = 1 - translucency;\n                }\n                this.mainObj.setOpacity(opacity);\n            }\n        }\n    }\n    private createObjects(parentObj: THREE.Object3D): void {\n        const dimensions = { width: 1, height: 1 };\n        if (this.debugFrame.value) {\n            const wireframe = DebugUtils.createWireframe(dimensions, 0);\n            parentObj.add(wireframe);\n        }\n        const spriteTranslation = new MapSpriteTranslation(dimensions.width, dimensions.height);\n        const { spriteOffset, anchorPointWorld } = spriteTranslation.compute();\n        const anchorOffset = this.computeSpriteAnchorOffset(spriteOffset);\n        const container = new THREE.Object3D();\n        container.matrixAutoUpdate = false;\n        this.mainObj = this.createMainObject(anchorOffset);\n        if (this.mainObj) {\n            this.mainObj.setExtraLight(this.extraLight);\n            const shouldBatch = this.useSpriteBatching && !this.renderOrder;\n            this.mainObj.setBatched(shouldBatch);\n            if (shouldBatch) {\n                this.mainObj.setBatchPalettes([this.palette]);\n            }\n            const isTranslucent = this.objectArt.translucent;\n            const translucency = this.objectArt.translucency;\n            if (isTranslucent || translucency > 0) {\n                this.mainObj.setForceTransparent(true);\n            }\n            this.mainObj.create3DObject();\n            if (this.renderOrder) {\n                if (shouldBatch) {\n                    throw new Error(\"Render order not supported with batching\");\n                }\n                const shapeMesh = this.mainObj.getShapeMesh();\n                shapeMesh.renderOrder = this.renderOrder;\n                (shapeMesh as any).material.depthTest = !this.renderOrder;\n                (shapeMesh as any).material.transparent = !!this.renderOrder;\n            }\n            const mainObj3D = this.mainObj.get3DObject();\n            if (mainObj3D) {\n                container.add(mainObj3D);\n            }\n            container.position.x = anchorPointWorld.x;\n            container.position.z = anchorPointWorld.y;\n            if (this.objectArt.zAdjust) {\n                MathUtils.translateTowardsCamera(container, this.camera as any, -this.objectArt.zAdjust * Coords.ISO_WORLD_SCALE);\n            }\n            container.updateMatrix();\n            parentObj.add(container);\n        }\n    }\n    setExtraLight(light: THREE.Vector3): void {\n        this.extraLight = light;\n        this.mainObj?.setExtraLight(this.extraLight);\n    }\n    setRenderOrder(order: number): void {\n        if (this.mainObj) {\n            throw new Error(\"Render order must be set before 3DObject is created\");\n        }\n        this.renderOrder = order;\n    }\n    private computeSpriteAnchorOffset(spriteOffset: {\n        x: number;\n        y: number;\n    }): {\n        x: number;\n        y: number;\n    } {\n        const drawOffset = this.objectArt.getDrawOffset();\n        return {\n            x: spriteOffset.x + drawOffset.x + this.extraOffset.x,\n            y: spriteOffset.y + drawOffset.y + this.extraOffset.y\n        };\n    }\n    private createMainObject(offset: {\n        x: number;\n        y: number;\n    }): ShpRenderable | undefined {\n        let shpFile: any;\n        try {\n            shpFile = this.shpFile = this.imageFinder.findByObjectArt(this.objectArt as any);\n        }\n        catch (error) {\n            if (error instanceof ImageFinder.MissingImageError) {\n                console.warn(error.message);\n                return undefined;\n            }\n            throw error;\n        }\n        const renderable = ShpRenderable.factory(shpFile, this.palette, this.camera, offset);\n        renderable.setFlat(this.objectArt.flat);\n        const animProps = new AnimProps(this.objectArt.art, shpFile);\n        this.animation = new Animation(animProps, this.gameSpeed as any);\n        this.animationRunner = new SimpleRunner();\n        this.animationRunner.animation = this.animation;\n        return renderable;\n    }\n    getAnimProps(): AnimProps | undefined {\n        return this.animation?.props;\n    }\n    getShpFile(): any {\n        return this.shpFile;\n    }\n    remapColor(colorMap: any): void {\n        if (this.mainObj) {\n            throw new Error(\"Palette can only be remapped before creating 3DObject\");\n        }\n        const clonedPalette = this.palette.clone();\n        clonedPalette.remap(colorMap);\n        this.palette = clonedPalette;\n    }\n    isAnimFinished(): boolean {\n        return this.animation?.getState() === AnimationState.STOPPED;\n    }\n    isAnimNotStarted(): boolean {\n        return this.animation?.getState() === AnimationState.NOT_STARTED;\n    }\n    endAnimationLoop(): void {\n        this.animation?.endLoopAndPlayToEnd();\n    }\n    reset(): void {\n        this.animation?.reset();\n    }\n    dispose(): void {\n        this.mainObj?.dispose();\n        if (this.soundHandle?.isLoop) {\n            this.soundHandle.stop();\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/BoxIntersectObject3D.ts",
    "content": "import * as THREE from 'three';\nexport class BoxIntersectObject3D extends THREE.Object3D {\n    private static ray: THREE.Ray = new THREE.Ray();\n    private static matrix: THREE.Matrix4 = new THREE.Matrix4();\n    private static box: THREE.Box3 = new THREE.Box3();\n    private static center: THREE.Vector3 = new THREE.Vector3();\n    private boxSize: THREE.Vector3;\n    constructor(boxSize: THREE.Vector3) {\n        super();\n        this.boxSize = boxSize;\n    }\n    raycast(raycaster: THREE.Raycaster, intersects: THREE.Intersection[]): void {\n        if (this.parent) {\n            BoxIntersectObject3D.matrix.copy(this.parent.matrixWorld).invert();\n            BoxIntersectObject3D.ray.copy(raycaster.ray).applyMatrix4(BoxIntersectObject3D.matrix);\n            BoxIntersectObject3D.center.copy(this.position);\n            const box = BoxIntersectObject3D.box.setFromCenterAndSize(BoxIntersectObject3D.center, this.boxSize);\n            if (BoxIntersectObject3D.ray.intersectsBox(box)) {\n                const point = new THREE.Vector3();\n                box.getCenter(point);\n                point.applyMatrix4(this.parent.matrixWorld);\n                intersects.push({\n                    distance: raycaster.ray.origin.distanceTo(point),\n                    point: point,\n                    object: this\n                });\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/Building.ts",
    "content": "import * as ShpBuilder from \"@/engine/renderable/builder/ShpBuilder\";\nimport * as DamageType from \"@/engine/renderable/entity/building/DamageType\";\nimport * as AnimationType from \"@/engine/renderable/entity/building/AnimationType\";\nimport * as OverlayUtils from \"@/engine/gfx/OverlayUtils\";\nimport * as GameObjectBuilding from \"@/game/gameobject/Building\";\nimport * as Animation from \"@/engine/Animation\";\nimport * as wallTypes from \"@/game/map/wallTypes\";\nimport * as Coords from \"@/game/Coords\";\nimport * as math from \"@/util/math\";\nimport * as AnimProps from \"@/engine/AnimProps\";\nimport * as WithPosition from \"@/engine/renderable/WithPosition\";\nimport * as ShpRenderable from \"@/engine/renderable/ShpRenderable\";\nimport * as ImageFinder from \"@/engine/ImageFinder\";\nimport { MissingImageError } from \"@/engine/ImageFinder\";\nimport * as DebugUtils from \"@/engine/gfx/DebugUtils\";\nimport * as MapSpriteTranslation from \"@/engine/renderable/MapSpriteTranslation\";\nimport * as BuildingAnimArtProps from \"@/engine/renderable/entity/building/BuildingAnimArtProps\";\nimport * as typeGuard from \"@/util/typeGuard\";\nimport * as HighlightAnimRunner from \"@/engine/renderable/entity/HighlightAnimRunner\";\nimport * as TechnoRules from \"@/game/rules/TechnoRules\";\nimport * as AttackTrait from \"@/game/gameobject/trait/AttackTrait\";\nimport * as SideType from \"@/game/SideType\";\nimport * as FactoryTrait from \"@/game/gameobject/trait/FactoryTrait\";\nimport * as UnitRepairTrait from \"@/game/gameobject/trait/UnitRepairTrait\";\nimport * as DeathType from \"@/game/gameobject/common/DeathType\";\nimport * as InvulnerableAnimRunner from \"@/engine/renderable/entity/InvulnerableAnimRunner\";\nimport * as BuildingShpHelper from \"@/engine/renderable/entity/building/BuildingShpHelper\";\nimport * as ExtraLightHelper from \"@/engine/renderable/entity/unit/ExtraLightHelper\";\nimport * as AlphaRenderable from \"@/engine/renderable/AlphaRenderable\";\nimport * as DebugRenderable from \"@/engine/renderable/DebugRenderable\";\nimport * as MathUtils from \"@/engine/gfx/MathUtils\";\nimport * as THREE from \"three\";\nconst d = ShpBuilder;\nconst p = DamageType;\nconst A = AnimationType;\nconst s = OverlayUtils;\nconst m = GameObjectBuilding;\nconst f = Animation;\nconst r = wallTypes;\nconst g = Coords;\nconst u = math;\nconst y = AnimProps;\nconst R = WithPosition;\nconst T = ShpRenderable;\nconst P = ImageFinder;\nconst v = DebugUtils;\nconst b = MapSpriteTranslation;\nconst I = BuildingAnimArtProps;\nconst i = typeGuard;\nconst k = HighlightAnimRunner;\nconst S = TechnoRules;\nconst w = AttackTrait;\nconst a = SideType;\nconst C = FactoryTrait;\nconst E = UnitRepairTrait;\nconst n = DeathType;\nconst B = InvulnerableAnimRunner;\nconst N = BuildingShpHelper;\nconst x = ExtraLightHelper;\nconst o = AlphaRenderable;\nconst O = DebugRenderable;\nconst M = MathUtils;\nconst j = new Map()\n    .set(A.AnimationType.PRODUCTION, A.AnimationType.IDLE)\n    .set(A.AnimationType.BUILDUP, A.AnimationType.IDLE)\n    .set(A.AnimationType.SPECIAL_DOCKING, A.AnimationType.IDLE)\n    .set(A.AnimationType.SPECIAL_REPAIR_START, A.AnimationType.SPECIAL_REPAIR_LOOP)\n    .set(A.AnimationType.SPECIAL_REPAIR_LOOP, A.AnimationType.SPECIAL_REPAIR_END)\n    .set(A.AnimationType.SPECIAL_REPAIR_END, A.AnimationType.IDLE)\n    .set(A.AnimationType.SUPER_CHARGE_START, A.AnimationType.SUPER_CHARGE_LOOP)\n    .set(A.AnimationType.SUPER_CHARGE_LOOP, A.AnimationType.SUPER_CHARGE_END)\n    .set(A.AnimationType.SUPER_CHARGE_END, A.AnimationType.IDLE)\n    .set(A.AnimationType.FACTORY_DEPLOYING, A.AnimationType.IDLE)\n    .set(A.AnimationType.FACTORY_ROOF_DEPLOYING, A.AnimationType.IDLE);\nconst l = new Map()\n    .set(A.AnimationType.SUPER_CHARGE_START, [\n    A.AnimationType.SUPER,\n    1,\n])\n    .set(A.AnimationType.SUPER_CHARGE_LOOP, [\n    A.AnimationType.SUPER,\n    2,\n])\n    .set(A.AnimationType.SUPER_CHARGE_END, [A.AnimationType.SUPER, 3])\n    .set(A.AnimationType.SPECIAL_REPAIR_START, [\n    A.AnimationType.SPECIAL,\n    0,\n])\n    .set(A.AnimationType.SPECIAL_REPAIR_LOOP, [\n    A.AnimationType.SPECIAL,\n    1,\n])\n    .set(A.AnimationType.SPECIAL_REPAIR_END, [\n    A.AnimationType.SPECIAL,\n    2,\n])\n    .set(A.AnimationType.SPECIAL_DOCKING, [\n    A.AnimationType.SPECIAL,\n    0,\n])\n    .set(A.AnimationType.SPECIAL_SHOOT, [A.AnimationType.SPECIAL, 0])\n    .set(A.AnimationType.FACTORY_DEPLOYING, [\n    A.AnimationType.FACTORY_DEPLOYING,\n    0,\n])\n    .set(A.AnimationType.FACTORY_UNDER_DOOR, [\n    A.AnimationType.FACTORY_DEPLOYING,\n    1,\n])\n    .set(A.AnimationType.FACTORY_ROOF_DEPLOYING, [\n    A.AnimationType.FACTORY_ROOF_DEPLOYING,\n    0,\n])\n    .set(A.AnimationType.FACTORY_UNDER_ROOF_DOOR, [\n    A.AnimationType.FACTORY_ROOF_DEPLOYING,\n    1,\n]);\nexport class Building {\n    static lampTextures = new Map();\n    gameObject: any;\n    selectionModel: any;\n    rules: any;\n    art: any;\n    imageFinder: any;\n    voxels: any;\n    voxelAnims: any;\n    palette: any;\n    animPalette: any;\n    isoPalette: any;\n    camera: any;\n    lighting: any;\n    debugFrame: any;\n    gameSpeed: any;\n    vxlBuilderFactory: any;\n    useSpriteBatching: any;\n    buildingImageDataCache: any;\n    pipOverlay: any;\n    worldSound: any;\n    initialAnimType: any;\n    animObjects: Map<any, any>;\n    animations: Map<any, any>;\n    animSounds: Map<any, any>;\n    powered: boolean;\n    repairStopRequested: boolean;\n    repairStartRequested: boolean;\n    highlightAnimRunner: any;\n    invulnAnimRunner: any;\n    plugins: any[];\n    objectArt: any;\n    objectRules: any;\n    type: any;\n    paletteRemaps: any;\n    lastOwnerColor: any;\n    vxlExtraLight: any;\n    shpExtraLight: any;\n    baseVxlExtraLight: any;\n    baseShpExtraLight: any;\n    animArtProps: any;\n    mainShpFile: any;\n    bibShpFile: any;\n    animShpFiles: any;\n    shpFrameInfos: any;\n    aggregatedImageData: any;\n    withPosition: any;\n    target: any;\n    intersectTarget: any;\n    placeholderObj: any;\n    mainObj: any;\n    bib: any;\n    fireObjects: any;\n    turretBuilders: any;\n    turret: any;\n    turretRot: any;\n    rubbleObj: any;\n    rangeCircle: any;\n    rangeCircleWrapper: any;\n    spriteOffset: any;\n    spriteWrap: any;\n    muzzleAnims: any;\n    currentAnimType: any;\n    lastHasC4Charge: any;\n    lastInvulnerable: any;\n    lastWarpedOut: any;\n    lastAttackState: any;\n    lastFactoryStatus: any;\n    lastRepairStatus: any;\n    lastSuperWeaponAlmostCharged: any;\n    lastTurretFacing: any;\n    lastTurretRotating: any;\n    lastPowered: any;\n    lastOccupiedState: any;\n    lastHealth: any;\n    lastWallType: any;\n    lastOverpowered: any;\n    renderableManager: any;\n    ambientSound: any;\n    turretRotateSound: any;\n    poweredSound: any;\n    constructor(e: any, t: any, i: any, r: any, s: any, a: any, n: any, o: any, l: any, c: any, h: any, u: any, d: any, g: any, p: any, m: any, f: any, y: any, T: any, v: any, b: any, S = A.AnimationType.IDLE) {\n        this.gameObject = e;\n        this.selectionModel = t;\n        this.rules = i;\n        this.art = r;\n        this.imageFinder = s;\n        this.voxels = n;\n        this.voxelAnims = o;\n        this.palette = l;\n        this.animPalette = c;\n        this.isoPalette = h;\n        this.camera = u;\n        this.lighting = d;\n        this.debugFrame = g;\n        this.gameSpeed = p;\n        this.vxlBuilderFactory = m;\n        this.useSpriteBatching = f;\n        this.buildingImageDataCache = T;\n        this.pipOverlay = v;\n        this.worldSound = b;\n        this.initialAnimType = S;\n        this.animObjects = new Map();\n        this.animations = new Map();\n        this.animSounds = new Map();\n        this.powered = true;\n        this.repairStopRequested = false;\n        this.repairStartRequested = false;\n        this.highlightAnimRunner = new k.HighlightAnimRunner(this.gameSpeed);\n        this.invulnAnimRunner = new B.InvulnerableAnimRunner(this.gameSpeed);\n        this.plugins = [];\n        this.objectArt = e.art;\n        this.objectRules = e.rules;\n        this.type = this.objectRules.name;\n        this.paletteRemaps = [...this.rules.colors.values()].map((e) => this.palette.clone().remap(e));\n        this.palette.remap(this.gameObject.owner.color);\n        this.lastOwnerColor = this.gameObject.owner.color;\n        this.updateBaseLight();\n        this.vxlExtraLight = new THREE.Vector3().copy(this.baseVxlExtraLight);\n        this.shpExtraLight = new THREE.Vector3().copy(this.baseShpExtraLight);\n        var w = (this.animArtProps = new I.BuildingAnimArtProps());\n        this.animArtProps.read(this.objectArt.art, this.art);\n        let C;\n        try {\n            C = this.imageFinder.findByObjectArt(this.objectArt);\n        }\n        catch (e) {\n            if (!(e instanceof MissingImageError))\n                throw e;\n            console.warn(e.message);\n        }\n        this.mainShpFile = C;\n        let E;\n        try {\n            E = this.objectArt.bibShape\n                ? this.imageFinder.find(this.objectArt.bibShape, this.objectArt.useTheaterExtension)\n                : void 0;\n        }\n        catch (e) {\n            if (!(e instanceof MissingImageError))\n                throw e;\n            console.warn(e.message);\n        }\n        this.bibShpFile = E;\n        let x = new N.BuildingShpHelper(this.imageFinder);\n        w = this.animShpFiles = x.collectAnimShpFiles(w as any, this.objectArt) as any;\n        let O = (this.shpFrameInfos = x.getShpFrameInfos(this.objectArt, C, E, w as any)), M = this.buildingImageDataCache.get(this.gameObject.name);\n        M ||\n            ((M = y.aggregate(O.values(), `agg_${this.objectRules.name}.shp`)),\n                this.buildingImageDataCache.set(this.gameObject.name, M)),\n            (this.aggregatedImageData = M),\n            (this.withPosition = new R.WithPosition());\n    }\n    updateBaseLight() {\n        (this.baseShpExtraLight = this.lighting\n            .compute(this.objectArt.lightingType, this.gameObject.tile)\n            .addScalar(-1)),\n            (this.baseVxlExtraLight = new THREE.Vector3().setScalar(Math.PI * 1.5 + this.lighting.computeNoAmbient(this.objectArt.lightingType, this.gameObject.tile)));\n    }\n    updateLighting() {\n        this.updateBaseLight(),\n            this.vxlExtraLight.copy(this.baseVxlExtraLight),\n            this.shpExtraLight.copy(this.baseShpExtraLight),\n            this.plugins.forEach((e) => e.updateLighting?.());\n    }\n    get3DObject() {\n        return this.target;\n    }\n    getIntersectTarget() {\n        return this.intersectTarget;\n    }\n    updateIntersectTarget() {\n        this.intersectTarget = [\n            this.placeholderObj?.get3DObject(),\n            this.mainObj?.getShapeMesh(),\n            this.bib?.getShapeMesh(),\n            ...[...this.animObjects.values()]\n                .flat()\n                .map((e) => e.getShapeMesh()),\n        ].filter(i.isNotNullOrUndefined);\n    }\n    getUiName() {\n        var e = this.plugins.reduce((e, t) => t.getUiNameOverride?.() ?? e, void 0);\n        return void 0 !== e ? e : this.gameObject.getUiName();\n    }\n    create3DObject() {\n        let t = this.get3DObject();\n        if (!t) {\n            (t = new THREE.Object3D()),\n                (t.name = \"building_\" + this.type),\n                (t.userData.id = this.gameObject.id),\n                (this.target = t),\n                (t.matrixAutoUpdate = false),\n                (this.withPosition.matrixUpdate = true),\n                this.withPosition.applyTo(this);\n            var e = this.gameObject.rules.alphaImage;\n            if (e) {\n                var i = this.imageFinder.tryFind(e, false);\n                if (i) {\n                    let e = new o.AlphaRenderable(i, this.camera, new THREE.Vector2(0, (g.Coords.ISO_TILE_SIZE + 1) / 2));\n                    e.create3DObject(), t.add(e.get3DObject());\n                }\n                else\n                    console.warn(`<${this.objectRules.name}>: Alpha image \"${e}\" not found`);\n            }\n            (this.objectRules.lightIntensity &&\n                (this.createLamp(t), this.objectRules.isLightpost)) ||\n                (this.createObjects(t),\n                    this.updateIntersectTarget(),\n                    this.pipOverlay &&\n                        (this.pipOverlay.create3DObject(),\n                            t.add(this.pipOverlay.get3DObject())),\n                    this.updateImage(this.computeDamageType(this.gameObject.healthTrait.health)),\n                    this.mainObj?.setExtraLight(this.shpExtraLight),\n                    [...this.animObjects.values()].forEach((e) => {\n                        e.forEach((e) => {\n                            let t = this.animations.get(e);\n                            t.props.getArt().has(\"UseNormalLight\") ||\n                                e.setExtraLight(this.shpExtraLight);\n                        });\n                    }),\n                    this.bib?.setExtraLight(this.shpExtraLight),\n                    this.turretBuilders?.forEach((e) => {\n                        e instanceof d.ShpBuilder\n                            ? e.setExtraLight(this.shpExtraLight)\n                            : e.setExtraLight(this.vxlExtraLight);\n                    }));\n        }\n    }\n    createLamp(e) {\n        var t = this.objectRules;\n        let i = (1 + t.lightRedTint) * (1 + Math.abs(t.lightIntensity)) -\n            1, r = (1 + t.lightGreenTint) *\n            (1 + Math.abs(t.lightIntensity)) -\n            1, s = (1 + t.lightBlueTint) * (1 + Math.abs(t.lightIntensity)) -\n            1;\n        var a = Math.max(i, r, s);\n        1 < a && ((i /= a), (r /= a), (s /= a));\n        let n = new THREE.Color(i, r, s).multiplyScalar(0.9);\n        a = n.getHexString() as any;\n        let o = Building.lampTextures.get(a);\n        if (!o) {\n            o = this.createLampTexture(a);\n            Building.lampTextures.set(a, o);\n        }\n        (a = new THREE.MeshBasicMaterial({\n            map: o,\n            depthTest: false,\n            depthWrite: false,\n            transparent: true,\n            blending: THREE.CustomBlending,\n            blendEquation: 0 < t.lightIntensity\n                ? THREE.AddEquation\n                : THREE.ReverseSubtractEquation,\n            blendSrc: THREE.DstColorFactor,\n            blendDst: THREE.OneFactor,\n        }) as any),\n            (t = t.lightVisibility),\n            (t = new THREE.PlaneGeometry(2 * t, 2 * t));\n        let l = new THREE.Mesh(t, a as any);\n        (l.rotation.x = -Math.PI / 2),\n            (l.renderOrder = 999995),\n            (l.matrixAutoUpdate = false),\n            l.updateMatrix(),\n            e.add(l);\n    }\n    createLampTexture(e) {\n        let t = document.createElement(\"canvas\");\n        t.width = t.height = 32;\n        let i = t.getContext(\"2d\");\n        (i.fillStyle = \"black\"), i.fillRect(0, 0, 32, 32);\n        let r = i.createRadialGradient(16, 16, 0, 16, 16, 16);\n        r.addColorStop(0, \"#\" + e),\n            r.addColorStop(1, \"black\"),\n            i.arc(16, 16, 16, 0, 2 * Math.PI),\n            (i.fillStyle = r),\n            i.fill();\n        let s = new THREE.Texture(t);\n        return (s.needsUpdate = true), s;\n    }\n    setPosition(e) {\n        var t = this.gameObject.getFoundationCenterOffset();\n        this.withPosition.setPosition(e.x - t.x, e.y, e.z - t.y);\n    }\n    getPosition() {\n        return this.withPosition.getPosition();\n    }\n    registerPlugin(e) {\n        this.plugins.push(e);\n    }\n    highlight() {\n        this.plugins.some((e) => e.shouldDisableHighlight?.()) ||\n            this.highlightAnimRunner.animate(2);\n    }\n    update(i) {\n        if (!this.objectRules.isLightpost) {\n            this.gameObject.isDestroyed ||\n                void 0 !== this.currentAnimType ||\n                this.setAnimation(this.initialAnimType, i),\n                this.plugins.forEach((e) => e.update(i)),\n                this.pipOverlay?.update(i);\n            var t = this.gameObject.c4ChargeTrait?.hasCharge();\n            !this.gameObject.isDestroyed &&\n                this.lastHasC4Charge !== t &&\n                t &&\n                ((this.lastHasC4Charge = t), this.highlight());\n            var r = this.highlightAnimRunner.shouldUpdate(), s = this.gameObject.invulnerableTrait.isActive(), t = (s !== this.lastInvulnerable) as any;\n            (this.lastInvulnerable = s) &&\n                t &&\n                this.invulnAnimRunner.animate(),\n                this.invulnAnimRunner.shouldUpdate() &&\n                    this.invulnAnimRunner.tick(i),\n                (r || t || s) &&\n                    (r && this.highlightAnimRunner.tick(i),\n                        (s = s ? this.invulnAnimRunner.getValue() : 0),\n                        (n =\n                            (r ? this.highlightAnimRunner.getValue() : 0) || s),\n                        (s = this.lighting.getAmbientIntensity()),\n                        x.ExtraLightHelper.multiplyVxl(this.vxlExtraLight, this.baseVxlExtraLight, s, n),\n                        x.ExtraLightHelper.multiplyShp(this.shpExtraLight, this.baseShpExtraLight, n));\n            var a, n = this.gameObject.warpedOutTrait.isActive();\n            if (n !== this.lastWarpedOut) {\n                let t = (this.lastWarpedOut = n) ? 0.5 : 1;\n                for (a of [\n                    this.mainObj,\n                    this.bib,\n                    ...[...this.animObjects.values()].flat(),\n                ])\n                    a?.setOpacity(t);\n                this.turretBuilders?.forEach((e) => e.setOpacity(t)),\n                    this.placeholderObj?.setOpacity(t);\n            }\n            this.gameObject.isDestroyed ||\n                ((o = this.gameObject.owner.color),\n                    this.lastOwnerColor !== o &&\n                        (this.palette.remap(o),\n                            this.mainObj?.setPalette(this.palette),\n                            [...this.animObjects.values()].forEach((e) => {\n                                e.forEach((e) => e.setPalette(this.palette));\n                            }),\n                            this.bib?.setPalette(this.palette),\n                            this.turretBuilders?.forEach((e) => e.setPalette(this.palette)),\n                            this.placeholderObj?.setPalette(this.palette),\n                            (this.lastOwnerColor = o))),\n                !this.gameObject.isDestroyed &&\n                    j.has(this.currentAnimType) &&\n                    ((l = j.get(this.currentAnimType)),\n                        this.hasObjectWithStoppedAnimation(this.currentAnimType) && this.setAnimation(l, i)),\n                this.gameObject.isDestroyed ||\n                    this.gameObject.buildStatus !==\n                        m.BuildStatus.BuildDown ||\n                    this.currentAnimType === A.AnimationType.UNBUILD ||\n                    this.setAnimation(A.AnimationType.UNBUILD, i);\n            var o = this.gameObject.attackTrait?.attackState;\n            if ((void 0 === this.lastAttackState ||\n                (this.lastAttackState !== o &&\n                    !this.gameObject.isDestroyed)) &&\n                ((this.lastAttackState = o),\n                    !this.gameObject.isDestroyed &&\n                        this.hasAnimation(A.AnimationType.SPECIAL_SHOOT) &&\n                        (o === w.AttackState.FireUp\n                            ? this.setAnimation(A.AnimationType.SPECIAL_SHOOT, i)\n                            : this.currentAnimType ===\n                                A.AnimationType.SPECIAL_SHOOT &&\n                                this.setAnimation(A.AnimationType.IDLE, i)),\n                    o === w.AttackState.JustFired &&\n                        this.objectArt.muzzleFlash)) {\n                let e = this.createMuzzleFlashAnim(this.spriteOffset, this.renderableManager);\n                e &&\n                    (e.create3DObject(),\n                        this.spriteWrap.add(e.get3DObject()),\n                        (this.muzzleAnims = this.muzzleAnims || []),\n                        this.muzzleAnims.push(e));\n            }\n            var l = this.gameObject.factoryTrait;\n            if (l) {\n                var c = l.status;\n                if (this.lastFactoryStatus !== c &&\n                    !this.gameObject.isDestroyed) {\n                    o = this.lastFactoryStatus;\n                    if (((this.lastFactoryStatus = c), void 0 !== o)) {\n                        let e, t = false;\n                        [\n                            S.FactoryType.BuildingType,\n                            S.FactoryType.NavalUnitType,\n                        ].includes(l.type)\n                            ? (e = A.AnimationType.PRODUCTION)\n                            : l.type === S.FactoryType.UnitType\n                                ? ((e = l.deliveringUnit?.rules.consideredAircraft\n                                    ? A.AnimationType.FACTORY_ROOF_DEPLOYING\n                                    : A.AnimationType.FACTORY_DEPLOYING),\n                                    (t = true))\n                                : (e = void 0),\n                            e &&\n                                this.hasAnimation(e) &&\n                                (c === C.FactoryStatus.Delivering\n                                    ? this.setAnimation(e, i)\n                                    : t &&\n                                        this.setAnimation(A.AnimationType.IDLE, i));\n                    }\n                }\n            }\n            c = this.gameObject.unitRepairTrait?.status;\n            this.lastRepairStatus === c ||\n                this.gameObject.isDestroyed ||\n                ((d = this.lastRepairStatus),\n                    (this.lastRepairStatus = c),\n                    this.hasAnimation(A.AnimationType.SPECIAL_REPAIR_START) &&\n                        (c === E.RepairStatus.Repairing\n                            ? ((this.currentAnimType !==\n                                A.AnimationType.SPECIAL_REPAIR_LOOP &&\n                                this.currentAnimType !==\n                                    A.AnimationType.SPECIAL_REPAIR_END) ||\n                                (d as any) !== E.RepairStatus.Idle\n                                ? this.setAnimation(A.AnimationType.SPECIAL_REPAIR_START, i)\n                                : (this.repairStartRequested = true),\n                                (this.repairStopRequested = false))\n                            : (this.currentAnimType ===\n                                A.AnimationType.SPECIAL_REPAIR_START\n                                ? (this.repairStopRequested = true)\n                                : this.endCurrentAnimation(),\n                                (this.repairStartRequested = false))));\n            let e = this.gameObject.superWeaponTrait?.getSuperWeapon(this.gameObject);\n            !e ||\n                !this.hasAnimation(A.AnimationType.SUPER_CHARGE_START) ||\n                this.gameObject.isDestroyed ||\n                ((g =\n                    e.getTimerSeconds() <=\n                        60 * this.objectRules.chargedAnimTime) !==\n                    this.lastSuperWeaponAlmostCharged &&\n                    ((this.lastSuperWeaponAlmostCharged = g)\n                        ? this.setAnimation(A.AnimationType.SUPER_CHARGE_START, i)\n                        : this.endCurrentAnimation())),\n                this.repairStopRequested &&\n                    this.currentAnimType ===\n                        A.AnimationType.SPECIAL_REPAIR_LOOP &&\n                    (this.endCurrentAnimation(),\n                        (this.repairStopRequested = false)),\n                this.repairStartRequested &&\n                    this.currentAnimType === A.AnimationType.IDLE &&\n                    (this.setAnimation(A.AnimationType.SPECIAL_REPAIR_START, i),\n                        (this.repairStartRequested = false)),\n                this.muzzleAnims && this.updateMuzzleAnims(i),\n                n ||\n                    (this.animations.forEach((e, t) => {\n                        switch (e.getState()) {\n                            case f.AnimationState.STOPPED:\n                                return;\n                            case f.AnimationState.DELAYED:\n                                e.update(i),\n                                    (t.get3DObject().visible =\n                                        e.getState() !== f.AnimationState.DELAYED);\n                                break;\n                            case f.AnimationState.NOT_STARTED:\n                                e.start(i);\n                            // falls through\n                            case f.AnimationState.RUNNING:\n                            default:\n                                e.update(i);\n                        }\n                        t.setFrame(e.getCurrentFrame());\n                    }),\n                        this.animObjects.forEach((e, t) => {\n                            let a = this.animArtProps.getByType(t);\n                            e.forEach((t, e) => {\n                                var i = a[e];\n                                let r = this.animations.get(t);\n                                var s = i.translucent, i = i.translucency;\n                                if (s || 0 < i) {\n                                    let e;\n                                    (e = s\n                                        ? ((s = r.props),\n                                            1 - r.getCurrentFrame() / (s.end - s.start))\n                                        : 1 - i),\n                                        t.setOpacity(e);\n                                }\n                            });\n                        })),\n                this.toggleRangeCircleVisibility((this.gameObject.showWeaponRange ||\n                    (this.selectionModel.isSelected() &&\n                        -1 !== this.gameObject.rules.techLevel)) &&\n                    !n);\n            var h, u, c = (this.gameObject.wallTrait?.wallType !==\n                this.lastWallType) as any, d = void 0 === this.lastOccupiedState ||\n                this.lastOccupiedState !==\n                    !!this.gameObject.garrisonTrait?.isOccupied(), g = void 0 === this.lastHealth ||\n                this.lastHealth !== this.gameObject.healthTrait.health;\n            (c || d || g) &&\n                ((h = this.computeDamageType(this.gameObject.healthTrait.health)),\n                    (g = g && h !== this.computeDamageType(this.lastHealth)),\n                    (this.lastOccupiedState =\n                        !!this.gameObject.garrisonTrait?.isOccupied()),\n                    (this.lastHealth = this.gameObject.healthTrait.health),\n                    (this.lastWallType = this.gameObject.wallTrait?.wallType),\n                    (c || d || g) && this.updateImage(h),\n                    g &&\n                        h === p.DamageType.DESTROYED &&\n                        this.objectRules.explosion?.length &&\n                        this.createExplosionAnims(this.renderableManager)),\n                this.gameObject.turretTrait &&\n                    ((h = this.gameObject.turretTrait.facing) !==\n                        this.lastTurretFacing &&\n                        ((this.lastTurretFacing = h),\n                            (this.turretRot.rotation.y = THREE.MathUtils.degToRad(h)),\n                            this.turretRot.updateMatrix()),\n                        (h = this.gameObject.turretTrait.isRotating() && !n),\n                        this.lastTurretRotating !== h &&\n                            ((this.lastTurretRotating = h),\n                                (u = this.objectRules.turretRotateSound) &&\n                                    (h && !this.gameObject.isDestroyed\n                                        ? (this.turretRotateSound =\n                                            this.worldSound?.playEffect(u, this.gameObject, this.gameObject.owner))\n                                        : this.turretRotateSound?.stop()))),\n                this.gameObject.poweredTrait &&\n                    (this.gameObject.isDestroyed\n                        ? this.poweredSound &&\n                            (this.poweredSound.stop(),\n                                (this.poweredSound = void 0))\n                        : (u =\n                            this.gameObject.poweredTrait.isPoweredOn() &&\n                                !n) !== this.lastPowered &&\n                            (this.setPowered(u),\n                                (this.lastPowered = u),\n                                this.poweredSound?.stop(),\n                                (u = u\n                                    ? this.gameObject.rules.workingSound\n                                    : this.gameObject.rules.notWorkingSound) &&\n                                    !n &&\n                                    (this.poweredSound = this.worldSound?.playEffect(u, this.gameObject, this.gameObject.owner, 0.25))));\n        }\n    }\n    createExplosionAnims(e) {\n        var i = this.objectArt.foundation, r = this.objectRules.explosion;\n        for (let a = 0; a < i.width; a++)\n            for (let t = 0; t < i.height; t++) {\n                var s = r[u.getRandomInt(0, r.length - 1)];\n                e.createTransientAnim(s, (e) => {\n                    e.setPosition(g.Coords.tile3dToWorld(a, t, 0).add(this.withPosition.getPosition()));\n                });\n            }\n    }\n    updateMuzzleAnims(t) {\n        let i = this.muzzleAnims, r = [];\n        i.forEach((e) => {\n            e.update(t),\n                e.isAnimFinished() &&\n                    (this.spriteWrap.remove(e.get3DObject()),\n                        e.dispose(),\n                        r.push(e));\n        }),\n            r.forEach((e) => i.splice(i.indexOf(e), 1));\n    }\n    getNormalizedAnimType(e) {\n        let t = 0, i = e;\n        return l.has(e) && ([i, t] = l.get(e)), [i, t];\n    }\n    hasObjectWithStoppedAnimation(t) {\n        var [i, r] = this.getNormalizedAnimType(t), list = this.animObjects.get(i);\n        if (list && list.length > 0) {\n            const clampedIndex = Math.min(r, list.length - 1);\n            const animObj = list[clampedIndex];\n            const anim = this.animations.get(animObj);\n            if (anim && anim.getState() === f.AnimationState.STOPPED)\n                return true;\n        }\n        return false;\n    }\n    computeDamageType(e) {\n        if (!e)\n            return p.DamageType.DESTROYED;\n        let t;\n        return ((t =\n            e > 100 * this.rules.audioVisual.conditionYellow\n                ? p.DamageType.NORMAL\n                : e > 100 * this.rules.audioVisual.conditionRed\n                    ? p.DamageType.CONDITION_YELLOW\n                    : p.DamageType.CONDITION_RED),\n            ((t && this.objectRules.canBeOccupied) ||\n                t === p.DamageType.CONDITION_RED) &&\n                (t -= 1),\n            t);\n    }\n    updateImage(o) {\n        let l = o === p.DamageType.DESTROYED;\n        l\n            ? (this.objectRules.leaveRubble &&\n                this.rubbleObj &&\n                (this.rubbleObj.get3DObject().visible = true),\n                this.mainObj && (this.mainObj.get3DObject().visible = false))\n            : this.gameObject.wallTrait\n                ? this.updateWallImage(this.gameObject.wallTrait.wallType, o)\n                : this.updateMainObjFrame(!!this.gameObject.garrisonTrait?.isOccupied(), o),\n            this.bib &&\n                (l && (this.bib.get3DObject().visible = false),\n                    this.bib.setFrame(o !== p.DamageType.NORMAL ? 1 : 0)),\n            this.turret && l && (this.turret.visible = false),\n            this.animObjects.forEach((a, n) => {\n                a.forEach((t, i) => {\n                    if (n !== A.AnimationType.BUILDUP &&\n                        n !== A.AnimationType.UNBUILD) {\n                        l && a.forEach((e) => (e.get3DObject().visible = false));\n                        let e = this.animations.get(t);\n                        var r = o !== p.DamageType.NORMAL, s = this.animArtProps.getByType(n)[i];\n                        !r || s.damagedArt\n                            ? (e.props.setArt(r ? s.damagedArt : s.art),\n                                e.rewind())\n                            : console.warn(`<${this.gameObject.name}>: Missing damaged anim ${A.AnimationType[n]},` +\n                                i);\n                    }\n                });\n            });\n        let r = o !== p.DamageType.NORMAL && !l;\n        this.fireObjects.forEach((e) => {\n            e.get3DObject().visible = r;\n            let t = this.animations.get(e);\n            t.rewind();\n            var i = t.props.getArt().getString(\"StartSound\");\n            i && this.handleSoundChange(i, e, r, 0.15);\n        });\n    }\n    updateMainObjFrame(e, t) {\n        let i = e ? 2 : t;\n        var r;\n        this.mainShpFile &&\n            this.mainObj &&\n            ((r = this.shpFrameInfos.get(this.mainShpFile).frameCount),\n                i >= r &&\n                    (console.warn(`Building ${this.objectRules.name} has damage frame ` +\n                        i +\n                        ` (occupied=${e}, damageType=${p.DamageType[t]}) out of bounds`),\n                        (i = p.DamageType.NORMAL)),\n                this.mainObj.setFrame(i));\n    }\n    updateWallImage(e, t) {\n        var i;\n        this.mainObj &&\n            this.mainShpFile &&\n            ((i =\n                this.shpFrameInfos.get(this.mainShpFile).frameCount <\n                    r.wallTypes.length\n                    ? 1\n                    : r.wallTypes.length),\n                this.mainObj.setFrame(e + t * i));\n    }\n    createObjects(t) {\n        var e = this.objectArt.foundation;\n        this.debugFrame.value &&\n            ((a = v.DebugUtils.createWireframe(e, this.objectArt.height) as any),\n                t.add(a));\n        let i = new b.MapSpriteTranslation(e.width, e.height);\n        var { spriteOffset: r, anchorPointWorld: s } = i.compute() as any, a = (this.spriteOffset = this.computeSpriteAnchorOffset(r));\n        let n = (this.spriteWrap = new THREE.Object3D());\n        n.matrixAutoUpdate = false;\n        let o = n, l = { ...a }, c = false;\n        r = this.objectArt.zShapePointMove;\n        if ((this.gameObject.rules.refinery ||\n            this.gameObject.rules.nukeSilo) &&\n            r.length) {\n            (o = new THREE.Object3D()),\n                (o.matrixAutoUpdate = false),\n                n.add(o),\n                (c = true);\n            r = {\n                x: -r[0] / g.Coords.ISO_TILE_SIZE,\n                y: -r[1] / g.Coords.ISO_TILE_SIZE,\n            } as any;\n            let e = new b.MapSpriteTranslation(r.x, r.y);\n            var { spriteOffset: h, anchorPointWorld: r } = e.compute() as any;\n            (o.position.x = r.x),\n                (o.position.z = r.y),\n                o.updateMatrix(),\n                (l.x += h.x),\n                (l.y += h.y);\n        }\n        this.mainShpFile\n            ? ((this.mainObj = this.createMainObject(this.mainShpFile, l, c)),\n                this.mainObj.create3DObject(),\n                o.add(this.mainObj.get3DObject()),\n                this.mainObj.getFlat() &&\n                    (M.MathUtils.translateTowardsCamera(this.mainObj.get3DObject(), this.camera, +g.Coords.ISO_WORLD_SCALE),\n                        this.mainObj.get3DObject().updateMatrix()))\n            : ((this.placeholderObj = new O.DebugRenderable(e, this.objectArt.height, this.palette)),\n                this.placeholderObj.setBatched(this.useSpriteBatching),\n                this.useSpriteBatching &&\n                    this.placeholderObj.setBatchPalettes(this.paletteRemaps),\n                this.placeholderObj.create3DObject(),\n                t.add(this.placeholderObj.get3DObject())),\n            this.objectRules.leaveRubble &&\n                ((this.rubbleObj = this.createRubbleObject(a)),\n                    this.rubbleObj &&\n                        (this.rubbleObj.setExtraLight(this.shpExtraLight),\n                            this.rubbleObj.create3DObject(),\n                            (this.rubbleObj.get3DObject().visible = false),\n                            n.add(this.rubbleObj.get3DObject())));\n        let u = this.createAnimObjects(l, c);\n        if ((u.forEach((e) => {\n            o.add(e);\n        }),\n            (this.fireObjects = this.createFireObjects(a)),\n            this.fireObjects.forEach((e) => {\n                n.add(e.get3DObject());\n            }),\n            this.objectRules.turret &&\n                (({ turret: h, turretRot: e } = this.createTurretObject(a, s) as any),\n                    (this.turret = h),\n                    (this.turretRot = e),\n                    n.add(this.turret)),\n            this.bibShpFile)) {\n            (this.bib = this.createBibObject(this.bibShpFile, a)),\n                this.bib.create3DObject();\n            let e = this.bib.get3DObject();\n            M.MathUtils.translateTowardsCamera(e, this.camera, -1),\n                e.updateMatrix(),\n                n.add(this.bib.get3DObject());\n        }\n        if (this.gameObject.primaryWeapon ||\n            this.gameObject.rules.hasRadialIndicator) {\n            a =\n                this.gameObject.psychicDetectorTrait?.radiusTiles ??\n                    this.gameObject.gapGeneratorTrait?.radiusTiles ??\n                    this.gameObject.primaryWeapon?.range;\n            if (a) {\n                a = this.rangeCircle = this.createRangeCircle(a) as any;\n                let e = (this.rangeCircleWrapper = new THREE.Object3D());\n                (e.matrixAutoUpdate = false),\n                    (e.position.x = s.x / 2),\n                    (e.position.z = s.y / 2),\n                    e.updateMatrix(),\n                    (e.visible = false),\n                    e.add(a as any),\n                    t.add(e);\n            }\n        }\n        (n.position.x = s.x),\n            (n.position.z = s.y),\n            n.updateMatrix(),\n            t.add(n);\n    }\n    computeSpriteAnchorOffset(e) {\n        var t = this.objectArt.getDrawOffset();\n        return { x: e.x + t.x, y: e.y + t.y };\n    }\n    createMainObject(e, t, i = false) {\n        let r = false;\n        this.objectRules.turret &&\n            \"CAOUTP\" !== this.objectRules.name &&\n            (r = true);\n        let s = T.ShpRenderable.factory(this.aggregatedImageData.file, this.palette, this.camera, t, this.objectArt.hasShadow, 0, !r, 0, i);\n        return (s.setSize(e),\n            s.setFrameOffset(this.aggregatedImageData.imageIndexes.get(e)),\n            s.setBatched(this.useSpriteBatching),\n            this.useSpriteBatching &&\n                s.setBatchPalettes(this.paletteRemaps),\n            s.setFlat(r),\n            s);\n    }\n    createRubbleObject(t) {\n        var i = this.mainShpFile;\n        if (i) {\n            let e = T.ShpRenderable.factory(this.aggregatedImageData.file, this.isoPalette, this.camera, t, this.objectArt.hasShadow);\n            if ((e.setSize(i),\n                !(this.shpFrameInfos.get(i).frameCount < 4)))\n                return (e.setFrameOffset(this.aggregatedImageData.imageIndexes.get(i)),\n                    e.setBatched(this.useSpriteBatching),\n                    this.useSpriteBatching &&\n                        e.setBatchPalettes([this.isoPalette]),\n                    e.setFlat(true),\n                    e.setFrame(3),\n                    e);\n            console.warn(`Building image ${this.objectArt.imageName} has no rubble frame (missing 4th frame)`);\n        }\n    }\n    createAnimObjects(n, o) {\n        let l = [];\n        return (this.animArtProps.getAll().forEach((e, t) => {\n            let i = [], r = 1;\n            for (var s of e) {\n                var a = this.animShpFiles.get(s);\n                if (a) {\n                    let e = this.createAnimObject(s, a, n, r++, o);\n                    e && (l.push(e.get3DObject()), i.push(e));\n                }\n            }\n            this.animObjects.set(t, i);\n        }),\n            l);\n    }\n    createFireObjects(n) {\n        let o = [], l = 0;\n        for (;;) {\n            let e = this.objectArt.art.getString(\"DamageFireOffset\" + l++);\n            if (!e)\n                break;\n            var c = this.rules.audioVisual.fireNames, h = c[u.getRandomInt(0, c.length - 1)];\n            let t;\n            try {\n                t = this.imageFinder.find(h, this.objectArt.useTheaterExtension);\n            }\n            catch (e) {\n                if (e instanceof MissingImageError) {\n                    console.warn(e.message);\n                    continue;\n                }\n                throw e;\n            }\n            c = e.split(/\\.|,/).filter((e) => \"\" !== e);\n            let i = parseInt(c[0], 10), r = parseInt(c[1], 10);\n            c = this.animPalette;\n            let s = new d.ShpBuilder(t, c, this.camera, g.Coords.ISO_WORLD_SCALE, true, 3);\n            s.setOffset({ x: n.x + i, y: n.y + r });\n            let a = new T.ShpRenderable(s);\n            a.setBatched(this.useSpriteBatching),\n                this.useSpriteBatching && a.setBatchPalettes([c]),\n                a.create3DObject(),\n                (a.get3DObject().visible = false);\n            (h = this.art.getAnimation(h)),\n                (h = new y.AnimProps(h.art, t));\n            this.animations.set(a, new f.Animation(h, this.gameSpeed)),\n                o.push(a);\n        }\n        return o;\n    }\n    createMuzzleFlashAnim(e, i) {\n        if (this.objectArt.muzzleFlash?.length) {\n            var r: any = u.getRandomInt(0, this.objectArt.muzzleFlash.length - 1), s = this.objectArt.muzzleFlash[r], r = this.gameObject.owner.country?.side === a.SideType.GDI\n                ? this.gameObject.primaryWeapon\n                : this.gameObject.secondaryWeapon;\n            if (r) {\n                r = r.rules.anim;\n                if (r.length) {\n                    r = r[u.getRandomInt(0, r.length - 1)];\n                    let t = { x: e.x + s.x, y: e.y + s.y };\n                    return i.createAnim(r, (e) => {\n                        e.extraOffset = t;\n                    }, true);\n                }\n            }\n        }\n    }\n    createAnimObject(e, t, i, r, s) {\n        var a = e.art;\n        let n = new y.AnimProps(a, t);\n        (e.type !== A.AnimationType.BUILDUP &&\n            e.type !== A.AnimationType.UNBUILD) ||\n            ((o = n.shadow ? t.numImages / 2 : t.numImages),\n                (n.rate = (o as any) / (60 * this.rules.general.buildupTime)));\n        var o = { x: i.x + e.offset.x, y: i.y + e.offset.y };\n        let l = T.ShpRenderable.factory(this.aggregatedImageData.file, this.palette, this.camera, o, n.shadow, 0, !e.flat, r, s && !e.flat);\n        return (l.setSize(t),\n            l.setFrameOffset(this.aggregatedImageData.imageIndexes.get(t)),\n            l.setBatched(this.useSpriteBatching),\n            this.useSpriteBatching &&\n                l.setBatchPalettes(this.paletteRemaps),\n            l.setFlat(e.flat),\n            (e.translucent || 0 < e.translucency) &&\n                l.setForceTransparent(true),\n            l.create3DObject(),\n            this.animations.set(l, new f.Animation(n, this.gameSpeed)),\n            l);\n    }\n    createBibObject(e, t) {\n        let i = T.ShpRenderable.factory(this.aggregatedImageData.file, this.palette, this.camera, t, this.objectArt.hasShadow);\n        return (i.setSize(e),\n            i.setFrameOffset(this.aggregatedImageData.imageIndexes.get(e)),\n            i.setBatched(this.useSpriteBatching),\n            this.useSpriteBatching &&\n                i.setBatchPalettes(this.paletteRemaps),\n            i.setFlat(true),\n            i);\n    }\n    createTurretObject(i, e) {\n        this.turretBuilders = [];\n        let r = new THREE.Object3D();\n        r.matrixAutoUpdate = false;\n        let s = new THREE.Object3D();\n        s.matrixAutoUpdate = false;\n        let a = this.objectRules.turretAnim;\n        var n = {\n            x: this.objectRules.turretAnimX,\n            y: this.objectRules.turretAnimY,\n        };\n        let o;\n        if (this.objectRules.turretAnimIsVoxel) {\n            var l = !this.objectArt.noHva;\n            let t = a.toLowerCase() + \".vxl\";\n            var c = this.voxels.get(t);\n            if (c) {\n                var h = l\n                    ? this.voxelAnims.get(t.replace(\".vxl\", \".hva\"))\n                    : void 0;\n                let e = this.vxlBuilderFactory.create(c, h, this.paletteRemaps, this.palette);\n                this.turretBuilders.push(e),\n                    (o = e.build()),\n                    o.children.forEach((e) => (e.castShadow = false));\n            }\n            else\n                console.warn(`Turret missing for building ${this.type}. Vxl file ${t} not found. `);\n            if (a.toLowerCase().includes(\"tur\")) {\n                let i = t.replace(\"tur\", \"barl\");\n                h = this.voxels.get(i);\n                if (h) {\n                    var u = l\n                        ? this.voxelAnims.get(i.replace(\".vxl\", \".hva\"))\n                        : void 0;\n                    let e = this.vxlBuilderFactory.create(h, u, this.paletteRemaps, this.palette);\n                    this.turretBuilders.push(e);\n                    let t = e.build();\n                    t.children.forEach((e) => (e.castShadow = false)),\n                        s.add(t);\n                }\n            }\n            u = g.Coords.screenDistanceToWorld(n.x, n.y);\n            (r.position.x = -e.x + u.x), (r.position.z = -e.y + u.y);\n        }\n        else {\n            let t;\n            try {\n                t = this.imageFinder.find(a, this.objectArt.useTheaterExtension);\n            }\n            catch (e) {\n                if (!(e instanceof MissingImageError))\n                    throw e;\n                console.warn(e.message);\n            }\n            if (t) {\n                let e = new d.ShpBuilder(t, this.palette, this.camera, g.Coords.ISO_WORLD_SCALE, true, 2);\n                e.setBatched(this.useSpriteBatching),\n                    this.useSpriteBatching &&\n                        e.setBatchPalettes(this.paletteRemaps),\n                    this.turretBuilders.push(e),\n                    e.setOffset({ x: i.x + n.x, y: i.y + n.y }),\n                    (o = e.build());\n            }\n        }\n        return (o && s.add(o),\n            r.add(s),\n            M.MathUtils.translateTowardsCamera(r, this.camera, -(this.objectRules.turretAnimZAdjust +\n                this.objectRules.turretAnimY /\n                    Math.cos(this.camera.rotation.y)) * g.Coords.ISO_WORLD_SCALE),\n            r.updateMatrix(),\n            { turret: r, turretRot: s });\n    }\n    createRangeCircle(e) {\n        var t = e * g.Coords.getWorldTileSize();\n        let i = this.gameObject.owner.color, r = s.OverlayUtils.createGroundCircle(t, i.asHex());\n        return (r.matrixAutoUpdate = false), r.updateMatrix(), r;\n    }\n    toggleRangeCircleVisibility(e) {\n        var t;\n        this.rangeCircleWrapper &&\n            ((this.rangeCircleWrapper.visible = e),\n                (t = this.gameObject.overpoweredTrait?.isOverpowered()) !==\n                    this.lastOverpowered &&\n                    ((this.lastOverpowered = t),\n                        this.rangeCircle &&\n                            (this.rangeCircleWrapper.remove(this.rangeCircle),\n                                this.rangeCircle.material.dispose(),\n                                this.rangeCircle.geometry.dispose()),\n                        (t =\n                            this.gameObject.overpoweredTrait?.getWeapon()?.range) &&\n                            ((this.rangeCircle = this.createRangeCircle(t)),\n                                this.rangeCircleWrapper.add(this.rangeCircle))));\n    }\n    setAnimationVisibility(e, i, t = -1) {\n        let r = this.animObjects.get(e);\n        if (void 0 === r)\n            throw new Error(`Missing animObjects for animType \"${A.AnimationType[e]}\"`);\n        if (-1 !== t) {\n            if (t >= r.length) {\n                t = Math.max(0, r.length - 1);\n            }\n            r = [r[t]];\n        }\n        for (var s of r) {\n            s.get3DObject().visible = i;\n            let e = this.animations.get(s).props.getArt(), t = e.getString(\"Report\");\n            (t = t || e.getString(\"StartSound\")),\n                t && this.handleSoundChange(t, s, i);\n        }\n    }\n    setActiveAnimationVisible() {\n        let e = this.animArtProps.getByType(A.AnimationType.ACTIVE);\n        this.objectRules.refinery && (e = [e[0]]),\n            e.forEach(({ showWhenUnpowered: e }, t) => {\n                try {\n                    this.setAnimationVisibility(A.AnimationType.ACTIVE, this.powered || e, t);\n                }\n                catch (e) {\n                    RangeError;\n                }\n            });\n    }\n    setPowered(r) {\n        if (((this.powered = r),\n            this.currentAnimType === A.AnimationType.IDLE &&\n                this.setActiveAnimationVisible(),\n            this.objectRules.superWeapon &&\n                this.hasAnimation(A.AnimationType.SUPER))) {\n            var [t, i] = this.getNormalizedAnimType(A.AnimationType.SUPER_CHARGE_LOOP), s = this.animObjects.get(t);\n            if (void 0 === s)\n                throw new Error(`Missing anim object for normalized anim type \"${A.AnimationType[t]}\"`);\n            i = s[i];\n            let e = this.animations.get(i);\n            r ? e.unpause() : e.pause();\n        }\n        else\n            this.animObjects\n                .get(A.AnimationType.ACTIVE)\n                .forEach((e, t) => {\n                let i = this.animations.get(e);\n                i &&\n                    (!r &&\n                        this.animArtProps.getByType(A.AnimationType.ACTIVE)[t]\n                            .pauseWhenUnpowered\n                        ? i.pause()\n                        : i.unpause());\n            });\n    }\n    hasAnimation(e) {\n        return (e === A.AnimationType.IDLE ||\n            (([e] = this.getNormalizedAnimType(e)),\n                this.animObjects.has(e) && !!this.animObjects.get(e).length));\n    }\n    setAnimation(e, t) {\n        if (!this.gameObject.healthTrait.health)\n            throw new Error(\"We can't switch building animation for a destroyed building\");\n        switch ((this.hasAnimation(e) || (e = A.AnimationType.IDLE),\n            (this.currentAnimType = e),\n            this.setAnimationVisibility(A.AnimationType.IDLE, false),\n            this.setAnimationVisibility(A.AnimationType.SPECIAL, false),\n            this.setAnimationVisibility(A.AnimationType.PRODUCTION, false),\n            this.setAnimationVisibility(A.AnimationType.SUPER, false),\n            this.setAnimationVisibility(A.AnimationType.BUILDUP, false),\n            this.setAnimationVisibility(A.AnimationType.UNBUILD, false),\n            this.setAnimationVisibility(A.AnimationType.FACTORY_DEPLOYING, false),\n            this.setAnimationVisibility(A.AnimationType.FACTORY_ROOF_DEPLOYING, false),\n            this.setActiveAnimationVisible(),\n            e !== A.AnimationType.BUILDUP &&\n                e !== A.AnimationType.UNBUILD\n                ? (this.mainObj &&\n                    (this.mainObj.get3DObject().visible = true),\n                    this.bib && (this.bib.get3DObject().visible = true),\n                    this.turret && (this.turret.visible = true))\n                : (this.mainObj &&\n                    (this.mainObj.get3DObject().visible = false),\n                    this.bib && (this.bib.get3DObject().visible = false),\n                    this.turret && (this.turret.visible = false)),\n            (e !== A.AnimationType.FACTORY_DEPLOYING &&\n                e !== A.AnimationType.FACTORY_ROOF_DEPLOYING) ||\n                (this.mainObj &&\n                    (this.mainObj.get3DObject().visible = false)),\n            e)) {\n            case A.AnimationType.PRODUCTION:\n                this.setAnimationVisibility(A.AnimationType.PRODUCTION, true),\n                    this.animObjects\n                        .get(A.AnimationType.PRODUCTION)\n                        .forEach((e) => {\n                        this.animations.get(e).start(t);\n                    });\n                break;\n            case A.AnimationType.BUILDUP:\n                this.setAnimationVisibility(A.AnimationType.ACTIVE, false),\n                    this.setAnimationVisibility(A.AnimationType.BUILDUP, true),\n                    this.animObjects\n                        .get(A.AnimationType.BUILDUP)\n                        .forEach((e) => {\n                        this.animations.get(e).start(t);\n                    });\n                break;\n            case A.AnimationType.UNBUILD:\n                this.setAnimationVisibility(A.AnimationType.ACTIVE, false),\n                    this.setAnimationVisibility(A.AnimationType.UNBUILD, true),\n                    this.animObjects\n                        .get(A.AnimationType.UNBUILD)\n                        .forEach((e) => {\n                        this.animations.get(e).start(t);\n                    });\n                break;\n            case A.AnimationType.FACTORY_DEPLOYING:\n                if (this.hasAnimation(A.AnimationType.FACTORY_DEPLOYING) &&\n                    this.objectRules.factory) {\n                    this.setAnimationVisibility(A.AnimationType.FACTORY_DEPLOYING, true),\n                        this.animObjects\n                            .get(A.AnimationType.FACTORY_DEPLOYING)\n                            .forEach((e) => {\n                            this.animations.get(e).start(t);\n                        });\n                    break;\n                }\n            // falls through\n            case A.AnimationType.FACTORY_ROOF_DEPLOYING:\n                if (this.hasAnimation(A.AnimationType.FACTORY_ROOF_DEPLOYING) &&\n                    this.objectRules.factory) {\n                    this.setAnimationVisibility(A.AnimationType.FACTORY_ROOF_DEPLOYING, true),\n                        this.animObjects\n                            .get(A.AnimationType.FACTORY_ROOF_DEPLOYING)\n                            .forEach((e) => {\n                            this.animations.get(e).start(t);\n                        });\n                    break;\n                }\n            // falls through\n            case A.AnimationType.SPECIAL_REPAIR_START:\n            case A.AnimationType.SPECIAL_REPAIR_LOOP:\n            case A.AnimationType.SPECIAL_REPAIR_END:\n            case A.AnimationType.SPECIAL_DOCKING:\n                if (this.hasAnimation(A.AnimationType.SPECIAL) &&\n                    ((e === A.AnimationType.SPECIAL_DOCKING &&\n                        this.objectRules.refinery) ||\n                        (e !== A.AnimationType.SPECIAL_DOCKING &&\n                            this.objectRules.unitRepair))) {\n                    var [i, r] = this.getNormalizedAnimType(e);\n                    this.setAnimationVisibility(i, true, r);\n                    {\n                        const list = this.animObjects.get(i);\n                        r = list[Math.min(r, list.length - 1)];\n                    }\n                    this.animations.get(r).start(t);\n                    break;\n                }\n            // falls through\n            case A.AnimationType.SPECIAL_SHOOT:\n                if (this.objectRules.isBaseDefense) {\n                    this.setAnimationVisibility(A.AnimationType.ACTIVE, false);\n                    var [r, s] = this.getNormalizedAnimType(e);\n                    this.setAnimationVisibility(r, true, s);\n                    {\n                        const list = this.animObjects.get(r);\n                        s = list[Math.min(s, list.length - 1)];\n                    }\n                    this.animations.get(s).start(t);\n                    break;\n                }\n            case A.AnimationType.SUPER_CHARGE_START:\n            case A.AnimationType.SUPER_CHARGE_LOOP:\n            case A.AnimationType.SUPER_CHARGE_END:\n                if (this.objectRules.superWeapon &&\n                    this.hasAnimation(A.AnimationType.SUPER)) {\n                    var [s, a] = this.getNormalizedAnimType(e);\n                    this.setAnimationVisibility(s, true, a);\n                    {\n                        const list = this.animObjects.get(s);\n                        a = list[Math.min(a, list.length - 1)];\n                    }\n                    this.animations.get(a).start(t);\n                    break;\n                }\n            // falls through\n            case A.AnimationType.IDLE:\n            default:\n                (this.currentAnimType = A.AnimationType.IDLE),\n                    this.objectRules.superWeapon &&\n                        this.hasAnimation(A.AnimationType.SUPER)\n                        ? (this.setAnimationVisibility(A.AnimationType.SUPER, true, 0),\n                            (a = this.animObjects.get(A.AnimationType.SUPER)[0]),\n                            this.animations.get(a).start(t))\n                        : (this.setAnimationVisibility(A.AnimationType.IDLE, true),\n                            this.animObjects\n                                .get(A.AnimationType.IDLE)\n                                .forEach((e) => {\n                                this.animations.get(e).start(t);\n                            }));\n        }\n    }\n    doWithAnimation(e, i) {\n        var [t, r] = this.getNormalizedAnimType(e);\n        let s = this.animObjects.get(t);\n        if (void 0 === s)\n            throw new Error(`Missing animObjects for anim type \"${A.AnimationType[t]}\"`);\n        t !== e && (s = [s[r]]),\n            s.forEach((e, t) => {\n                i(this.animations.get(e), e);\n            });\n    }\n    doWithCurrentAnimation(e) {\n        this.doWithAnimation(this.currentAnimType, e);\n    }\n    endCurrentAnimation() {\n        this.doWithCurrentAnimation((e) => e.endLoop());\n    }\n    handleSoundChange(e, t, i, r = 1) {\n        if (i) {\n            var s;\n            (this.animSounds.has(t) &&\n                this.animSounds.get(t).isPlaying()) ||\n                ((s = this.worldSound?.playEffect(e, this.gameObject, this.gameObject.owner, r)) &&\n                    this.animSounds.set(t, s));\n        }\n        else {\n            let e = this.animSounds.get(t);\n            e && e.isLoop && (e.stop(), this.animSounds.delete(t));\n        }\n    }\n    onCreate(t) {\n        (this.renderableManager = t),\n            this.plugins.forEach((e) => e.onCreate(t)),\n            this.objectRules.ambientSound &&\n                (this.ambientSound = this.worldSound?.playEffect(this.objectRules.ambientSound, this.gameObject, void 0, 0.25)),\n            this.pipOverlay?.onCreate(t);\n    }\n    onRemove(t) {\n        if (((this.renderableManager = void 0),\n            this.plugins.forEach((e) => e.onRemove(t)),\n            this.animSounds.forEach((e) => e.stop()),\n            this.ambientSound?.stop(),\n            this.turretRotateSound?.stop(),\n            this.poweredSound?.stop(),\n            this.gameObject.isDestroyed))\n            return this.gameObject.deathType === n.DeathType.Temporal ||\n                this.gameObject.deathType === n.DeathType.None\n                ? void 0\n                : void (this.objectRules.explosion?.length &&\n                    this.createExplosionAnims(t));\n    }\n    dispose() {\n        this.plugins.forEach((e) => e.dispose()),\n            this.pipOverlay?.dispose(),\n            this.placeholderObj?.dispose(),\n            this.mainObj?.dispose(),\n            this.rubbleObj?.dispose(),\n            this.bib?.dispose(),\n            this.fireObjects?.forEach((e) => e.dispose()),\n            this.turretBuilders?.forEach((e) => e.dispose()),\n            [...(this.animObjects?.values() ?? [])].forEach((e) => e.forEach((e) => e.dispose()));\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/Debris.ts",
    "content": "import { WithPosition } from \"@/engine/renderable/WithPosition\";\nimport { ShpRenderable } from \"@/engine/renderable/ShpRenderable\";\nimport { MapSpriteTranslation } from \"@/engine/renderable/MapSpriteTranslation\";\nimport { Animation } from \"@/engine/Animation\";\nimport { AnimProps } from \"@/engine/AnimProps\";\nimport { SimpleRunner } from \"@/engine/animation/SimpleRunner\";\nimport { Coords } from \"@/game/Coords\";\nimport { ShadowRenderable } from \"@/engine/renderable/ShadowRenderable\";\nimport * as THREE from \"three\";\ninterface GameObject {\n    rules: any;\n    art: any;\n    tile: {\n        z: number;\n    };\n    tileElevation: number;\n    velocity: THREE.Vector3;\n    position: {\n        worldPosition: THREE.Vector3;\n    };\n    rotationAxis: THREE.Vector3;\n    angularVelocity: number;\n    name: string;\n    isDestroyed: boolean;\n    explodeAnim?: any;\n}\ninterface Rules {\n    voxelAnimRules: Map<string, any>;\n}\ninterface ImageFinder {\n    findByObjectArt(objectArt: any): any;\n}\ninterface Voxels {\n    get(filename: string): any;\n}\ninterface Palette {\n}\ninterface Camera {\n}\ninterface Lighting {\n    compute(lightingType: any, tile: any, tileElevation: number): THREE.Vector3;\n    computeNoAmbient(lightingType: any, tile: any, tileElevation: number): number;\n}\ninterface GameSpeed {\n}\ninterface VxlBuilderFactory {\n    create(voxel: any, param2: any, palettes: Palette[], palette: Palette): VxlBuilder;\n}\ninterface VxlBuilder {\n    build(): THREE.Object3D;\n    setExtraLight(light: THREE.Vector3): void;\n    dispose(): void;\n}\ninterface Plugin {\n    updateLighting?(): void;\n    update(time: number): void;\n    onCreate(context: any): void;\n    onRemove(context: any): void;\n    dispose(): void;\n}\nexport class Debris {\n    private gameObject: GameObject;\n    private rules: Rules;\n    private imageFinder: ImageFinder;\n    private voxels: Voxels;\n    private palette: Palette;\n    private camera: Camera;\n    private lighting: Lighting;\n    private gameSpeed: GameSpeed;\n    private vxlBuilderFactory: VxlBuilderFactory;\n    private useSpriteBatching: boolean;\n    private plugins: Plugin[] = [];\n    private objectRules: any;\n    private objectArt: any;\n    private label: string;\n    private baseShpExtraLight!: THREE.Vector3;\n    private baseVxlExtraLight!: THREE.Vector3;\n    private vxlExtraLight!: THREE.Vector3;\n    private shpExtraLight!: THREE.Vector3;\n    private withPosition!: WithPosition;\n    private target?: THREE.Object3D;\n    private lastElevation?: number;\n    private shadowWrap?: THREE.Object3D;\n    private vxlBuilder?: VxlBuilder;\n    private vxlRotObj!: THREE.Object3D;\n    private shpAnimRunner!: SimpleRunner;\n    private shpRenderable?: ShpRenderable;\n    private shpShadowRenderable?: ShpRenderable;\n    constructor(gameObject: GameObject, rules: Rules, imageFinder: ImageFinder, voxels: Voxels, _unusedVoxelAnimCollection: unknown, palette: Palette, camera: Camera, lighting: Lighting, gameSpeed: GameSpeed, vxlBuilderFactory: VxlBuilderFactory, useSpriteBatching: boolean) {\n        this.gameObject = gameObject;\n        this.rules = rules;\n        this.imageFinder = imageFinder;\n        this.voxels = voxels;\n        this.palette = palette;\n        this.camera = camera;\n        this.lighting = lighting;\n        this.gameSpeed = gameSpeed;\n        this.vxlBuilderFactory = vxlBuilderFactory;\n        this.useSpriteBatching = useSpriteBatching;\n        this.objectRules = gameObject.rules;\n        this.objectArt = gameObject.art;\n        this.label = \"debris_\" + this.objectRules.name;\n        if (typeof this.lighting?.compute !== \"function\" ||\n            typeof this.lighting?.computeNoAmbient !== \"function\") {\n            throw new Error(`[Debris] invalid lighting dependency for \"${this.objectRules.name}\". ` +\n                `Expected Lighting with compute()/computeNoAmbient(), got \"${this.lighting?.constructor?.name ?? typeof this.lighting}\"`);\n        }\n        this.init();\n    }\n    private init(): void {\n        this.baseShpExtraLight = this.lighting\n            .compute(this.objectArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation)\n            .addScalar(-1);\n        this.baseVxlExtraLight = new THREE.Vector3().setScalar(Math.PI * 1.5 + this.lighting.computeNoAmbient(this.objectArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation));\n        this.vxlExtraLight = new THREE.Vector3().copy(this.baseVxlExtraLight);\n        this.shpExtraLight = new THREE.Vector3().copy(this.baseShpExtraLight);\n        this.withPosition = new WithPosition();\n    }\n    public registerPlugin(plugin: Plugin): void {\n        this.plugins.push(plugin);\n    }\n    public updateLighting(): void {\n        this.plugins.forEach((plugin) => plugin.updateLighting?.());\n        this.baseShpExtraLight = this.lighting\n            .compute(this.objectArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation)\n            .addScalar(-1);\n        this.baseVxlExtraLight = new THREE.Vector3().setScalar(Math.PI * 1.5 + this.lighting.computeNoAmbient(this.objectArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation));\n        this.vxlExtraLight.copy(this.baseVxlExtraLight);\n        this.shpExtraLight.copy(this.baseShpExtraLight);\n    }\n    public get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    public create3DObject(): void {\n        let obj = this.get3DObject();\n        if (!obj) {\n            obj = new THREE.Object3D();\n            obj.name = this.label;\n            this.target = obj;\n            obj.matrixAutoUpdate = false;\n            this.withPosition.matrixUpdate = true;\n            this.withPosition.applyTo(this);\n            this.createObjects(obj);\n            this.vxlBuilder?.setExtraLight(this.vxlExtraLight);\n            this.shpRenderable?.setExtraLight(this.shpExtraLight);\n        }\n    }\n    public setPosition(position: THREE.Vector3): void {\n        this.withPosition.setPosition(position.x, position.y, position.z);\n    }\n    public getPosition(): THREE.Vector3 {\n        return this.withPosition.getPosition();\n    }\n    public update(time: number, deltaTime: number = 0): void {\n        this.plugins.forEach((plugin) => plugin.update(time));\n        const elevation = this.gameObject.tile.z + this.gameObject.tileElevation;\n        if (this.lastElevation === undefined || this.lastElevation !== elevation) {\n            this.lastElevation = elevation;\n            this.baseVxlExtraLight = new THREE.Vector3().setScalar(Math.PI * 1.5 + this.lighting.computeNoAmbient(this.objectArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation));\n            this.baseShpExtraLight = this.lighting\n                .compute(this.objectArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation)\n                .addScalar(-1);\n            this.vxlExtraLight.copy(this.baseVxlExtraLight);\n            this.shpExtraLight.copy(this.baseShpExtraLight);\n            if (this.shadowWrap) {\n                this.shadowWrap.position.y = -Coords.tileHeightToWorld(this.gameObject.tileElevation);\n                this.shadowWrap.updateMatrix();\n            }\n        }\n        if (deltaTime > 0) {\n            const velocity = this.gameObject.velocity.clone();\n            const displacement = velocity.multiplyScalar(deltaTime);\n            const newPosition = displacement.add(this.gameObject.position.worldPosition);\n            this.setPosition(newPosition);\n        }\n        if (this.vxlBuilder) {\n            const { rotationAxis, angularVelocity } = this.gameObject;\n            this.vxlRotObj.rotateOnAxis(rotationAxis, THREE.MathUtils.degToRad(angularVelocity));\n            this.vxlRotObj.updateMatrix();\n        }\n        else {\n            this.shpAnimRunner.tick(time);\n            this.shpRenderable?.setFrame(this.shpAnimRunner.animation.getCurrentFrame());\n            this.shpShadowRenderable?.setFrame(this.shpAnimRunner.animation.getCurrentFrame());\n        }\n    }\n    private createObjects(parent: THREE.Object3D): void {\n        const rotationObj = this.vxlRotObj = new THREE.Object3D();\n        rotationObj.matrixAutoUpdate = false;\n        rotationObj.rotation.order = \"YXZ\";\n        const mainObject = this.createMainObject();\n        rotationObj.add(mainObject);\n        parent.add(rotationObj);\n    }\n    private computeSpriteAnchorOffset(offset: {\n        x: number;\n        y: number;\n    }): {\n        x: number;\n        y: number;\n    } {\n        const drawOffset = this.objectArt.getDrawOffset();\n        return { x: offset.x + drawOffset.x, y: offset.y + drawOffset.y };\n    }\n    private createMainObject(): THREE.Object3D {\n        const mainObj = new THREE.Object3D();\n        mainObj.matrixAutoUpdate = false;\n        if (this.rules.voxelAnimRules.has(this.gameObject.name)) {\n            const vxlFileName = this.getVxlFileName(this.objectRules, this.objectArt);\n            const voxel = this.voxels.get(vxlFileName);\n            if (!voxel) {\n                throw new Error(`VXL missing for anim ${this.objectRules.name}. Vxl file ${vxlFileName} not found. `);\n            }\n            const builder = this.vxlBuilderFactory.create(voxel, undefined, [this.palette], this.palette);\n            this.vxlBuilder = builder;\n            const vxlObject = builder.build();\n            mainObj.add(vxlObject);\n        }\n        else {\n            const spriteTranslation = new MapSpriteTranslation(1, 1);\n            const { spriteOffset, anchorPointWorld } = spriteTranslation.compute();\n            const anchorOffset = this.computeSpriteAnchorOffset(spriteOffset);\n            const image = this.imageFinder.findByObjectArt(this.objectArt);\n            const shpRenderable = this.shpRenderable = ShpRenderable.factory(image, this.palette, this.camera, anchorOffset, false);\n            shpRenderable.setBatched(this.useSpriteBatching);\n            if (this.useSpriteBatching) {\n                shpRenderable.setBatchPalettes([this.palette]);\n            }\n            shpRenderable.create3DObject();\n            mainObj.add(shpRenderable.get3DObject());\n            const shadowPalette = ShadowRenderable.getOrCreateShadowPalette();\n            const shpShadowRenderable = this.shpShadowRenderable = ShpRenderable.factory(image, shadowPalette, this.camera, anchorOffset, false);\n            shpShadowRenderable.setBatched(this.useSpriteBatching);\n            if (this.useSpriteBatching) {\n                shpShadowRenderable.setBatchPalettes([shadowPalette]);\n            }\n            shpShadowRenderable.setOpacity(0.5);\n            shpShadowRenderable.create3DObject();\n            const shadowWrap = this.shadowWrap = new THREE.Object3D();\n            shadowWrap.matrixAutoUpdate = false;\n            shadowWrap.add(shpShadowRenderable.get3DObject());\n            mainObj.add(shadowWrap);\n            mainObj.position.x = anchorPointWorld.x;\n            mainObj.position.z = anchorPointWorld.y;\n            mainObj.updateMatrix();\n            shpRenderable.setFlat(this.objectArt.flat);\n            const animProps = new AnimProps(this.objectArt.art, image);\n            const animation = new Animation(animProps, this.gameSpeed as any);\n            this.shpAnimRunner = new SimpleRunner();\n            this.shpAnimRunner.animation = animation;\n        }\n        return mainObj;\n    }\n    private getVxlFileName(objectRules: any, objectArt: any): string {\n        let imageName = objectArt.imageName;\n        if (objectRules.shareSource) {\n            imageName = objectRules.shareSource;\n            if (objectRules.shareTurretData) {\n                imageName += \"tur\";\n            }\n            else if (objectRules.shareBarrelData) {\n                imageName += \"barl\";\n            }\n        }\n        return imageName.toLowerCase() + \".vxl\";\n    }\n    public onCreate(context: any): void {\n        this.plugins.forEach((plugin) => plugin.onCreate(context));\n    }\n    public onRemove(context: any): void {\n        this.plugins.forEach((plugin) => plugin.onRemove(context));\n        if (this.gameObject.isDestroyed && this.get3DObject()) {\n            const explodeAnim = this.gameObject.explodeAnim;\n            if (explodeAnim) {\n                context.createTransientAnim(explodeAnim, (anim: any) => anim.setPosition(this.withPosition.getPosition()));\n            }\n        }\n    }\n    public dispose(): void {\n        this.plugins.forEach((plugin) => plugin.dispose());\n        this.shpRenderable?.dispose();\n        this.shpShadowRenderable?.dispose();\n        this.vxlBuilder?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/HighlightAnimRunner.ts",
    "content": "import { SimpleRunner } from '@/engine/animation/SimpleRunner';\nimport { Animation } from '@/engine/Animation';\nimport { AnimProps } from '@/engine/AnimProps';\nimport { IniSection } from '@/data/IniSection';\nimport { ShpFile } from '@/data/ShpFile';\nimport { BoxedVar } from '@/util/BoxedVar';\nexport class HighlightAnimRunner extends SimpleRunner {\n    private maxAmount: number;\n    declare animation: Animation;\n    constructor(gameSpeed: number | BoxedVar<number>, maxAmount: number = 0.5, loopEnd: number = 2, rate: number = 5) {\n        super();\n        this.maxAmount = maxAmount;\n        const props = new AnimProps(new IniSection(\"dummy\"), new ShpFile());\n        props.rate = rate;\n        props.loopEnd = loopEnd - 1;\n        props.loopCount = 2;\n        const speed = typeof gameSpeed === 'number' ? new BoxedVar(gameSpeed) : gameSpeed;\n        this.animation = new Animation(props, speed);\n        this.animation.stop();\n    }\n    animate(loopCount: number): void {\n        this.animation.props.loopCount = loopCount;\n        this.animation.reset();\n    }\n    getValue(): number {\n        return (1 - this.getCurrentFrame()) * this.maxAmount;\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/Infantry.ts",
    "content": "import { WithPosition } from \"@/engine/renderable/WithPosition\";\nimport * as ImageFinder from \"@/engine/ImageFinder\";\nimport { MissingImageError } from \"@/engine/ImageFinder\";\nimport { DebugUtils } from \"@/engine/gfx/DebugUtils\";\nimport { ShpRenderable } from \"@/engine/renderable/ShpRenderable\";\nimport { Coords } from \"@/game/Coords\";\nimport { SimpleRunner } from \"@/engine/animation/SimpleRunner\";\nimport { Animation, AnimationState } from \"@/engine/Animation\";\nimport { AnimProps } from \"@/engine/AnimProps\";\nimport { IniSection } from \"@/data/IniSection\";\nimport * as sequenceMap from \"@/game/gameobject/infantry/sequenceMap\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { SequenceType } from \"@/game/art/SequenceType\";\nimport * as math from \"@/util/math\";\nimport * as THREE from \"three\";\nimport { InfDeathType } from \"@/game/gameobject/infantry/InfDeathType\";\nimport { VeteranLevel } from \"@/game/gameobject/unit/VeteranLevel\";\nimport { HighlightAnimRunner } from \"@/engine/renderable/entity/HighlightAnimRunner\";\nimport { DeathType } from \"@/game/gameobject/common/DeathType\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { BlobShadow } from \"@/engine/renderable/entity/unit/BlobShadow\";\nimport { BoxIntersectObject3D } from \"@/engine/renderable/entity/BoxIntersectObject3D\";\nimport { ExtraLightHelper } from \"@/engine/renderable/entity/unit/ExtraLightHelper\";\nimport { DebugRenderable } from \"@/engine/renderable/DebugRenderable\";\nimport { MathUtils } from \"@/engine/gfx/MathUtils\";\nexport class Infantry {\n    private gameObject: any;\n    private rules: any;\n    private art: any;\n    private imageFinder: any;\n    private palette: any;\n    private camera: any;\n    private lighting: any;\n    private debugFrame: any;\n    private gameSpeed: any;\n    private selectionModel: any;\n    private useSpriteBatching: boolean;\n    private useMeshInstancing: boolean;\n    private pipOverlay: any;\n    private worldSound: any;\n    private crashingSequencePlaying: boolean = false;\n    private deathAnimSequencePlaying: boolean = false;\n    private idleActionDue: boolean = false;\n    private disguiseChanged: boolean = false;\n    private highlightAnimRunner: HighlightAnimRunner;\n    private plugins: any[] = [];\n    private objectArt: any;\n    private label: string;\n    private paletteRemaps: any[];\n    private withPosition: WithPosition;\n    private baseExtraLight: THREE.Vector3;\n    private extraLight: THREE.Vector3;\n    private target: THREE.Object3D;\n    private posWrap: THREE.Object3D;\n    private shpRenderable: ShpRenderable;\n    private placeholder: DebugRenderable;\n    private blobShadow: BlobShadow;\n    private animRunner: SimpleRunner;\n    private currentSequenceParams: any;\n    private sequenceQueue: any[] = [];\n    private renderableManager: any;\n    private ambientSound: any;\n    private deathPromiseResolve: (() => void) | undefined;\n    private deathAnimRenderable: any;\n    private deadBodyAnimRenderable: any;\n    private paradropAnim: any;\n    private disguise: any;\n    private lastVeteranLevel: VeteranLevel;\n    private lastElevation: number;\n    private lastOwnerColor: any;\n    private lastWarpedOut: boolean;\n    private lastCloaked: boolean;\n    private lastZone: ZoneType;\n    private lastDirection: number;\n    private computedDirection: number;\n    private lastMoving: boolean;\n    private lastFiring: boolean;\n    private lastPanicked: boolean;\n    private lastStance: StanceType;\n    constructor(gameObject: any, rules: any, art: any, imageFinder: any, theater: any, palette: any, camera: any, lighting: any, debugFrame: any, gameSpeed: any, selectionModel: any, useSpriteBatching: boolean, useMeshInstancing: boolean, pipOverlay: any, worldSound: any) {\n        this.gameObject = gameObject;\n        this.rules = rules;\n        this.art = art;\n        this.imageFinder = imageFinder;\n        this.palette = palette;\n        this.camera = camera;\n        this.lighting = lighting;\n        this.debugFrame = debugFrame;\n        this.gameSpeed = gameSpeed;\n        this.selectionModel = selectionModel;\n        this.useSpriteBatching = useSpriteBatching;\n        this.useMeshInstancing = useMeshInstancing;\n        this.pipOverlay = pipOverlay;\n        this.worldSound = worldSound;\n        this.highlightAnimRunner = new HighlightAnimRunner(this.gameSpeed);\n        this.objectArt = gameObject.art;\n        this.label = \"infantry_\" + gameObject.rules.name;\n        this.paletteRemaps = [...this.rules.colors.values()].map((color: any) => this.palette.clone().remap(color));\n        this.palette = this.palette.remap(this.gameObject.owner.color);\n        this.withPosition = new WithPosition();\n        this.updateBaseLight();\n        this.extraLight = new THREE.Vector3().copy(this.baseExtraLight);\n    }\n    updateBaseLight(): void {\n        this.baseExtraLight = this.lighting\n            .compute(this.objectArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation)\n            .addScalar(-1 + this.rules.audioVisual.extraInfantryLight);\n    }\n    registerPlugin(plugin: any): void {\n        this.plugins.push(plugin);\n    }\n    updateLighting(): void {\n        this.plugins.forEach((plugin) => plugin.updateLighting?.());\n        this.updateBaseLight();\n        this.extraLight.copy(this.baseExtraLight);\n    }\n    get3DObject(): THREE.Object3D {\n        return this.target;\n    }\n    getIntersectTarget(): THREE.Object3D {\n        return this.target;\n    }\n    getUiName(): string {\n        const override = this.plugins.reduce((name, plugin) => plugin.getUiNameOverride?.() ?? name, undefined);\n        return override !== undefined ? override : this.gameObject.getUiName();\n    }\n    create3DObject(): void {\n        let obj = this.get3DObject();\n        if (!obj) {\n            obj = new BoxIntersectObject3D(new THREE.Vector3(0.5, 2 / 3, 0.5).multiplyScalar(Coords.LEPTONS_PER_TILE));\n            obj.name = this.label;\n            obj.userData.id = this.gameObject.id;\n            this.target = obj;\n            obj.matrixAutoUpdate = false;\n            this.withPosition.matrixUpdate = true;\n            this.withPosition.applyTo(this);\n            this.createObjects(obj);\n            this.shpRenderable?.setExtraLight(this.extraLight);\n            if (this.pipOverlay) {\n                this.pipOverlay.create3DObject();\n                this.posWrap.add(this.pipOverlay.get3DObject());\n            }\n        }\n    }\n    setPosition(position: {\n        x: number;\n        y: number;\n        z: number;\n    }): void {\n        this.withPosition.setPosition(position.x, position.y, position.z);\n    }\n    getPosition(): THREE.Vector3 {\n        return this.withPosition.getPosition();\n    }\n    highlight(): void {\n        if (!this.plugins.some((plugin) => plugin.shouldDisableHighlight?.())) {\n            if (this.highlightAnimRunner.animation.getState() !== AnimationState.RUNNING) {\n                this.highlightAnimRunner.animate(2);\n            }\n        }\n    }\n    update(deltaTime: number): void {\n        this.plugins.forEach((plugin) => plugin.update(deltaTime));\n        const { zone, stance, isCrashing, isMoving, isFiring, isPanicked, owner, veteranLevel, } = this.gameObject;\n        this.pipOverlay?.update(deltaTime);\n        this.blobShadow?.update(deltaTime, undefined as any);\n        if (veteranLevel !== this.lastVeteranLevel) {\n            if (veteranLevel === VeteranLevel.Elite && this.lastVeteranLevel !== undefined) {\n                this.highlightAnimRunner.animate(30);\n            }\n            this.lastVeteranLevel = veteranLevel;\n        }\n        const elevation = this.gameObject.tile.z + this.gameObject.tileElevation;\n        if (this.lastElevation === undefined || this.lastElevation !== elevation) {\n            this.lastElevation = elevation;\n            this.updateBaseLight();\n            this.extraLight.copy(this.baseExtraLight);\n        }\n        if (this.highlightAnimRunner.shouldUpdate()) {\n            this.highlightAnimRunner.tick(deltaTime);\n            ExtraLightHelper.multiplyShp(this.extraLight as any, this.baseExtraLight as any, this.highlightAnimRunner.getValue());\n        }\n        const currentOwner = this.disguise?.owner ?? owner;\n        if (this.lastOwnerColor !== currentOwner.color) {\n            this.palette.remap(currentOwner.color);\n            (this.shpRenderable ?? this.placeholder)?.setPalette(this.palette);\n            this.lastOwnerColor = currentOwner.color;\n        }\n        const warpedOut = this.gameObject.warpedOutTrait.isActive();\n        const warpedOutChanged = warpedOut !== this.lastWarpedOut;\n        this.lastWarpedOut = warpedOut;\n        const cloaked = this.gameObject.cloakableTrait?.isCloaked();\n        const cloakedChanged = cloaked !== this.lastCloaked;\n        this.lastCloaked = cloaked;\n        if ((warpedOutChanged || cloakedChanged)) {\n            (this.shpRenderable ?? this.placeholder)?.setOpacity(warpedOut || cloaked ? 0.5 : 1);\n        }\n        if (!isCrashing && (this.lastZone === undefined || this.lastZone !== zone)) {\n            if (zone === ZoneType.Water) {\n                if (this.gameObject.rules.enterWaterSound) {\n                    this.worldSound?.playEffect(this.gameObject.rules.enterWaterSound, this.gameObject, owner);\n                }\n            }\n            else if (this.lastZone === ZoneType.Water) {\n                if (this.gameObject.rules.leaveWaterSound) {\n                    this.worldSound?.playEffect(this.gameObject.rules.leaveWaterSound, this.gameObject, owner);\n                }\n            }\n            if (this.blobShadow) {\n                this.shpRenderable?.setShadowVisible(!this.blobShadow.get3DObject().visible);\n            }\n        }\n        if (this.gameObject.isDestroyed && this.deathPromiseResolve) {\n            if (this.deadBodyAnimRenderable) {\n                (this.shpRenderable ?? this.placeholder).get3DObject().visible = false;\n                this.deadBodyAnimRenderable.update(deltaTime);\n                if (this.deadBodyAnimRenderable.isAnimFinished()) {\n                    this.deathPromiseResolve();\n                    return;\n                }\n            }\n            else {\n                if (!this.deathAnimRenderable) {\n                    if (this.deathAnimSequencePlaying) {\n                        if (this.animRunner && this.animRunner.animation.getState() !== AnimationState.STOPPED) {\n                            this.animRunner.tick(deltaTime);\n                            this.updateShapeFrame(this.computeFacingNumber(this.gameObject.direction));\n                            return;\n                        }\n                        else {\n                            if ([InfDeathType.Gunfire, InfDeathType.Explode].includes(this.gameObject.infDeathType) &&\n                                this.gameObject.rules.isHuman &&\n                                this.gameObject.zone === ZoneType.Ground) {\n                                this.prepareDeadBodyAnim();\n                            }\n                            else {\n                                this.deathPromiseResolve();\n                            }\n                            return;\n                        }\n                    }\n                    const sequence = this.sequenceQueue.shift();\n                    if (sequence) {\n                        this.deathAnimSequencePlaying = true;\n                        this.setAnimParams(sequence, deltaTime, false);\n                        return;\n                    }\n                    throw new Error(\"We should have a death sequence scheduled right now\");\n                }\n                (this.shpRenderable ?? this.placeholder).get3DObject().visible = false;\n                this.deathAnimRenderable.update(deltaTime);\n                if (this.deathAnimRenderable.isAnimFinished()) {\n                    if ([InfDeathType.Gunfire, InfDeathType.Explode].includes(this.gameObject.infDeathType) &&\n                        this.gameObject.rules.isHuman) {\n                        this.prepareDeadBodyAnim();\n                    }\n                    else {\n                        this.deathPromiseResolve();\n                    }\n                    return;\n                }\n            }\n        }\n        else {\n            if (this.gameObject.warpedOutTrait.isActive())\n                return;\n            if (isCrashing && !this.crashingSequencePlaying) {\n                this.crashingSequencePlaying = true;\n                const crashingSequences = sequenceMap.getCrashingSequences(this.gameObject);\n                if (crashingSequences) {\n                    this.sequenceQueue = crashingSequences;\n                }\n            }\n        }\n        if (this.lastDirection === undefined || this.lastDirection !== this.gameObject.direction) {\n            this.lastDirection = this.gameObject.direction;\n            this.computedDirection = this.gameObject.direction;\n        }\n        const wasIdleActionDue = this.idleActionDue;\n        this.idleActionDue = this.gameObject.idleActionTrait.actionDueThisTick();\n        let shouldTriggerIdleAction = this.idleActionDue && !wasIdleActionDue;\n        if (this.lastMoving === undefined || this.lastMoving !== isMoving ||\n            this.lastFiring === undefined || this.lastFiring !== isFiring ||\n            this.lastZone === undefined || this.lastZone !== zone ||\n            this.lastPanicked === undefined || this.lastPanicked !== isPanicked ||\n            this.disguiseChanged) {\n            const disguiseChanged = this.disguiseChanged;\n            const firingChanged = this.lastFiring !== isFiring;\n            this.lastMoving = isMoving;\n            this.lastFiring = isFiring;\n            this.lastZone = zone;\n            this.lastPanicked = isPanicked;\n            this.computedDirection = this.gameObject.direction;\n            this.disguiseChanged = false;\n            if (!isCrashing) {\n                this.sequenceQueue = [];\n                if (!firingChanged || isFiring || disguiseChanged) {\n                    let sequence = this.findSequenceBy(zone, stance, isMoving, isFiring, isPanicked);\n                    if (sequence !== undefined) {\n                        if (this.disguise && [SequenceType.FireUp, SequenceType.FireProne].includes(sequence)) {\n                            sequence = SequenceType.Ready;\n                        }\n                        this.setAnimParams(sequence, deltaTime, !isFiring);\n                    }\n                }\n            }\n        }\n        if (this.lastStance === undefined || this.lastStance !== stance) {\n            this.sequenceQueue = [];\n            shouldTriggerIdleAction = false;\n            const transitionSequence = sequenceMap.getStanceTransitionSequenceBy(this.lastStance, stance);\n            this.lastStance = stance;\n            if (transitionSequence && this.objectArt.sequences.has(transitionSequence)) {\n                this.sequenceQueue.push(transitionSequence);\n            }\n            const sequence = this.findSequenceBy(zone, stance, isMoving, isFiring, isPanicked);\n            if (sequence !== undefined) {\n                this.sequenceQueue.push(sequence);\n            }\n            if (this.currentSequenceParams?.onlyFacing !== undefined) {\n                this.computedDirection = this.directionFromFacingNo(this.currentSequenceParams.onlyFacing);\n            }\n            const nextSequence = this.sequenceQueue.shift();\n            this.setAnimParams(nextSequence, deltaTime, !transitionSequence);\n            if (nextSequence === SequenceType.Paradrop) {\n                const parachuteArt = this.rules.audioVisual.parachute;\n                this.paradropAnim = this.renderableManager.createAnim(parachuteArt, undefined, true);\n                this.paradropAnim.remapColor(owner.color);\n                this.paradropAnim.create3DObject();\n                this.paradropAnim.get3DObject().position.y = Coords.tileHeightToWorld(1);\n                this.paradropAnim.get3DObject().updateMatrix();\n                this.posWrap.add(this.paradropAnim.get3DObject());\n            }\n            else if (this.paradropAnim) {\n                this.paradropAnim.endAnimationLoop();\n                if (this.blobShadow) {\n                    this.posWrap.remove(this.blobShadow.get3DObject());\n                    this.blobShadow.dispose();\n                    this.blobShadow = undefined;\n                    this.shpRenderable?.setShadowVisible(true);\n                }\n            }\n        }\n        if (this.paradropAnim) {\n            this.paradropAnim.update(deltaTime);\n            if (this.paradropAnim.isAnimFinished()) {\n                this.posWrap.remove(this.paradropAnim.get3DObject());\n                this.paradropAnim = undefined;\n            }\n        }\n        if (!this.sequenceQueue.length && !isMoving && !isFiring &&\n            (stance === StanceType.None || stance === StanceType.Guard) &&\n            zone !== ZoneType.Air && shouldTriggerIdleAction) {\n            if (Math.random() >= 0.5) {\n                const idleSequence = this.findIdleSequence(zone, stance, this.objectArt);\n                if (idleSequence) {\n                    this.setAnimParams(idleSequence, deltaTime, false);\n                }\n            }\n            else {\n                this.computedDirection = Math.floor(360 * Math.random());\n            }\n        }\n        if (this.animRunner) {\n            if (this.animRunner.animation.getState() === AnimationState.STOPPED && this.currentSequenceParams) {\n                if ([SequenceType.Idle1, SequenceType.Idle2].includes(this.currentSequenceParams.type) &&\n                    this.currentSequenceParams.onlyFacing !== undefined) {\n                    this.computedDirection = this.directionFromFacingNo(this.currentSequenceParams.onlyFacing);\n                }\n                let nextSequence;\n                if (this.sequenceQueue.length) {\n                    nextSequence = this.sequenceQueue.shift();\n                }\n                else {\n                    nextSequence = this.findSequenceBy(zone, stance, isMoving, isFiring, isPanicked);\n                }\n                if (nextSequence !== undefined) {\n                    this.setAnimParams(nextSequence, deltaTime, !isFiring);\n                }\n            }\n            this.animRunner.tick(deltaTime);\n            const facingNumber = this.computeFacingNumber(this.computedDirection);\n            this.updateShapeFrame(facingNumber);\n        }\n    }\n    findIdleSequence(zone: ZoneType, stance: StanceType, art: any): SequenceType | undefined {\n        let sequences = sequenceMap.getIdleSequenceBy(zone, stance);\n        if (sequences?.length) {\n            sequences = sequences.filter((seq) => art.sequences.has(seq));\n            if (!sequences.length && zone !== ZoneType.Ground) {\n                sequences = sequenceMap.getIdleSequenceBy(ZoneType.Ground, stance)?.filter((seq) => art.sequences.has(seq));\n            }\n        }\n        if (sequences) {\n            return sequences[math.getRandomInt(0, sequences.length - 1)];\n        }\n    }\n    prepareDeadBodyAnim(): void {\n        const deadBodies = this.rules.audioVisual.deadBodies;\n        const deadBodyArt = deadBodies[math.getRandomInt(0, deadBodies.length - 1)];\n        this.deadBodyAnimRenderable = this.renderableManager.createAnim(deadBodyArt, undefined, true);\n        this.deadBodyAnimRenderable.create3DObject();\n        this.posWrap.add(this.deadBodyAnimRenderable.get3DObject());\n    }\n    findSequenceBy(zone: ZoneType, stance: StanceType, isMoving: boolean, isFiring: boolean, isPanicked: boolean): SequenceType | undefined {\n        const sequence = sequenceMap.findSequence(zone, stance, isMoving, isFiring, isPanicked, [...this.objectArt.sequences.keys()]);\n        if (sequence !== undefined)\n            return sequence;\n        console.warn(`Couldn't find a sequence for infantry \"${this.gameObject.name}\" ` +\n            `(moving=${isMoving}, firing=${isFiring})`);\n    }\n    setAnimParams(sequenceType: SequenceType, time: number, loop: boolean = true): void {\n        if (this.animRunner) {\n            const sequence = this.objectArt.sequences.get(sequenceType);\n            if (sequence) {\n                this.currentSequenceParams = sequence;\n                const props = this.animRunner.animation.props;\n                props.loopCount = loop ? -1 : 1;\n                props.loopEnd = sequence.frameCount - 1;\n                if ([SequenceType.Deploy, SequenceType.Undeploy, SequenceType.Paradrop].includes(sequenceType)) {\n                    if (sequenceType === SequenceType.Paradrop) {\n                        props.rate = 2 * AnimProps.defaultRate;\n                    }\n                    else {\n                        props.rate = AnimProps.defaultRate;\n                    }\n                }\n                else {\n                    props.rate = AnimProps.defaultRate / 2;\n                }\n                if ([SequenceType.Walk].includes(sequenceType)) {\n                    props.rate /= 1.33;\n                }\n                this.animRunner.animation.start(time);\n            }\n            else {\n                console.warn(`Infantry \"${this.gameObject.name}\" is missing sequence \"${SequenceType[sequenceType]}\"`);\n            }\n        }\n    }\n    updateShapeFrame(facingNumber: number): void {\n        if (this.currentSequenceParams && this.shpRenderable && this.animRunner) {\n            const { startFrame, facingMult } = this.currentSequenceParams;\n            const frameIndex = startFrame + facingMult * facingNumber + this.animRunner.animation.getCurrentFrame();\n            if (frameIndex < this.shpRenderable.frameCount) {\n                this.shpRenderable.setFrame(frameIndex);\n            }\n        }\n    }\n    computeFacingNumber(direction: number): number {\n        return Math.round((((direction - 45 + 360) % 360) / 360) * 8) % 8;\n    }\n    directionFromFacingNo(facingNumber: number): number {\n        return 45 + (360 * facingNumber) / 8;\n    }\n    createObjects(parent: THREE.Object3D): void {\n        if (this.debugFrame.value) {\n            const wireframe = DebugUtils.createWireframe({ width: 0.5, height: 0.5 }, 1);\n            wireframe.translateX(-Coords.getWorldTileSize() / 4);\n            wireframe.translateZ(-Coords.getWorldTileSize() / 4);\n            parent.add(wireframe);\n        }\n        const posWrap = this.posWrap = new THREE.Object3D();\n        posWrap.matrixAutoUpdate = false;\n        parent.add(posWrap);\n        const mainObject = this.createMainObject(this.objectArt);\n        posWrap.add(mainObject);\n        if ((this.gameObject.rules.movementZone !== MovementZone.Fly || this.objectArt.isVoxel) &&\n            this.gameObject.stance !== StanceType.Paradrop) {\n            this.blobShadow = new BlobShadow(this.gameObject, 3, this.useMeshInstancing);\n            this.blobShadow.create3DObject();\n            this.posWrap.add(this.blobShadow.get3DObject());\n        }\n    }\n    createMainObject(art: any): THREE.Object3D {\n        let image;\n        try {\n            image = this.imageFinder.findByObjectArt(art);\n        }\n        catch (error) {\n            if (!(error instanceof MissingImageError))\n                throw error;\n            console.warn(`<${this.gameObject.name}>: ` + error.message);\n        }\n        if (!image) {\n            this.placeholder = new DebugRenderable({ width: 0.25, height: 0.25 }, this.objectArt.height, this.palette, { centerFoundation: true });\n            this.placeholder.setBatched(this.useSpriteBatching);\n            if (this.useSpriteBatching) {\n                this.placeholder.setBatchPalettes(this.paletteRemaps);\n            }\n            this.placeholder.create3DObject();\n            return this.placeholder.get3DObject();\n        }\n        const drawOffset = art.getDrawOffset();\n        const renderable = this.shpRenderable = ShpRenderable.factory(image, this.palette, this.camera, drawOffset, art.hasShadow);\n        renderable.setBatched(this.useSpriteBatching);\n        if (this.useSpriteBatching) {\n            renderable.setBatchPalettes(this.paletteRemaps);\n        }\n        renderable.create3DObject();\n        const object = renderable.get3DObject();\n        MathUtils.translateTowardsCamera(object, this.camera, 15 * Coords.ISO_WORLD_SCALE);\n        object.updateMatrix();\n        const animProps = new AnimProps(new IniSection(\"dummy\"), image);\n        const animation = new Animation(animProps, this.gameSpeed);\n        this.animRunner = new SimpleRunner();\n        this.animRunner.animation = animation;\n        return object;\n    }\n    setDisguise(disguise: any): void {\n        if (this.gameObject.isDestroyed || this.gameObject.isCrashing)\n            return;\n        this.objectArt = disguise?.objectArt ?? this.gameObject.art;\n        this.updateShpRenderableFromArt(this.objectArt);\n        this.disguiseChanged = true;\n        this.disguise = disguise;\n    }\n    updateShpRenderableFromArt(art: any): void {\n        const currentObject = (this.shpRenderable ?? this.placeholder)?.get3DObject();\n        if (currentObject) {\n            this.posWrap.remove(currentObject);\n            (this.shpRenderable ?? this.placeholder)?.dispose();\n        }\n        this.posWrap.add(this.createMainObject(art));\n    }\n    onCreate(renderableManager: any): void {\n        this.renderableManager = renderableManager;\n        this.plugins.forEach((plugin) => plugin.onCreate(renderableManager));\n        if (this.gameObject.rules.ambientSound) {\n            this.ambientSound = this.worldSound?.playEffect(this.gameObject.rules.ambientSound, this.gameObject);\n        }\n    }\n    onRemove(renderableManager: any): Promise<void> | void {\n        this.renderableManager = undefined;\n        this.plugins.forEach((plugin) => plugin.onRemove(renderableManager));\n        this.ambientSound?.stop();\n        if (this.gameObject.isDestroyed &&\n            this.gameObject.deathType !== DeathType.Temporal &&\n            this.gameObject.deathType !== DeathType.Crush &&\n            this.gameObject.stance !== StanceType.Paradrop) {\n            const deathSequences = sequenceMap.getDeathSequence(this.gameObject, this.gameObject.infDeathType);\n            if (deathSequences) {\n                if (deathSequences.length > 1) {\n                    const randomSequence = deathSequences[math.getRandomInt(0, deathSequences.length - 1)];\n                    this.sequenceQueue = [randomSequence];\n                }\n                else {\n                    this.sequenceQueue = [deathSequences[0]];\n                }\n                if (this.disguise) {\n                    this.objectArt = this.gameObject.art;\n                    this.updateShpRenderableFromArt(this.gameObject.art);\n                }\n            }\n            else {\n                if (!this.gameObject.rules.isHuman)\n                    return;\n                const deathAnim = sequenceMap.getDeathAnim(this.rules, this.gameObject.infDeathType);\n                if (!deathAnim)\n                    return;\n                const animData = this.art.getAnimation(deathAnim);\n                this.deathAnimRenderable = renderableManager.createAnim(deathAnim, undefined, true);\n                this.deathAnimRenderable.create3DObject();\n                this.create3DObject();\n                this.posWrap.add(this.deathAnimRenderable.get3DObject());\n                if (animData.isFlamingGuy) {\n                    const artClone = animData.art.clone();\n                    artClone.set(\"Shadow\", \"yes\");\n                    artClone.set(\"LoopCount\", \"0\");\n                    artClone.set(\"Start\", String(8 * animData.runningFrames));\n                    const animProps = this.deathAnimRenderable.getAnimProps();\n                    animProps.setArt(artClone);\n                }\n            }\n            this.renderableManager = renderableManager;\n            return new Promise<void>((resolve) => {\n                this.deathPromiseResolve = () => {\n                    this.renderableManager = undefined;\n                    resolve();\n                };\n            });\n        }\n    }\n    dispose(): void {\n        this.plugins.forEach((plugin) => plugin.dispose());\n        this.pipOverlay?.dispose();\n        this.shpRenderable?.dispose();\n        this.placeholder?.dispose();\n        this.deathAnimRenderable?.dispose();\n        this.deadBodyAnimRenderable?.dispose();\n        this.paradropAnim?.dispose();\n        this.blobShadow?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/InvulnerableAnimRunner.ts",
    "content": "import { SimpleRunner } from '@/engine/animation/SimpleRunner';\nimport { Animation } from '@/engine/Animation';\nimport { AnimProps } from '@/engine/AnimProps';\nimport { IniSection } from '@/data/IniSection';\nimport { ShpFile } from '@/data/ShpFile';\nexport class InvulnerableAnimRunner extends SimpleRunner {\n    private minAmount: number;\n    private maxAmount: number;\n    private steps: number;\n    declare animation: Animation;\n    constructor(gameSpeed: number, minAmount: number = -0.75, maxAmount: number = -0.5, steps: number = 10, rate: number = 10) {\n        super();\n        this.minAmount = minAmount;\n        this.maxAmount = maxAmount;\n        this.steps = steps;\n        const props = new AnimProps(new IniSection(\"dummy\"), new ShpFile());\n        props.rate = rate;\n        props.loopEnd = steps;\n        props.loopCount = -1;\n        this.animation = new Animation(props, gameSpeed as any);\n        this.animation.stop();\n    }\n    animate(): void {\n        this.animation.reset();\n    }\n    getValue(): number {\n        return this.minAmount +\n            ((1 + Math.sin((2 * Math.PI * this.getCurrentFrame()) / this.steps)) / 2) *\n                (this.maxAmount - this.minAmount);\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/IsoCoords.ts",
    "content": "import { Coords } from '@/game/Coords';\nexport class IsoCoords {\n    private static worldOrigin: {\n        x: number;\n        y: number;\n    };\n    static init(origin: {\n        x: number;\n        y: number;\n    }): void {\n        this.worldOrigin = origin;\n    }\n    static worldToScreen(x: number, y: number): {\n        x: number;\n        y: number;\n    } {\n        if (!this.worldOrigin) {\n            throw new Error(\"Coords not initialized with world origin\");\n        }\n        x -= this.worldOrigin.x;\n        y -= this.worldOrigin.y;\n        return {\n            x: (x /= Coords.ISO_WORLD_SCALE) - (y /= Coords.ISO_WORLD_SCALE),\n            y: (x + y) / 2,\n        };\n    }\n    static screenToWorld(x: number, y: number): {\n        x: number;\n        y: number;\n    } {\n        if (!this.worldOrigin) {\n            throw new Error(\"Coords not initialized with world origin\");\n        }\n        return {\n            x: ((x + 2 * y) / 2) * Coords.ISO_WORLD_SCALE + this.worldOrigin.x,\n            y: ((2 * y - x) / 2) * Coords.ISO_WORLD_SCALE + this.worldOrigin.y,\n        };\n    }\n    static vecWorldToScreen(vec: {\n        x: number;\n        y: number;\n        z: number;\n    }): {\n        x: number;\n        y: number;\n    } {\n        let screen = this.worldToScreen(vec.x, vec.z);\n        screen.y -= this.tileHeightToScreen(Coords.worldToTileHeight(vec.y));\n        return screen;\n    }\n    static tileToScreen(tileX: number, tileY: number): {\n        x: number;\n        y: number;\n    } {\n        const world = Coords.tileToWorld(tileX, tileY);\n        return this.worldToScreen(world.x, world.y);\n    }\n    static tileHeightToScreen(height: number): number {\n        return height * (Coords.ISO_TILE_SIZE / 2);\n    }\n    static tile3dToScreen(tileX: number, tileY: number, height: number): {\n        x: number;\n        y: number;\n    } {\n        let screen = this.tileToScreen(tileX, tileY);\n        screen.y -= this.tileHeightToScreen(height);\n        return screen;\n    }\n    static screenTileToScreen(tileX: number, tileY: number): {\n        x: number;\n        y: number;\n    } {\n        return {\n            x: tileX * Coords.ISO_TILE_SIZE,\n            y: (tileY * Coords.ISO_TILE_SIZE) / 2,\n        };\n    }\n    static screenToScreenTile(x: number, y: number): {\n        x: number;\n        y: number;\n    } {\n        return {\n            x: x / Coords.ISO_TILE_SIZE,\n            y: y / (Coords.ISO_TILE_SIZE / 2),\n        };\n    }\n    static screenTileToWorld(tileX: number, tileY: number): {\n        x: number;\n        y: number;\n    } {\n        const screen = this.screenTileToScreen(tileX, tileY);\n        return this.screenToWorld(screen.x, screen.y);\n    }\n    static getScreenTileSize(): {\n        width: number;\n        height: number;\n    } {\n        return {\n            width: this.tileToScreen(1, 0).x - this.tileToScreen(0, 1).x,\n            height: this.tileToScreen(1, 1).y - this.tileToScreen(0, 0).y,\n        };\n    }\n    static screenDistanceToWorld(x: number, y: number): number {\n        return Coords.screenDistanceToWorld(x, y);\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/Overlay.ts",
    "content": "import { ShpFile } from \"@/data/ShpFile\";\nimport { Coords } from \"@/game/Coords\";\nimport { WithPosition } from \"@/engine/renderable/WithPosition\";\nimport { DebugUtils } from \"@/engine/gfx/DebugUtils\";\nimport { MapSpriteTranslation } from \"@/engine/renderable/MapSpriteTranslation\";\nimport { ShpRenderable } from \"@/engine/renderable/ShpRenderable\";\nimport * as MathUtils from \"@/util/math\";\nimport { BridgeOverlayTypes, OverlayBridgeType } from \"@/game/map/BridgeOverlayTypes\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { DeathType } from \"@/game/gameobject/common/DeathType\";\nimport { BoxIntersectObject3D } from \"@/engine/renderable/entity/BoxIntersectObject3D\";\nimport { MathUtils as EngineMathUtils } from \"@/engine/gfx/MathUtils\";\nimport { MapSurface, MAGIC_OFFSET } from \"@/engine/renderable/entity/map/MapSurface\";\nimport { wallTypes } from \"@/game/map/wallTypes\";\nimport * as THREE from \"three\";\ninterface GameObject {\n    id: string;\n    name: string;\n    rules: any;\n    art: any;\n    tile: any;\n    overlayId: number;\n    value: number;\n    healthTrait?: {\n        health: number;\n    };\n    wallTrait?: any;\n    isDestroyed?: boolean;\n    deathType?: DeathType;\n    isHighBridge(): boolean;\n    isBridge(): boolean;\n    isTiberium(): boolean;\n    isBridgePlaceholder(): boolean;\n    isLowBridge(): boolean;\n    isXBridge(): boolean;\n    getFoundation(): {\n        width: number;\n        height: number;\n    };\n    getUiName(): string;\n}\ninterface Rules {\n    audioVisual: {\n        conditionYellow: number;\n        bridgeExplosions: string[];\n    };\n    getOverlay(name: string): any;\n    getOverlayName(id: number): string;\n}\ninterface Art {\n    getObject(name: string, type: ObjectType): any;\n}\ninterface ObjectArt {\n    lightingType: any;\n    hasShadow: boolean;\n    flat: boolean;\n    getDrawOffset(): THREE.Vector3;\n}\ninterface ImageFinder {\n    findByObjectArt(art: ObjectArt): any;\n}\ninterface Palette {\n}\ninterface Camera {\n}\ninterface Lighting {\n    compute(lightingType: any, tile: any, offset: number): THREE.Vector3;\n}\ninterface DebugFrame {\n    value: boolean;\n}\ninterface MapOverlayLayer {\n    shouldBeBatched(gameObject: GameObject): boolean;\n    addObject(gameObject: GameObject): void;\n    removeObject(gameObject: GameObject): void;\n    hasObject(gameObject: GameObject): boolean;\n    setObjectFrame(gameObject: GameObject, frame: number): void;\n    getObjectFrameCount(gameObject: GameObject): number;\n}\ninterface TransientAnimCreator {\n    createTransientAnim(animName: string, callback: (anim: any) => void): void;\n}\nexport class Overlay {\n    private gameObject: GameObject;\n    private rules: Rules;\n    private art: Art;\n    private imageFinder: ImageFinder;\n    private palette: Palette;\n    private camera: Camera;\n    private lighting: Lighting;\n    private debugFrame: DebugFrame;\n    private bridgeImageCache: Map<OverlayBridgeType, ShpFile>;\n    private mapOverlayLayer: MapOverlayLayer;\n    private useSpriteBatching: boolean;\n    private isInvisible: boolean = false;\n    private objectRules: any;\n    private objectArt: ObjectArt;\n    private label: string;\n    private withPosition: WithPosition;\n    private extraLight: THREE.Vector3;\n    private target?: THREE.Object3D;\n    private lastOverlayHash?: number;\n    private mainRenderable?: ShpRenderable;\n    private intersectTarget?: THREE.Object3D;\n    constructor(gameObject: GameObject, rules: Rules, art: Art, imageFinder: ImageFinder, palette: Palette, camera: Camera, lighting: Lighting, debugFrame: DebugFrame, bridgeImageCache: Map<OverlayBridgeType, ShpFile>, mapOverlayLayer: MapOverlayLayer, useSpriteBatching: boolean) {\n        this.gameObject = gameObject;\n        this.rules = rules;\n        this.art = art;\n        this.imageFinder = imageFinder;\n        this.palette = palette;\n        this.camera = camera;\n        this.lighting = lighting;\n        this.debugFrame = debugFrame;\n        this.bridgeImageCache = bridgeImageCache;\n        this.mapOverlayLayer = mapOverlayLayer;\n        this.useSpriteBatching = useSpriteBatching;\n        this.objectRules = gameObject.rules;\n        this.objectArt = gameObject.art;\n        this.label = \"overlay_\" + this.objectRules.name;\n        this.init();\n    }\n    private init(): void {\n        this.withPosition = new WithPosition();\n        this.extraLight = new THREE.Vector3();\n        this.updateLighting();\n    }\n    private updateLighting(): void {\n        const lightingType = this.objectArt.lightingType;\n        this.extraLight\n            .copy(this.lighting.compute(lightingType, this.gameObject.tile, this.gameObject.isHighBridge() ? 4 : 0))\n            .addScalar(-1);\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    create3DObject(): void {\n        let object3D = this.get3DObject();\n        if (!object3D) {\n            object3D = new THREE.Object3D();\n            object3D.name = this.label;\n            object3D.userData.id = this.gameObject.id;\n            this.target = object3D;\n            object3D.matrixAutoUpdate = false;\n            this.withPosition.matrixUpdate = true;\n            this.withPosition.applyTo(this);\n            this.createObjects(object3D);\n        }\n    }\n    update(deltaTime: number): void {\n        if (this.isInvisible)\n            return;\n        const isDamaged = !!(this.gameObject.healthTrait &&\n            this.gameObject.healthTrait.health <=\n                100 * this.rules.audioVisual.conditionYellow);\n        const overlayHash = 1e5 * this.gameObject.overlayId +\n            10 * this.gameObject.value +\n            Number(isDamaged);\n        if (overlayHash !== this.lastOverlayHash) {\n            this.lastOverlayHash = overlayHash;\n            const frame = this.computeFrame(isDamaged);\n            if (this.mainRenderable) {\n                if (frame < this.mainRenderable.frameCount) {\n                    this.mainRenderable.setFrame(frame);\n                }\n            }\n            else {\n                this.mapOverlayLayer.setObjectFrame(this.gameObject, frame);\n            }\n        }\n    }\n    private computeFrame(isDamaged: boolean): number {\n        const gameObject = this.gameObject;\n        let value = gameObject.value;\n        if (gameObject.isBridge()) {\n            if (value === 0) {\n                value = MathUtils.getRandomInt(0, 3);\n            }\n        }\n        else if (gameObject.wallTrait && isDamaged) {\n            const frameCount = this.mainRenderable\n                ? this.mainRenderable.frameCount\n                : this.mapOverlayLayer.getObjectFrameCount(this.gameObject);\n            const wallTypeOffset = frameCount < wallTypes.length ? 1 : wallTypes.length;\n            value += wallTypeOffset;\n        }\n        return value;\n    }\n    setPosition(position: THREE.Vector3): void {\n        this.withPosition.setPosition(position.x, position.y, position.z);\n    }\n    getPosition(): THREE.Vector3 {\n        return this.withPosition.getPosition();\n    }\n    getIntersectTarget(): THREE.Object3D | undefined {\n        return this.intersectTarget;\n    }\n    getUiName(): string {\n        return this.gameObject.getUiName();\n    }\n    private createObjects(parent: THREE.Object3D): void {\n        const foundation = this.gameObject.getFoundation();\n        if (this.debugFrame.value) {\n            const wireframe = this.createWireframe(foundation, 1);\n            parent.add(wireframe);\n        }\n        if (this.objectRules.isRubble || this.gameObject.isBridgePlaceholder()) {\n            this.isInvisible = true;\n            return;\n        }\n        const needsIntersection = this.gameObject.isBridge() ||\n            this.gameObject.isTiberium() ||\n            this.gameObject.rules.wall;\n        if (this.mapOverlayLayer?.shouldBeBatched(this.gameObject)) {\n            this.mapOverlayLayer.addObject(this.gameObject);\n            if (needsIntersection) {\n                const intersectBox = new BoxIntersectObject3D(new THREE.Vector3(1, 0, 1).multiplyScalar(Coords.LEPTONS_PER_TILE));\n                intersectBox.position.add(new THREE.Vector3(foundation.width / 2, 0, foundation.height / 2).multiplyScalar(Coords.LEPTONS_PER_TILE));\n                intersectBox.matrixAutoUpdate = false;\n                intersectBox.updateMatrix();\n                parent.add(intersectBox);\n                this.intersectTarget = intersectBox;\n            }\n        }\n        else {\n            const container = new THREE.Object3D();\n            container.matrixAutoUpdate = false;\n            const spriteTranslation = new MapSpriteTranslation(foundation.width, foundation.height);\n            const { spriteOffset, anchorPointWorld } = spriteTranslation.compute();\n            const drawOffset = spriteOffset.clone().add(this.objectArt.getDrawOffset());\n            let imageSource: ShpFile;\n            if (this.gameObject.isLowBridge()) {\n                const bridgeType = BridgeOverlayTypes.getOverlayBridgeType(this.gameObject.overlayId);\n                let cachedImage = this.bridgeImageCache.get(bridgeType);\n                if (!cachedImage) {\n                    cachedImage = this.buildVirtualBridgeFile(bridgeType);\n                    this.bridgeImageCache.set(bridgeType, cachedImage);\n                }\n                imageSource = cachedImage;\n            }\n            else {\n                imageSource = this.imageFinder.findByObjectArt(this.objectArt);\n            }\n            const mainRenderable = this.mainRenderable = this.createMainObject(imageSource, drawOffset as any);\n            mainRenderable.create3DObject();\n            container.add(mainRenderable.get3DObject());\n            if (needsIntersection && mainRenderable) {\n                this.intersectTarget = mainRenderable.getShapeMesh();\n            }\n            const tileSize = Coords.getWorldTileSize();\n            container.position.x = anchorPointWorld.x;\n            container.position.z = anchorPointWorld.y;\n            const isXBridge = this.gameObject.isXBridge();\n            if (this.gameObject.isBridge()) {\n                container.position.x += tileSize / 2;\n                container.position.z += tileSize / 2;\n                container.position.x += isXBridge ? 0 : tileSize;\n                container.position.z += isXBridge ? tileSize : 0;\n            }\n            if (this.gameObject.isHighBridge()) {\n                container.position.x -= +Coords.ISO_WORLD_SCALE;\n                container.position.z -= +Coords.ISO_WORLD_SCALE;\n                container.position.x += tileSize + (isXBridge ? 0.5 * tileSize : 0);\n                container.position.z += tileSize + (isXBridge ? 0.5 * tileSize : 0);\n                const shadowMesh = mainRenderable.getShadowMesh();\n                if (shadowMesh) {\n                    EngineMathUtils.translateTowardsCamera(shadowMesh, this.camera as any, (MAGIC_OFFSET + 0.05) * Coords.ISO_WORLD_SCALE);\n                    shadowMesh.updateMatrix();\n                }\n                const bridgeShadowSurface = this.createBridgeShadowSurface();\n                parent.add(bridgeShadowSurface);\n            }\n            if (this.gameObject.isBridge()) {\n                const shapeMesh = mainRenderable.getShapeMesh();\n                if (shapeMesh) {\n                    (shapeMesh as THREE.Mesh).renderOrder = -1;\n                    const mat = (shapeMesh as THREE.Mesh).material as THREE.Material;\n                    mat.depthTest = false;\n                    mat.depthWrite = false;\n                }\n                const shadowMesh = mainRenderable.getShadowMesh();\n                if (shadowMesh) {\n                    (shadowMesh as THREE.Mesh).renderOrder = -1;\n                    const smat = (shadowMesh as THREE.Mesh).material as THREE.Material;\n                    smat.depthTest = false;\n                    smat.depthWrite = false;\n                }\n            }\n            container.updateMatrix();\n            parent.add(container);\n        }\n    }\n    private buildVirtualBridgeFile(bridgeType: OverlayBridgeType): ShpFile {\n        const minId = bridgeType === OverlayBridgeType.Concrete\n            ? BridgeOverlayTypes.minLowBridgeConcreteId\n            : BridgeOverlayTypes.minLowBridgeWoodId;\n        const maxId = bridgeType === OverlayBridgeType.Concrete\n            ? BridgeOverlayTypes.maxLowBridgeConcreteId\n            : BridgeOverlayTypes.maxLowBridgeWoodId;\n        const shpFile = new ShpFile();\n        shpFile.filename = \"agg_\" + this.gameObject.name + \".shp\";\n        for (let id = minId; id <= maxId; id++) {\n            const overlay = this.rules.getOverlay(this.rules.getOverlayName(id));\n            const objectArt = this.art.getObject(overlay.name, ObjectType.Overlay);\n            const imageFile = this.imageFinder.findByObjectArt(objectArt);\n            if (!shpFile.width) {\n                shpFile.width = imageFile.width;\n                shpFile.height = imageFile.height;\n            }\n            shpFile.addImage(imageFile.getImage(1));\n        }\n        return shpFile;\n    }\n    private createBridgeShadowSurface(): THREE.Mesh {\n        const foundation = this.gameObject.getFoundation();\n        const width = foundation.width * Coords.getWorldTileSize();\n        const height = foundation.height * Coords.getWorldTileSize();\n        const geometry = new THREE.PlaneGeometry(width, height);\n        geometry.applyMatrix4(new THREE.Matrix4()\n            .makeTranslation(width / 2, MAGIC_OFFSET, height / 2)\n            .multiply(new THREE.Matrix4().makeRotationX(-Math.PI / 2)));\n        const material = new THREE.ShadowMaterial();\n        material.transparent = true;\n        material.opacity = 0.5;\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.receiveShadow = true;\n        mesh.renderOrder = 5;\n        return mesh;\n    }\n    private createWireframe(foundation: {\n        width: number;\n        height: number;\n    }, thickness: number): THREE.Object3D {\n        const wireframe = DebugUtils.createWireframe(foundation, thickness);\n        const isBridge = this.gameObject.isBridge();\n        wireframe.position.y += isBridge ? Coords.tileHeightToWorld(-1) : 0;\n        return wireframe;\n    }\n    private createMainObject(imageSource: any, drawOffset: THREE.Vector3): ShpRenderable {\n        const isWall = this.objectRules.wall;\n        const heightOffset = this.gameObject.isHighBridge() ? 4 : 0;\n        const renderable = ShpRenderable.factory(imageSource, this.palette, this.camera, drawOffset, this.objectArt.hasShadow && !this.gameObject.isLowBridge(), heightOffset, isWall);\n        renderable.setBatched(this.useSpriteBatching);\n        if (this.useSpriteBatching) {\n            renderable.setBatchPalettes([this.palette]);\n        }\n        renderable.setFlat(this.objectArt.flat);\n        renderable.setExtraLight(this.extraLight);\n        return renderable;\n    }\n    onRemove(transientAnimCreator: TransientAnimCreator): void {\n        if (this.mapOverlayLayer?.hasObject(this.gameObject)) {\n            this.mapOverlayLayer.removeObject(this.gameObject);\n        }\n        if (this.gameObject.isDestroyed &&\n            (this.gameObject.deathType === DeathType.Demolish || this.gameObject.isHighBridge())) {\n            const foundation = this.gameObject.getFoundation();\n            const explosions = this.rules.audioVisual.bridgeExplosions;\n            for (let x = 0; x < foundation.width; x++) {\n                for (let y = 0; y < foundation.height; y++) {\n                    const explosionType = explosions[MathUtils.getRandomInt(0, explosions.length - 1)];\n                    transientAnimCreator.createTransientAnim(explosionType, (anim) => {\n                        anim.setPosition(Coords.tile3dToWorld(x, y, 0).add(this.withPosition.getPosition()));\n                    });\n                }\n            }\n        }\n    }\n    dispose(): void {\n        this.mainRenderable?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/PipOverlay.ts",
    "content": "import { TextureAtlas } from '../../gfx/TextureAtlas';\nimport { IndexedBitmap } from '../../../data/Bitmap';\nimport { SpriteUtils } from '../../gfx/SpriteUtils';\nimport { Coords } from '../../../game/Coords';\nimport { TextureUtils } from '../../gfx/TextureUtils';\nimport { SelectionLevel } from '../../../game/gameobject/selection/SelectionLevel';\nimport { PipColor } from '../../../game/type/PipColor';\nimport { CompositeDisposable } from '../../../util/disposable/CompositeDisposable';\nimport { OverlayUtils } from '../../gfx/OverlayUtils';\nimport { RallyPointFx } from '../fx/RallyPointFx';\nimport { FlyerHelperMode } from './unit/FlyerHelperMode';\nimport { ZoneType } from '../../../game/gameobject/unit/ZoneType';\nimport { BufferGeometryUtils } from '../../gfx/BufferGeometryUtils';\nimport { PaletteBasicMaterial } from '../../gfx/material/PaletteBasicMaterial';\nimport { BatchedMesh, BatchMode } from '../../gfx/batch/BatchedMesh';\nimport { HealthLevel } from '../../../game/gameobject/unit/HealthLevel';\nimport { DebugLabel } from './unit/DebugLabel';\nimport * as THREE from 'three';\nconst HEALTH_BAR_OFFSET = -1;\nconst CONTROL_GROUP_SIZE = { width: 8, height: 11 };\nconst BORDER_WIDTH = 1;\nconst SELECTION_LEVEL_MAP: Record<number, SelectionLevel> = {\n    [0]: SelectionLevel.Hover,\n    2: SelectionLevel.Selected,\n    1: SelectionLevel.Hover,\n    3: SelectionLevel.Hover,\n    4: SelectionLevel.Selected,\n    5: SelectionLevel.Selected,\n};\nconst HEALTH_LEVEL_TO_IMAGE = new Map<HealthLevel, number>()\n    .set(HealthLevel.Green, 15)\n    .set(HealthLevel.Yellow, 16)\n    .set(HealthLevel.Red, 17);\ninterface SpriteGeometryConfig {\n    texture: THREE.Texture;\n    textureArea: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    align: {\n        x: number;\n        y: number;\n    };\n    offset?: {\n        x: number;\n        y: number;\n    };\n    camera: THREE.Camera;\n    scale: number;\n}\ninterface ImageHandle {\n    bitmap: IndexedBitmap;\n    shpFile: any;\n}\ninterface UnitHealthBarResult {\n    healthBarWrapper: THREE.Object3D;\n    selectionBox: THREE.Mesh;\n}\ninterface GameObject {\n    isBuilding(): boolean;\n    isUnit(): boolean;\n    isVehicle(): boolean;\n    isAircraft(): boolean;\n    isInfantry(): boolean;\n    isDestroyed: boolean;\n    isCrashing: boolean;\n    isSpawned: boolean;\n    name: string;\n    ammo?: number;\n    veteranLevel?: number;\n    debugLabel?: string;\n    tileElevation: number;\n    zone: ZoneType;\n    owner: any;\n    position: {\n        worldPosition: THREE.Vector3;\n    };\n    tile: {\n        occluded: boolean;\n    };\n    art: {\n        height: number;\n        isVoxel: boolean;\n        canBeHidden: boolean;\n        foundation: {\n            width: number;\n            height: number;\n        };\n    };\n    rules: {\n        consideredAircraft: boolean;\n        missileSpawn: boolean;\n        factory?: string;\n        storage?: number;\n        passengers?: number;\n        spawnsNumber?: number;\n        maxNumberOccupants?: number;\n        size: number;\n        pip: PipColor;\n    };\n    healthTrait: {\n        health: number;\n        level: HealthLevel;\n    };\n    garrisonTrait?: {\n        units: {\n            length: number;\n        };\n    };\n    rallyTrait?: {\n        getRallyPoint(): {\n            rx: number;\n            ry: number;\n            z: number;\n        } | null;\n    };\n    autoRepairTrait?: {\n        isDisabled(): boolean;\n    };\n    harvesterTrait?: {\n        gems: number;\n        ore: number;\n    };\n    transportTrait?: {\n        units: GameObject[];\n    };\n    airSpawnTrait?: {\n        availableSpawns: number;\n    };\n}\ninterface SelectionModel {\n    getSelectionLevel(): SelectionLevel;\n    getControlGroupNumber(): number | undefined;\n}\ninterface AnimFactory {\n    (name: string): any;\n}\nexport class PipOverlay {\n    private static atlasCache?: TextureAtlas;\n    private static atlasImageHandles = new Map<any, ImageHandle>();\n    private static geometries = new Map<any, THREE.BufferGeometry>();\n    private static buildingHealthGeoCache = new Map<string, THREE.BufferGeometry>();\n    private static unitHealthGeoCache = new Map<string, THREE.BufferGeometry>();\n    private static unitHealthTextures = new Map<boolean, THREE.Texture>();\n    private static unitHealthMaterials = new Map<boolean, THREE.Material>();\n    private static controlGroupTextures = new Map<number, THREE.Texture>();\n    private static controlGroupMaterials = new Map<THREE.Texture, THREE.Material>();\n    private static primaryFactoryTextures = new Map<number, THREE.Texture>();\n    private static primaryFactoryMaterials = new Map<THREE.Texture, THREE.Material>();\n    private static material?: THREE.Material;\n    private static pipBrdFile: any;\n    private static pipsFile: any;\n    private static pips2File: any;\n    private paradropRules: any;\n    private audioVisualRules: any;\n    private gameObject: GameObject;\n    private viewer: {\n        value?: any;\n    };\n    private alliances: any;\n    private selectionModel: SelectionModel;\n    private imageFinder: any;\n    private palette: any;\n    private camera: THREE.Camera;\n    private strings: Map<string, string>;\n    private flyerHelperOpt: {\n        value: FlyerHelperMode;\n    };\n    private hiddenObjectsOpt: {\n        value: boolean;\n    };\n    private debugTextEnabled: {\n        value: boolean;\n    };\n    private animFactory: AnimFactory;\n    private useSpriteBatching: boolean;\n    private useMeshInstancing: boolean;\n    private lastPrimaryFactory = false;\n    private lastHealth?: number;\n    private lastOwner?: any;\n    private lastOwnerColorHex?: number;\n    private lastPipsDataKey?: any;\n    private lastControlGroup?: number;\n    private lastRallyPoint?: any;\n    private lastRepairState?: boolean;\n    private lastVeteranLevel?: number;\n    private lastSelectionLevel?: SelectionLevel;\n    private lastDebugLabel?: string;\n    private lastDebugTextEnabled?: boolean;\n    private invalidatedElements: (boolean | undefined)[] = [];\n    private rootObj?: THREE.Object3D;\n    private healthBar?: THREE.Object3D;\n    private selectionBox?: THREE.Mesh;\n    private pipsSprite?: THREE.Mesh;\n    private controlGroupSprite?: THREE.Mesh;\n    private primaryFactorySprite?: THREE.Mesh;\n    private veteranIndicator?: THREE.Mesh;\n    private rallyLine?: RallyPointFx;\n    private repairWrench?: any;\n    private flyHelper?: any;\n    private behindAnim?: any;\n    private debugLabel?: DebugLabel;\n    private lastDebugLabelOwnerColorHex?: number;\n    private disposables = new CompositeDisposable();\n    static clearCaches(): void {\n        PipOverlay.atlasCache?.dispose();\n        PipOverlay.atlasCache = undefined;\n        PipOverlay.atlasImageHandles.clear();\n        [...PipOverlay.unitHealthTextures.values()].forEach(texture => texture.dispose());\n        PipOverlay.unitHealthTextures.clear();\n        PipOverlay.unitHealthMaterials.forEach(material => material.dispose());\n        PipOverlay.unitHealthMaterials.clear();\n        [...PipOverlay.controlGroupTextures.values()].forEach(texture => texture.dispose());\n        PipOverlay.controlGroupTextures.clear();\n        PipOverlay.controlGroupMaterials.forEach(material => material.dispose());\n        PipOverlay.controlGroupMaterials.clear();\n        [...PipOverlay.primaryFactoryTextures.values()].forEach(texture => texture.dispose());\n        PipOverlay.primaryFactoryTextures.clear();\n        PipOverlay.primaryFactoryMaterials.forEach(material => material.dispose());\n        PipOverlay.primaryFactoryMaterials.clear();\n    }\n    constructor(paradropRules: any, audioVisualRules: any, gameObject: GameObject, viewer: {\n        value?: any;\n    }, alliances: any, selectionModel: SelectionModel, imageFinder: any, palette: any, camera: THREE.Camera, strings: Map<string, string>, flyerHelperOpt: {\n        value: FlyerHelperMode;\n    }, hiddenObjectsOpt: {\n        value: boolean;\n    }, debugTextEnabled: {\n        value: boolean;\n    }, animFactory: AnimFactory, useSpriteBatching: boolean, useMeshInstancing: boolean) {\n        this.paradropRules = paradropRules;\n        this.audioVisualRules = audioVisualRules;\n        this.gameObject = gameObject;\n        this.viewer = viewer;\n        this.alliances = alliances;\n        this.selectionModel = selectionModel;\n        this.imageFinder = imageFinder;\n        this.palette = palette;\n        this.camera = camera;\n        this.strings = strings;\n        this.flyerHelperOpt = flyerHelperOpt;\n        this.hiddenObjectsOpt = hiddenObjectsOpt;\n        this.debugTextEnabled = debugTextEnabled;\n        this.animFactory = animFactory;\n        this.useSpriteBatching = useSpriteBatching;\n        this.useMeshInstancing = useMeshInstancing;\n    }\n    create3DObject(): void {\n        let rootObj = this.rootObj;\n        if (!rootObj) {\n            rootObj = new THREE.Object3D();\n            rootObj.name = \"pip_overlay\";\n            rootObj.matrixAutoUpdate = false;\n            if (!PipOverlay.atlasCache) {\n                const atlas = this.initTexture();\n                PipOverlay.atlasCache = atlas;\n                [...PipOverlay.atlasImageHandles.keys()].forEach(imageHandle => {\n                    const geometry = SpriteUtils.createSpriteGeometry(this.buildSpriteGeometry(imageHandle));\n                    PipOverlay.geometries.set(imageHandle, geometry);\n                });\n                PipOverlay.material = new PaletteBasicMaterial({\n                    map: PipOverlay.atlasCache.getTexture(),\n                    palette: TextureUtils.textureFromPalette(this.palette),\n                    alphaTest: 0.5,\n                    transparent: true,\n                    depthTest: false,\n                });\n            }\n            if (this.gameObject.isBuilding()) {\n                this.healthBar = this.createBuildingHealthBar(this.gameObject);\n                rootObj.add(this.healthBar);\n                if (this.gameObject.art.height >= 1) {\n                    this.selectionBox = this.createBuildingSelectionBox(this.gameObject) as any;\n                    rootObj.add(this.selectionBox);\n                }\n                const occupationInfo = this.createBuildingOccupationInfo(this.gameObject);\n                if (occupationInfo) {\n                    rootObj.add(occupationInfo);\n                    this.pipsSprite = occupationInfo;\n                }\n                this.lastPipsDataKey = this.gameObject.garrisonTrait?.units.length;\n            }\n            else {\n                const { healthBarWrapper, selectionBox } = this.createUnitHealthBar(this.gameObject);\n                this.healthBar = healthBarWrapper;\n                this.selectionBox = selectionBox;\n                rootObj.add(this.healthBar);\n                if (this.gameObject.art.isVoxel &&\n                    (this.gameObject.rules.consideredAircraft || this.gameObject.isAircraft()) &&\n                    !this.gameObject.rules.missileSpawn) {\n                    const flyHelper = this.animFactory(this.audioVisualRules.flyerHelper);\n                    this.flyHelper = flyHelper;\n                    flyHelper.create3DObject();\n                    rootObj.add(flyHelper.get3DObject());\n                }\n                if (this.gameObject.isUnit()) {\n                    const behindAnim = this.animFactory(this.audioVisualRules.behind);\n                    behindAnim.setRenderOrder(999995);\n                    this.behindAnim = behindAnim;\n                }\n            }\n            if (this.gameObject.debugLabel && this.debugTextEnabled.value) {\n                const debugLabel = new DebugLabel(this.gameObject.debugLabel, this.gameObject.owner.color.asHex(), this.camera);\n                this.debugLabel = debugLabel;\n                debugLabel.create3DObject();\n                debugLabel.get3DObject().renderOrder = 999999;\n                rootObj.add(debugLabel.get3DObject());\n            }\n            this.lastHealth = this.gameObject.healthTrait.health;\n            this.lastOwner = this.gameObject.owner;\n            this.lastOwnerColorHex = this.gameObject.owner.color.asHex();\n            this.rootObj = rootObj;\n        }\n    }\n    onCreate(effectManager: any): void {\n        if (this.gameObject.isBuilding() && this.gameObject.rallyTrait) {\n            this.rallyLine = new RallyPointFx(this.camera as any, new THREE.Vector3(), new THREE.Vector3(), new THREE.Color(), 999999);\n            this.rallyLine.visible = false;\n            effectManager.addEffect(this.rallyLine);\n            this.disposables.add(() => this.rallyLine!.remove(), this.rallyLine);\n        }\n    }\n    private initTexture(): TextureAtlas {\n        PipOverlay.pipBrdFile = this.imageFinder.find(\"pipbrd\", false);\n        PipOverlay.pipsFile = this.imageFinder.find(\"pips\", false);\n        PipOverlay.pips2File = this.imageFinder.find(\"pips2\", false);\n        const files = [PipOverlay.pipBrdFile, PipOverlay.pipsFile, PipOverlay.pips2File];\n        const bitmaps: IndexedBitmap[] = [];\n        files.forEach(file => {\n            for (let i = 0; i < file.numImages; i++) {\n                const image = file.getImage(i);\n                const bitmap = new IndexedBitmap(image.width, image.height, image.imageData);\n                bitmaps.push(bitmap);\n                PipOverlay.atlasImageHandles.set(image, { bitmap, shpFile: file });\n            }\n        });\n        const atlas = new TextureAtlas();\n        atlas.pack(bitmaps);\n        return atlas;\n    }\n    private buildSpriteGeometry(imageHandle: any): SpriteGeometryConfig {\n        if (!PipOverlay.atlasCache) {\n            throw new Error(\"Must build texture atlas before geometry\");\n        }\n        const atlas = PipOverlay.atlasCache;\n        const { bitmap, shpFile } = PipOverlay.atlasImageHandles.get(imageHandle)!;\n        return {\n            texture: atlas.getTexture(),\n            textureArea: atlas.getImageRect(bitmap),\n            align: { x: 1, y: -1 },\n            offset: {\n                x: imageHandle.x - Math.floor(shpFile.width / 2),\n                y: imageHandle.y - Math.floor(shpFile.height / 2),\n            },\n            camera: this.camera,\n            scale: Coords.ISO_WORLD_SCALE,\n        };\n    }\n    private createBuildingHealthBar(gameObject: GameObject): THREE.Mesh {\n        const foundationHeight = gameObject.art.foundation.height;\n        const health = gameObject.healthTrait.health;\n        const spacing = 4 * Coords.ISO_WORLD_SCALE;\n        const maxPips = Math.floor((foundationHeight * Coords.getWorldTileSize()) / spacing);\n        const healthPips = Math.max(1, Math.floor((health / 100) * maxPips));\n        let pipImageIndex: number;\n        if (health > 100 * this.audioVisualRules.conditionYellow) {\n            pipImageIndex = 1;\n        }\n        else if (health > 100 * this.audioVisualRules.conditionRed) {\n            pipImageIndex = 2;\n        }\n        else {\n            pipImageIndex = 4;\n        }\n        const cacheKey = `${pipImageIndex}_${foundationHeight}_${healthPips}`;\n        let geometry = PipOverlay.buildingHealthGeoCache.get(cacheKey);\n        if (!geometry) {\n            const geometries: THREE.BufferGeometry[] = [];\n            const emptyPipImage = PipOverlay.pipsFile.getImage(0);\n            const healthPipImage = PipOverlay.pipsFile.getImage(pipImageIndex);\n            for (let i = 0; i < maxPips; i++) {\n                const image = i < healthPips ? healthPipImage : emptyPipImage;\n                const pipGeometry = PipOverlay.geometries.get(image)!.clone();\n                const yOffset = spacing * i + spacing / 2;\n                pipGeometry.applyMatrix4(new THREE.Matrix4().makeTranslation(spacing, 0, yOffset));\n                geometries.push(pipGeometry);\n            }\n            geometry = BufferGeometryUtils.mergeBufferGeometries(geometries);\n            PipOverlay.buildingHealthGeoCache.set(cacheKey, geometry);\n        }\n        const mesh = this.useMeshInstancing\n            ? new BatchedMesh(geometry, PipOverlay.material!, BatchMode.Instancing)\n            : new THREE.Mesh(geometry, PipOverlay.material!);\n        mesh.matrixAutoUpdate = false;\n        mesh.renderOrder = 999999;\n        const height = gameObject.art.height || 0.5;\n        mesh.position.y = Coords.tileHeightToWorld(height);\n        mesh.updateMatrix();\n        return mesh;\n    }\n    private createUnitHealthBar(gameObject: GameObject): UnitHealthBarResult {\n        const isVehicle = !gameObject.isInfantry();\n        const health = gameObject.healthTrait.health;\n        const healthLevel = gameObject.healthTrait.level;\n        let healthTexture = PipOverlay.unitHealthTextures.get(isVehicle);\n        if (!healthTexture) {\n            healthTexture = this.createUnitHealthTexture(isVehicle);\n            PipOverlay.unitHealthTextures.set(isVehicle, healthTexture);\n        }\n        const borderImage = PipOverlay.pipBrdFile.getImage(isVehicle ? 0 : 1);\n        const healthImageIndex = HEALTH_LEVEL_TO_IMAGE.get(healthLevel);\n        if (healthImageIndex === undefined) {\n            throw new Error(`Unhandled health level \"${healthLevel}\"`);\n        }\n        const healthPipImage = PipOverlay.pipsFile.getImage(healthImageIndex);\n        const maxPips = Math.floor((borderImage.width - 2 * BORDER_WIDTH) / healthPipImage.width);\n        const currentPips = Math.max(1, Math.floor((health / 100) * maxPips));\n        const healthGeoCacheKey = `${isVehicle ? 1 : 0}_${currentPips}`;\n        let healthGeometry = PipOverlay.unitHealthGeoCache.get(healthGeoCacheKey);\n        if (!healthGeometry) {\n            healthGeometry = SpriteUtils.createSpriteGeometry({\n                texture: healthTexture,\n                textureArea: {\n                    x: 0,\n                    y: (currentPips - 1) * borderImage.height,\n                    width: borderImage.width,\n                    height: borderImage.height,\n                },\n                camera: this.camera,\n                align: { x: 0, y: 0 },\n                scale: Coords.ISO_WORLD_SCALE,\n            });\n            PipOverlay.unitHealthGeoCache.set(healthGeoCacheKey, healthGeometry);\n        }\n        let healthMaterial = PipOverlay.unitHealthMaterials.get(isVehicle);\n        if (!healthMaterial) {\n            healthMaterial = new PaletteBasicMaterial({\n                map: healthTexture,\n                palette: TextureUtils.textureFromPalette(this.palette),\n                alphaTest: 0.5,\n                transparent: true,\n                depthTest: false,\n            });\n            PipOverlay.unitHealthMaterials.set(isVehicle, healthMaterial);\n        }\n        const healthMesh = this.useSpriteBatching\n            ? new BatchedMesh(healthGeometry, healthMaterial, BatchMode.Merging)\n            : new THREE.Mesh(healthGeometry, healthMaterial);\n        healthMesh.matrixAutoUpdate = false;\n        healthMesh.renderOrder = 999998;\n        const healthOffset = Coords.screenDistanceToWorld(Math.floor(borderImage.width / 2) + HEALTH_BAR_OFFSET, 0);\n        healthMesh.applyMatrix4(new THREE.Matrix4().makeTranslation(healthOffset.x, 0, healthOffset.y));\n        healthMesh.updateMatrix();\n        const borderGeometry = PipOverlay.geometries.get(borderImage)!;\n        const borderMesh = this.useSpriteBatching\n            ? new BatchedMesh(borderGeometry, PipOverlay.material!, BatchMode.Merging)\n            : new THREE.Mesh(borderGeometry, PipOverlay.material!);\n        borderMesh.matrixAutoUpdate = false;\n        const borderOffset = Coords.screenDistanceToWorld(Math.floor(PipOverlay.pipBrdFile.getImage(0).width / 2) + HEALTH_BAR_OFFSET, 0);\n        borderMesh.applyMatrix4(new THREE.Matrix4().makeTranslation(borderOffset.x, 0, borderOffset.y));\n        borderMesh.updateMatrix();\n        borderMesh.renderOrder = 999997;\n        const wrapper = new THREE.Object3D();\n        wrapper.matrixAutoUpdate = false;\n        wrapper.add(borderMesh);\n        wrapper.add(healthMesh);\n        const wrapperOffset = Coords.screenDistanceToWorld(-Math.floor(borderImage.width / 2), 0);\n        wrapper.applyMatrix4(new THREE.Matrix4().makeTranslation(wrapperOffset.x, Coords.tileHeightToWorld(2), wrapperOffset.y));\n        wrapper.updateMatrix();\n        return { healthBarWrapper: wrapper, selectionBox: borderMesh };\n    }\n    private createUnitHealthTexture(isVehicle: boolean): THREE.Texture {\n        const borderImage = PipOverlay.pipBrdFile.getImage(isVehicle ? 0 : 1);\n        const pipWidth = PipOverlay.pipsFile.getImage(HEALTH_LEVEL_TO_IMAGE.values().next().value).width;\n        const maxPips = Math.floor((borderImage.width - 2 * BORDER_WIDTH) / pipWidth);\n        const bitmap = new IndexedBitmap(borderImage.width, borderImage.height * maxPips);\n        for (let pips = 1; pips <= maxPips; ++pips) {\n            const healthPercent = (pips / maxPips) * 100;\n            let healthLevel: HealthLevel;\n            if (healthPercent > 100 * this.audioVisualRules.conditionYellow) {\n                healthLevel = HealthLevel.Green;\n            }\n            else if (healthPercent > 100 * this.audioVisualRules.conditionRed) {\n                healthLevel = HealthLevel.Yellow;\n            }\n            else {\n                healthLevel = HealthLevel.Red;\n            }\n            const imageIndex = HEALTH_LEVEL_TO_IMAGE.get(healthLevel);\n            if (imageIndex === undefined) {\n                throw new Error(`Unhandled health level \"${healthLevel}\"`);\n            }\n            const pipImage = PipOverlay.pipsFile.getImage(imageIndex);\n            const pipBitmap = new IndexedBitmap(pipImage.width, pipImage.height, pipImage.imageData);\n            const yOffset = (pips - 1) * borderImage.height;\n            for (let i = 0; i < pips; i++) {\n                const xOffset = pipImage.width * i;\n                bitmap.drawIndexedImage(pipBitmap, xOffset + BORDER_WIDTH, yOffset + BORDER_WIDTH);\n            }\n        }\n        const rgbaData = new Uint8Array(bitmap.width * bitmap.height * 4);\n        for (let i = 0; i < bitmap.data.length; i++) {\n            const base = i * 4;\n            rgbaData[base] = 0;\n            rgbaData[base + 1] = 0;\n            rgbaData[base + 2] = 0;\n            rgbaData[base + 3] = bitmap.data[i];\n        }\n        const texture = new THREE.DataTexture(rgbaData, bitmap.width, bitmap.height, THREE.RGBAFormat);\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        texture.flipY = true;\n        texture.needsUpdate = true;\n        (texture as any).colorSpace = (THREE as any).LinearSRGBColorSpace ?? (texture as any).colorSpace;\n        return texture;\n    }\n    private createBuildingSelectionBox(gameObject: GameObject): THREE.Object3D {\n        const container = new THREE.Object3D();\n        container.matrixAutoUpdate = false;\n        const foundation = gameObject.art.foundation;\n        const tileSize = Coords.getWorldTileSize();\n        const corners: [\n            number,\n            number\n        ][] = [\n            [0, 0], [0, 1], [1, 1], [1, 0]\n        ];\n        corners.forEach(([x, z], index) => {\n            const cornerMesh = this.createBuildingSelectionCornerMesh();\n            cornerMesh.matrixAutoUpdate = false;\n            cornerMesh.position.set(x * tileSize * foundation.width, Coords.tileHeightToWorld(gameObject.art.height), z * tileSize * foundation.height);\n            cornerMesh.rotation.y = (index * Math.PI) / 2;\n            cornerMesh.scale.set(((index % 2 === 0 ? foundation.width : foundation.height) / 4) * Coords.getWorldTileSize(), Coords.tileHeightToWorld(gameObject.art.height / 4), ((index % 2 === 0 ? foundation.height : foundation.width) / 4) * Coords.getWorldTileSize());\n            cornerMesh.updateMatrix();\n            container.add(cornerMesh);\n        });\n        return container;\n    }\n    private createBuildingSelectionCornerMesh(): THREE.LineSegments {\n        const positions = [\n            0, 0, 0, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1,\n        ];\n        const colors = new Array(positions.length).fill(1);\n        const geometry = new THREE.BufferGeometry();\n        geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));\n        geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colors), 3));\n        const material = new THREE.LineBasicMaterial({\n            vertexColors: true,\n        });\n        this.disposables.add(geometry, material);\n        return new THREE.LineSegments(geometry, material);\n    }\n    private createBuildingOccupationInfo(gameObject: GameObject): THREE.Mesh | undefined {\n        if (gameObject.garrisonTrait?.units.length &&\n            !this.objectIsOpaqueToViewer()) {\n            const occupiedSlots = gameObject.garrisonTrait.units.length;\n            const maxSlots = gameObject.rules.maxNumberOccupants!;\n            const geometries: THREE.BufferGeometry[] = [];\n            const spacing = 4 * Coords.ISO_WORLD_SCALE;\n            const emptySlotImage = PipOverlay.pipsFile.getImage(6);\n            const occupiedSlotImage = PipOverlay.pipsFile.getImage(7);\n            for (let i = 1; i <= maxSlots; i++) {\n                const image = i <= occupiedSlots ? occupiedSlotImage : emptySlotImage;\n                const geometry = PipOverlay.geometries.get(image)!.clone();\n                const xOffset = spacing * i + spacing / 2;\n                geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(xOffset, 0, gameObject.art.foundation.height * Coords.getWorldTileSize()));\n                geometries.push(geometry);\n            }\n            const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);\n            const mesh = this.useSpriteBatching\n                ? new BatchedMesh(mergedGeometry, PipOverlay.material!, BatchMode.Merging)\n                : new THREE.Mesh(mergedGeometry, PipOverlay.material!);\n            mesh.matrixAutoUpdate = false;\n            mesh.renderOrder = 999999;\n            return mesh;\n        }\n    }\n    private createPipsSprite(pipColors: PipColor[], totalSlots: number, isAircraft = false): THREE.Mesh | undefined {\n        if (!this.objectIsOpaqueToViewer()) {\n            const geometries: THREE.BufferGeometry[] = [];\n            const pipWidth = PipOverlay.pips2File.getImage(isAircraft ? 12 : 0).width;\n            const emptyPipImage = PipOverlay.pips2File.getImage(isAircraft ? 13 : 0);\n            for (let i = 0; i < totalSlots; i++) {\n                let pipImage: any;\n                if (i < pipColors.length) {\n                    const color = pipColors[i];\n                    let imageIndex = isAircraft ? 12 : 3;\n                    if (color === PipColor.Blue) {\n                        imageIndex = 5;\n                    }\n                    else if (color === PipColor.Red) {\n                        imageIndex = 4;\n                    }\n                    else if (color === PipColor.Yellow) {\n                        imageIndex = 2;\n                    }\n                    pipImage = PipOverlay.pips2File.getImage(imageIndex);\n                }\n                else {\n                    pipImage = emptyPipImage;\n                }\n                const geometry = PipOverlay.geometries.get(pipImage)!.clone();\n                const xOffset = pipWidth * i + pipWidth / 2;\n                const screenOffset = Coords.screenDistanceToWorld(-Math.floor(PipOverlay.pipBrdFile.getImage(this.gameObject.isInfantry() ? 1 : 0).width / 2) + xOffset, Math.floor(emptyPipImage.height / 2) + 3);\n                geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(screenOffset.x, 0, screenOffset.y));\n                geometries.push(geometry);\n            }\n            const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);\n            const mesh = this.useSpriteBatching\n                ? new BatchedMesh(mergedGeometry, PipOverlay.material!, BatchMode.Merging)\n                : new THREE.Mesh(mergedGeometry, PipOverlay.material!);\n            mesh.renderOrder = 999996;\n            return mesh;\n        }\n    }\n    private createControlGroupTexture(color: any): THREE.Texture {\n        const canvas = document.createElement('canvas');\n        const ctx = canvas.getContext('2d', { alpha: false })!;\n        const borderWidth = BORDER_WIDTH;\n        const size = CONTROL_GROUP_SIZE;\n        canvas.width = 10 * (size.width + 2 * borderWidth);\n        canvas.height = size.height + 2 * borderWidth;\n        ctx.fillStyle = '#000';\n        ctx.fillRect(0, 0, canvas.width, canvas.height);\n        ctx.strokeStyle = color.asHexString();\n        ctx.fillStyle = color.asHexString();\n        ctx.font = 'bold 12px Arial, sans-serif';\n        for (let i = 0; i < 10; i++) {\n            const x = (size.width + 2 * borderWidth) * i;\n            ctx.strokeRect(0.5 + x, 0.5, size.width + 2 * borderWidth - 1, canvas.height - 1);\n            ctx.fillText(String(i), x + borderWidth + 0.5, size.height);\n        }\n        const texture = new THREE.Texture(canvas);\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        texture.needsUpdate = true;\n        return texture;\n    }\n    private createControlGroupSprite(groupNumber: number): THREE.Mesh {\n        const color = this.gameObject.owner.color;\n        if (!PipOverlay.controlGroupTextures.has(color.asHex())) {\n            const texture = this.createControlGroupTexture(color);\n            PipOverlay.controlGroupTextures.set(color.asHex(), texture);\n        }\n        const texture = PipOverlay.controlGroupTextures.get(color.asHex())!;\n        const geometry = SpriteUtils.createSpriteGeometry({\n            texture,\n            textureArea: {\n                x: groupNumber * (CONTROL_GROUP_SIZE.width + 2 * BORDER_WIDTH),\n                y: 0,\n                width: CONTROL_GROUP_SIZE.width + 2 * BORDER_WIDTH,\n                height: CONTROL_GROUP_SIZE.height + 2 * BORDER_WIDTH,\n            },\n            camera: this.camera,\n            align: { x: 1, y: -1 },\n            scale: Coords.ISO_WORLD_SCALE,\n        });\n        let material = PipOverlay.controlGroupMaterials.get(texture);\n        if (!material) {\n            material = new THREE.MeshBasicMaterial({\n                map: texture,\n                alphaTest: 0.5,\n                transparent: true,\n                depthTest: false,\n            });\n            PipOverlay.controlGroupMaterials.set(texture, material);\n        }\n        const mesh = this.useSpriteBatching\n            ? new BatchedMesh(geometry, material, BatchMode.Merging)\n            : new THREE.Mesh(geometry, material);\n        mesh.matrixAutoUpdate = false;\n        mesh.renderOrder = 999996;\n        return mesh;\n    }\n    private createPrimaryFactoryTexture(color: any): THREE.Texture {\n        const canvas = OverlayUtils.createTextBox(this.strings.get('TXT_PRIMARY')!, {\n            color: color.asHexString(),\n            borderColor: color.asHexString(),\n            backgroundColor: '#000',\n            fontFamily: \"'Fira Sans Condensed', Arial, sans-serif\",\n            fontSize: 12,\n            fontWeight: '500',\n            paddingTop: 5,\n            paddingBottom: 5,\n            paddingLeft: 2,\n            paddingRight: 4,\n        });\n        const texture = new THREE.Texture(canvas);\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        texture.needsUpdate = true;\n        return texture;\n    }\n    private createPrimaryFactorySprite(): THREE.Mesh | undefined {\n        if (!this.objectIsOpaqueToViewer()) {\n            const color = this.gameObject.owner.color;\n            if (!PipOverlay.primaryFactoryTextures.has(color.asHex())) {\n                const texture = this.createPrimaryFactoryTexture(color);\n                PipOverlay.primaryFactoryTextures.set(color.asHex(), texture);\n            }\n            const texture = PipOverlay.primaryFactoryTextures.get(color.asHex())!;\n            const geometry = SpriteUtils.createSpriteGeometry({\n                texture,\n                camera: this.camera,\n                align: { x: 1, y: -1 },\n                offset: {\n                    x: -Math.floor((texture.image as any).width / 2),\n                    y: -Math.floor((texture.image as any).height / 2),\n                },\n                scale: Coords.ISO_WORLD_SCALE,\n            });\n            let material = PipOverlay.primaryFactoryMaterials.get(texture);\n            if (!material) {\n                material = new THREE.MeshBasicMaterial({\n                    map: texture,\n                    alphaTest: 0.5,\n                    transparent: true,\n                    depthTest: false,\n                });\n                PipOverlay.primaryFactoryMaterials.set(texture, material);\n            }\n            const mesh = this.useSpriteBatching\n                ? new BatchedMesh(geometry, material, BatchMode.Merging)\n                : new THREE.Mesh(geometry, material);\n            mesh.renderOrder = 999999;\n            return mesh;\n        }\n    }\n    private createVeteranIndicator(gameObject: GameObject): THREE.Mesh | undefined {\n        if (gameObject.veteranLevel) {\n            const image = PipOverlay.pipsFile.getImage(13 + gameObject.veteranLevel - 1);\n            const geometry = PipOverlay.geometries.get(image)!;\n            const mesh = this.useSpriteBatching\n                ? new BatchedMesh(geometry, PipOverlay.material!, BatchMode.Merging)\n                : new THREE.Mesh(geometry, PipOverlay.material!);\n            mesh.matrixAutoUpdate = false;\n            mesh.renderOrder = 999996;\n            mesh.receiveShadow = false;\n            return mesh;\n        }\n    }\n    private createRepairWrench(): any {\n        const wrench = this.animFactory('WRENCH');\n        wrench.setRenderOrder(999998);\n        return wrench;\n    }\n    private objectIsOpaqueToViewer(): boolean {\n        const viewer = this.viewer?.value;\n        return !(!viewer || viewer.isObserver) &&\n            !(this.gameObject.owner === viewer ||\n                this.alliances.areAllied(this.gameObject.owner, viewer));\n    }\n    update(deltaTime: number): void {\n        const gameObject = this.gameObject;\n        if (gameObject.isDestroyed || gameObject.isCrashing) {\n            this.rootObj!.visible = false;\n            return;\n        }\n        const ownerColorHex = gameObject.owner.color.asHex();\n        const ownerColorChanged = this.lastOwnerColorHex !== ownerColorHex;\n        if (ownerColorChanged) {\n            this.lastOwnerColorHex = ownerColorHex;\n            this.invalidatedElements[3] = true;\n            this.invalidatedElements[4] = true;\n            this.invalidatedElements[5] = true;\n        }\n        if (gameObject.healthTrait.health !== this.lastHealth) {\n            this.lastHealth = gameObject.healthTrait.health;\n            this.invalidatedElements[0] = true;\n        }\n        const selectionLevel = this.selectionModel.getSelectionLevel();\n        if (this.invalidatedElements[0] &&\n            (selectionLevel >= SELECTION_LEVEL_MAP[0] || selectionLevel >= SELECTION_LEVEL_MAP[3])) {\n            this.invalidatedElements[0] = undefined;\n            this.updateHealthBarSprite(selectionLevel);\n        }\n        const pipsDataKey = this.computePipsDataKey(gameObject);\n        if (this.lastPipsDataKey !== pipsDataKey || this.lastOwner !== gameObject.owner) {\n            this.lastPipsDataKey = pipsDataKey;\n            this.invalidatedElements[1] = true;\n        }\n        if (this.invalidatedElements[1] && selectionLevel >= SELECTION_LEVEL_MAP[1]) {\n            this.invalidatedElements[1] = undefined;\n            this.updatePipsSprite();\n        }\n        const controlGroup = this.selectionModel.getControlGroupNumber();\n        if (this.lastControlGroup !== controlGroup) {\n            this.lastControlGroup = controlGroup;\n            this.invalidatedElements[3] = true;\n        }\n        if (this.invalidatedElements[3] && selectionLevel >= SELECTION_LEVEL_MAP[3]) {\n            this.invalidatedElements[3] = undefined;\n            this.updateControlGroupSprite(controlGroup);\n        }\n        const isPrimaryFactory = gameObject.isBuilding() &&\n            !!gameObject.rules.factory &&\n            gameObject.owner.production?.getPrimaryFactory(gameObject.rules.factory) === gameObject;\n        if (this.lastPrimaryFactory !== isPrimaryFactory || this.lastOwner !== gameObject.owner) {\n            this.lastPrimaryFactory = isPrimaryFactory;\n            this.invalidatedElements[4] = true;\n        }\n        if (this.invalidatedElements[4] && selectionLevel >= SELECTION_LEVEL_MAP[4]) {\n            this.invalidatedElements[4] = undefined;\n            this.updatePrimaryFactorySprite(isPrimaryFactory);\n        }\n        const rallyPoint = (gameObject.isBuilding() && gameObject.rallyTrait?.getRallyPoint()) || undefined;\n        if (this.lastRallyPoint !== rallyPoint || this.lastOwner !== gameObject.owner) {\n            this.lastRallyPoint = rallyPoint;\n            this.invalidatedElements[5] = true;\n        }\n        if (this.invalidatedElements[5] && selectionLevel >= SELECTION_LEVEL_MAP[5] && this.rallyLine) {\n            this.invalidatedElements[5] = undefined;\n            this.updateRallyPointLine(rallyPoint, this.rallyLine);\n        }\n        if (gameObject.isBuilding()) {\n            const repairState = !gameObject.autoRepairTrait?.isDisabled();\n            if (this.lastRepairState !== repairState) {\n                this.lastRepairState = repairState;\n                this.updateRepairWrenchSprite(repairState);\n            }\n        }\n        else {\n            if (this.lastVeteranLevel !== gameObject.veteranLevel) {\n                this.lastVeteranLevel = gameObject.veteranLevel;\n                this.updateVeteranIndicatorSprite(gameObject);\n            }\n        }\n        this.updateFlyerHelper(selectionLevel, deltaTime);\n        this.updateBehindAnim(deltaTime);\n        this.updateDebugLabel();\n        if (this.lastSelectionLevel === undefined || this.lastSelectionLevel !== selectionLevel) {\n            this.lastSelectionLevel = selectionLevel;\n            const elementMap = new Map<number, any>([\n                [0, this.healthBar],\n                [2, this.selectionBox],\n                [1, this.pipsSprite],\n                [3, this.controlGroupSprite],\n                [4, this.primaryFactorySprite],\n                [5, this.rallyLine],\n            ]);\n            elementMap.forEach((element, index) => {\n                if (element) {\n                    element.visible = selectionLevel >= SELECTION_LEVEL_MAP[index];\n                }\n            });\n        }\n        this.lastOwner = gameObject.owner;\n        this.lastDebugTextEnabled = this.debugTextEnabled.value;\n        this.repairWrench?.update(deltaTime);\n    }\n    private updateFlyerHelper(selectionLevel: SelectionLevel, deltaTime: number): void {\n        if (this.flyHelper && this.gameObject.isUnit()) {\n            let shouldShow: boolean;\n            switch (this.flyerHelperOpt.value) {\n                case FlyerHelperMode.Never:\n                    shouldShow = false;\n                    break;\n                case FlyerHelperMode.Always:\n                    shouldShow = true;\n                    break;\n                case FlyerHelperMode.Selected:\n                    shouldShow = selectionLevel >= SelectionLevel.Selected;\n                    break;\n                default:\n                    shouldShow = false;\n            }\n            shouldShow = shouldShow && this.gameObject.zone === ZoneType.Air;\n            const flyHelperObj = this.flyHelper.get3DObject();\n            flyHelperObj.visible = shouldShow;\n            if (shouldShow) {\n                this.flyHelper.update(deltaTime);\n                const newY = -Coords.tileHeightToWorld(this.gameObject.tileElevation);\n                if (newY !== flyHelperObj.position.y) {\n                    flyHelperObj.position.y = newY;\n                    flyHelperObj.updateMatrix();\n                }\n            }\n        }\n    }\n    private updateBehindAnim(deltaTime: number): void {\n        if (this.behindAnim) {\n            const shouldShow = this.hiddenObjectsOpt.value &&\n                this.gameObject.isSpawned &&\n                this.gameObject.tile.occluded &&\n                this.gameObject.art.canBeHidden &&\n                this.gameObject.zone !== ZoneType.Air;\n            if (shouldShow) {\n                this.behindAnim.update(deltaTime);\n                if (!this.behindAnim.get3DObject()?.parent) {\n                    this.behindAnim.create3DObject();\n                    this.rootObj!.add(this.behindAnim.get3DObject());\n                    this.behindAnim.get3DObject().updateMatrix();\n                }\n            }\n            else if (this.behindAnim.get3DObject()?.parent) {\n                this.rootObj!.remove(this.behindAnim.get3DObject());\n            }\n        }\n    }\n    private updateDebugLabel(): void {\n        const ownerColorHex = this.gameObject.owner.color.asHex();\n        if (this.gameObject.debugLabel !== this.lastDebugLabel ||\n            this.gameObject.owner !== this.lastOwner ||\n            ownerColorHex !== this.lastDebugLabelOwnerColorHex ||\n            this.debugTextEnabled.value !== this.lastDebugTextEnabled) {\n            this.lastDebugLabel = this.gameObject.debugLabel;\n            this.lastDebugLabelOwnerColorHex = ownerColorHex;\n            if (this.debugLabel) {\n                this.rootObj!.remove(this.debugLabel.get3DObject());\n                this.debugLabel.dispose();\n                this.debugLabel = undefined;\n            }\n            if (this.gameObject.debugLabel && this.debugTextEnabled.value) {\n                const debugLabel = new DebugLabel(this.gameObject.debugLabel, this.gameObject.owner.color.asHex(), this.camera);\n                this.debugLabel = debugLabel;\n                debugLabel.create3DObject();\n                debugLabel.get3DObject().renderOrder = 999999;\n                this.rootObj!.add(debugLabel.get3DObject());\n            }\n        }\n    }\n    private updateRepairWrenchSprite(enabled: boolean): void {\n        if (this.repairWrench) {\n            this.rootObj!.remove(this.repairWrench.get3DObject());\n        }\n        if (enabled) {\n            this.repairWrench = this.createRepairWrench();\n            if (this.repairWrench) {\n                this.repairWrench.create3DObject();\n                this.rootObj!.add(this.repairWrench.get3DObject());\n            }\n        }\n    }\n    private updateVeteranIndicatorSprite(gameObject: GameObject): void {\n        if (this.veteranIndicator) {\n            this.rootObj!.remove(this.veteranIndicator);\n        }\n        this.veteranIndicator = this.createVeteranIndicator(gameObject);\n        if (this.veteranIndicator) {\n            this.rootObj!.add(this.veteranIndicator);\n            const offset = Coords.screenDistanceToWorld(Math.floor(PipOverlay.pipBrdFile.getImage(gameObject.isInfantry() ? 1 : 0).width / 2) - Math.floor(PipOverlay.pipsFile.getImage(13).width / 2), 0);\n            this.veteranIndicator.position.x = offset.x;\n            this.veteranIndicator.position.y = 0;\n            this.veteranIndicator.position.z = offset.y;\n            this.veteranIndicator.updateMatrix();\n        }\n    }\n    private updateRallyPointLine(rallyPoint: any, rallyLine: RallyPointFx): void {\n        rallyLine.visible = false;\n        if (rallyPoint && !this.objectIsOpaqueToViewer()) {\n            rallyLine.sourcePos = this.gameObject.position.worldPosition;\n            rallyLine.targetPos = Coords.tile3dToWorld(rallyPoint.rx + 0.5, rallyPoint.ry + 0.5, rallyPoint.z);\n            rallyLine.color = new THREE.Color(this.gameObject.owner.color.asHex());\n            rallyLine.needsUpdate = true;\n            rallyLine.visible = true;\n        }\n    }\n    private updatePrimaryFactorySprite(enabled: boolean): void {\n        if (this.primaryFactorySprite) {\n            this.rootObj!.remove(this.primaryFactorySprite);\n        }\n        if (enabled) {\n            const sprite = this.createPrimaryFactorySprite();\n            if (sprite) {\n                this.primaryFactorySprite = sprite;\n                this.rootObj!.add(sprite);\n            }\n        }\n    }\n    private updateControlGroupSprite(groupNumber: number | undefined): void {\n        if (this.controlGroupSprite) {\n            this.rootObj!.remove(this.controlGroupSprite);\n        }\n        if (groupNumber !== undefined) {\n            const sprite = this.createControlGroupSprite(groupNumber);\n            this.controlGroupSprite = sprite;\n            const gameObject = this.gameObject;\n            if (gameObject.isBuilding()) {\n                sprite.position.x = 1;\n                sprite.position.y = Coords.tileHeightToWorld(gameObject.art.height - 0.5);\n                sprite.position.z = Coords.getWorldTileSize() * gameObject.art.foundation.height;\n            }\n            else if (gameObject.isInfantry()) {\n                const offset = Coords.screenDistanceToWorld(-(CONTROL_GROUP_SIZE.width + 2 * BORDER_WIDTH +\n                    PipOverlay.pipBrdFile.getImage(1).width / 2 + 1), -PipOverlay.pipBrdFile.height / 2);\n                sprite.position.x = offset.x;\n                sprite.position.y = this.healthBar!.position.y;\n                sprite.position.z = offset.y;\n            }\n            else {\n                const offset = Coords.screenDistanceToWorld(-PipOverlay.pipBrdFile.getImage(0).width / 2, PipOverlay.pipBrdFile.height / 2);\n                sprite.position.x = offset.x;\n                sprite.position.y = this.healthBar!.position.y;\n                sprite.position.z = offset.y;\n            }\n            sprite.updateMatrix();\n            this.rootObj!.add(sprite);\n        }\n    }\n    private updatePipsSprite(): void {\n        if (this.pipsSprite) {\n            this.rootObj!.remove(this.pipsSprite);\n            this.pipsSprite = undefined;\n        }\n        const gameObject = this.gameObject;\n        let sprite: THREE.Mesh | undefined;\n        if (gameObject.isBuilding()) {\n            sprite = this.createBuildingOccupationInfo(gameObject);\n        }\n        else if (gameObject.isVehicle()) {\n            const pipColors: PipColor[] = [];\n            let totalSlots: number | undefined;\n            if (gameObject.harvesterTrait && gameObject.rules.storage! > 0) {\n                totalSlots = 5;\n                const storage = gameObject.rules.storage!;\n                const gemPips = Math.floor((gameObject.harvesterTrait.gems / storage) * totalSlots);\n                const orePips = Math.floor((gameObject.harvesterTrait.ore / storage) * totalSlots);\n                pipColors.push(...new Array(gemPips).fill(PipColor.Blue), ...new Array(orePips).fill(PipColor.Yellow));\n            }\n            else if (gameObject.transportTrait && gameObject.rules.passengers! > 0) {\n                totalSlots = gameObject.rules.passengers;\n                gameObject.transportTrait.units.forEach(unit => {\n                    let vehiclePips = 0;\n                    if (unit.isVehicle()) {\n                        pipColors.push(PipColor.Blue);\n                        vehiclePips++;\n                    }\n                    pipColors.push(...new Array(unit.rules.size - vehiclePips).fill(unit.isVehicle() ? PipColor.Red : unit.rules.pip));\n                });\n            }\n            else if (gameObject.airSpawnTrait) {\n                pipColors.push(...new Array(gameObject.airSpawnTrait.availableSpawns).fill(PipColor.Yellow));\n                totalSlots = gameObject.rules.spawnsNumber;\n            }\n            if (totalSlots) {\n                sprite = this.createPipsSprite(pipColors, totalSlots);\n            }\n        }\n        else if (gameObject.isAircraft() &&\n            gameObject.ammo &&\n            gameObject.name !== this.paradropRules.paradropPlane &&\n            !gameObject.rules.missileSpawn) {\n            sprite = this.createPipsSprite(new Array(gameObject.ammo).fill(PipColor.Green), gameObject.ammo, true);\n        }\n        if (sprite) {\n            sprite.updateMatrix();\n            this.rootObj!.add(sprite);\n            this.pipsSprite = sprite;\n        }\n    }\n    private computePipsDataKey(gameObject: GameObject): any {\n        if (gameObject.isBuilding()) {\n            return gameObject.garrisonTrait?.units.length;\n        }\n        else if (gameObject.isVehicle()) {\n            if (gameObject.harvesterTrait) {\n                return `${gameObject.harvesterTrait.ore}_${gameObject.harvesterTrait.gems}`;\n            }\n            else if (gameObject.transportTrait) {\n                return gameObject.transportTrait.units.length;\n            }\n            else if (gameObject.airSpawnTrait) {\n                return gameObject.airSpawnTrait.availableSpawns;\n            }\n        }\n        else if (gameObject.isAircraft()) {\n            return gameObject.ammo;\n        }\n        return undefined;\n    }\n    private updateHealthBarSprite(selectionLevel: SelectionLevel): void {\n        if (this.healthBar) {\n            this.rootObj!.remove(this.healthBar);\n            if (this.gameObject.isBuilding()) {\n                this.healthBar = this.createBuildingHealthBar(this.gameObject);\n            }\n            else {\n                const { healthBarWrapper, selectionBox } = this.createUnitHealthBar(this.gameObject);\n                this.healthBar = healthBarWrapper;\n                this.selectionBox = selectionBox;\n                selectionBox.visible = selectionLevel >= SELECTION_LEVEL_MAP[2];\n            }\n            this.rootObj!.add(this.healthBar);\n        }\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.rootObj;\n    }\n    dispose(): void {\n        this.disposables.dispose();\n        this.repairWrench?.dispose();\n        this.flyHelper?.dispose();\n        this.behindAnim?.dispose();\n        this.debugLabel?.dispose();\n        this.animFactory = undefined as any;\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/Projectile.ts",
    "content": "import { WithPosition } from \"@/engine/renderable/WithPosition\";\nimport { ShpRenderable } from \"@/engine/renderable/ShpRenderable\";\nimport { Projectile as GameProjectile, ProjectileState } from \"@/game/gameobject/Projectile\";\nimport { Coords } from \"@/game/Coords\";\nimport { LaserFx } from \"@/engine/renderable/fx/LaserFx\";\nimport { WeaponType } from \"@/game/WeaponType\";\nimport { TeslaFx } from \"@/engine/renderable/fx/TeslaFx\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nimport { LineTrailFx } from \"@/engine/renderable/fx/LineTrailFx\";\nimport { SparkFx } from \"@/engine/renderable/fx/SparkFx\";\nimport { RadBeamFx } from \"@/engine/renderable/fx/RadBeamFx\";\nimport { BlobShadow } from \"@/engine/renderable/entity/unit/BlobShadow\";\nimport { NukeLightingFx } from \"@/engine/gfx/lighting/NukeLightingFx\";\nimport { BatchedMesh } from \"@/engine/gfx/batch/BatchedMesh\";\nimport { ObjectRules } from \"@/game/rules/ObjectRules\";\nimport { quaternionFromVec3 } from \"@/game/math/geometry\";\nimport { PaletteType } from \"@/engine/type/PaletteType\";\nimport * as THREE from \"three\";\nexport class Projectile {\n    private static sonicWaveGeometry?: THREE.PlaneGeometry;\n    private static sonicWaveMaterial?: THREE.MeshBasicMaterial;\n    public gameObject: any;\n    public rules: any;\n    public imageFinder: any;\n    public voxels: any;\n    public voxelAnims: any;\n    public theater: any;\n    public palette: any;\n    public specialPalette: any;\n    public camera: any;\n    public gameSpeed: any;\n    public lighting: any;\n    public lightingDirector: any;\n    public vxlBuilderFactory: any;\n    public useSpriteBatching: boolean;\n    public useMeshInstancing: boolean;\n    public plugins: any[] = [];\n    public objectArt: any;\n    public label: string;\n    public withPosition: WithPosition;\n    public extraLight: THREE.Vector3;\n    public paletteRemaps: any[];\n    public target?: THREE.Object3D;\n    public blobShadow?: BlobShadow;\n    public vxlRotWrapper?: THREE.Object3D;\n    public lastDirection?: number;\n    public shpRenderable?: ShpRenderable;\n    public sonicWaveMesh?: THREE.Mesh | BatchedMesh;\n    public lastState?: any;\n    public renderableManager?: any;\n    public vxlBuilder?: any;\n    public lineTrailFx?: LineTrailFx;\n    constructor(gameObject: any, rules: any, imageFinder: any, voxels: any, voxelAnims: any, theater: any, palette: any, specialPalette: any, camera: any, gameSpeed: any, lighting: any, lightingDirector: any, vxlBuilderFactory: any, useSpriteBatching: boolean, useMeshInstancing: boolean) {\n        this.gameObject = gameObject;\n        this.rules = rules;\n        this.imageFinder = imageFinder;\n        this.voxels = voxels;\n        this.voxelAnims = voxelAnims;\n        this.theater = theater;\n        this.palette = palette;\n        this.specialPalette = specialPalette;\n        this.camera = camera;\n        this.gameSpeed = gameSpeed;\n        this.lighting = lighting;\n        this.lightingDirector = lightingDirector;\n        this.vxlBuilderFactory = vxlBuilderFactory;\n        this.useSpriteBatching = useSpriteBatching;\n        this.useMeshInstancing = useMeshInstancing;\n        this.plugins = [];\n        this.objectArt = gameObject.art;\n        this.label = \"projectile_\" + gameObject.rules.name;\n        this.withPosition = new WithPosition();\n        this.extraLight = new THREE.Vector3();\n        this.updateLighting();\n        if (this.gameObject.rules.firersPalette) {\n            const paletteType = this.gameObject.fromObject?.art.paletteType ?? PaletteType.Unit;\n            const customPaletteName = this.gameObject.fromObject?.art.customPaletteName;\n            this.palette = this.theater.getPalette(paletteType, customPaletteName);\n            if (this.gameObject.art.remapable) {\n                this.palette = this.palette.clone();\n                this.palette.remap(this.gameObject.fromPlayer.color);\n            }\n        }\n        if (this.gameObject.rules.firersPalette && this.objectArt.remapable) {\n            this.paletteRemaps = [...this.rules.colors.values()].map((color: any) => this.palette.clone().remap(color));\n        }\n        else {\n            this.paletteRemaps = [this.palette];\n        }\n    }\n    registerPlugin(plugin: any): void {\n        this.plugins.push(plugin);\n    }\n    updateLighting(): void {\n        this.plugins.forEach((plugin) => plugin.updateLighting?.());\n        if (this.objectArt.isVoxel) {\n            this.extraLight.setScalar(Math.PI * 1.5 + this.lighting.computeNoAmbient(this.objectArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation));\n        }\n        else {\n            this.extraLight\n                .copy(this.lighting.compute(this.objectArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation))\n                .addScalar(-1);\n        }\n    }\n    getIntersectTarget(): any { }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    create3DObject(): void {\n        let obj = this.get3DObject();\n        if (!obj) {\n            obj = new THREE.Object3D();\n            obj.name = this.label;\n            this.target = obj;\n            obj.matrixAutoUpdate = false;\n            this.withPosition.matrixUpdate = true;\n            this.withPosition.applyTo(this);\n            this.createObjects(obj);\n        }\n    }\n    setPosition(position: {\n        x: number;\n        y: number;\n        z: number;\n    }): void {\n        this.withPosition.setPosition(position.x, position.y, position.z);\n    }\n    getPosition(): THREE.Vector3 {\n        return this.withPosition.getPosition();\n    }\n    update(time: number, deltaTime: number): void {\n        this.plugins.forEach((plugin) => plugin.update(time));\n        if (deltaTime > 0 && !this.gameObject.isDestroyed) {\n            const velocity = this.gameObject.velocity.clone();\n            const movement = velocity.multiplyScalar(deltaTime);\n            const newPosition = movement.add(this.gameObject.position.worldPosition);\n            this.setPosition(newPosition);\n        }\n        this.blobShadow?.update(time, deltaTime);\n        const direction = this.gameObject.direction;\n        if (!this.vxlRotWrapper &&\n            this.lastDirection !== undefined &&\n            this.lastDirection === direction) {\n        }\n        else {\n            if (this.shpRenderable && this.shpRenderable.frameCount > 2) {\n                this.lastDirection = direction;\n                this.updateShapeFrame(direction);\n            }\n            else if (this.vxlRotWrapper) {\n                const quaternion = quaternionFromVec3(this.gameObject.velocity.clone().negate());\n                this.vxlRotWrapper.rotation.setFromQuaternion(quaternion, \"YXZ\");\n                if (this.gameObject.rules.vertical) {\n                    this.vxlRotWrapper.rotation.y = THREE.MathUtils.degToRad(180 + direction);\n                }\n                this.vxlRotWrapper.updateMatrix();\n            }\n            else if (this.sonicWaveMesh) {\n                this.sonicWaveMesh.rotation.y = THREE.MathUtils.degToRad(direction);\n                this.sonicWaveMesh.updateMatrix();\n            }\n        }\n        if (this.gameObject.state !== this.lastState) {\n            this.lastState = this.gameObject.state;\n            if (this.gameObject.state === ProjectileState.Impact) {\n                this.target!.visible = false;\n                this.renderableManager.createTransientAnim(this.gameObject.impactAnim, (anim: any) => {\n                    anim.setPosition(this.withPosition.getPosition());\n                });\n                if (this.gameObject.isNuke) {\n                    this.lightingDirector.addEffect(new NukeLightingFx());\n                }\n            }\n        }\n    }\n    updateShapeFrame(direction: number): void {\n        let frame = 0;\n        if (this.objectArt.rotates) {\n            frame = Math.round((((direction - 45 + 360) % 360) / 360) * 32) % 32;\n        }\n        this.shpRenderable!.setFrame(frame);\n    }\n    createObjects(parent: THREE.Object3D): void {\n        if (this.gameObject.fromWeapon.rules.isSonic) {\n            if (!Projectile.sonicWaveGeometry) {\n                Projectile.sonicWaveGeometry = this.createSonicWaveGeometry();\n            }\n            if (!Projectile.sonicWaveMaterial) {\n                Projectile.sonicWaveMaterial = new THREE.MeshBasicMaterial({\n                    color: 0xbcbc,\n                    blending: THREE.CustomBlending,\n                    blendEquation: THREE.AddEquation,\n                    blendSrc: THREE.DstColorFactor,\n                    blendDst: THREE.OneFactor,\n                    transparent: true,\n                    opacity: 0.25,\n                    alphaTest: 0.01,\n                    depthTest: false,\n                    depthWrite: false,\n                });\n            }\n            const mesh = new (this.useMeshInstancing ? BatchedMesh : THREE.Mesh)(Projectile.sonicWaveGeometry, Projectile.sonicWaveMaterial);\n            mesh.rotation.order = \"YXZ\";\n            mesh.rotation.x = -Math.PI / 2;\n            mesh.rotation.y = THREE.MathUtils.degToRad(this.gameObject.direction);\n            mesh.updateMatrix();\n            mesh.matrixAutoUpdate = false;\n            parent.add(mesh);\n            this.sonicWaveMesh = mesh;\n            return;\n        }\n        if (!this.gameObject.rules.inviso &&\n            this.gameObject.rules.imageName !== ObjectRules.IMAGE_NONE) {\n            if (this.gameObject.art.isVoxel) {\n                const imageName = this.objectArt.imageName.toLowerCase();\n                const vxlFile = imageName + \".vxl\";\n                const vxlData = this.voxels.get(vxlFile);\n                if (!vxlData) {\n                    throw new Error(`VXL missing for projectile ${this.gameObject.rules.name}. Vxl file ${vxlFile} not found.`);\n                }\n                const hvaData = this.objectArt.noHva\n                    ? undefined\n                    : this.voxelAnims.get(imageName + \".hva\");\n                const builder = this.vxlBuilder = this.vxlBuilderFactory.create(vxlData, hvaData, this.paletteRemaps, this.palette);\n                builder.setExtraLight(this.extraLight);\n                const vxlObject = builder.build();\n                const rotWrapper = this.vxlRotWrapper = new THREE.Object3D();\n                rotWrapper.rotation.order = \"YXZ\";\n                rotWrapper.matrixAutoUpdate = false;\n                rotWrapper.add(vxlObject);\n                parent.add(rotWrapper);\n            }\n            else {\n                const imageData = this.imageFinder.findByObjectArt(this.objectArt);\n                const drawOffset = this.objectArt.getDrawOffset();\n                const isArcing = this.gameObject.rules.arcing;\n                const hasShadow = this.gameObject.rules.shadow && !isArcing && imageData.numImages > 1;\n                const renderable = ShpRenderable.factory(imageData, this.palette, this.camera, drawOffset, hasShadow);\n                renderable.setBatched(this.useSpriteBatching);\n                if (this.useSpriteBatching) {\n                    renderable.setBatchPalettes(this.paletteRemaps);\n                }\n                renderable.setExtraLight(this.extraLight);\n                renderable.create3DObject();\n                this.shpRenderable = renderable;\n                parent.add(renderable.get3DObject());\n                if (isArcing) {\n                    this.blobShadow = new BlobShadow(this.gameObject, 1.5, this.useMeshInstancing);\n                    this.blobShadow.create3DObject();\n                    parent.add(this.blobShadow.get3DObject());\n                }\n            }\n            if (this.gameObject.fromWeapon.type === WeaponType.DeathWeapon) {\n                parent.visible = false;\n            }\n        }\n    }\n    createSonicWaveGeometry(): THREE.PlaneGeometry {\n        const geometry = new THREE.PlaneGeometry(Coords.LEPTONS_PER_TILE, Coords.LEPTONS_PER_TILE / 3, 10, 10);\n        const positionAttribute = geometry.getAttribute(\"position\") as THREE.BufferAttribute;\n        for (let i = 0; i < positionAttribute.count; i++) {\n            const x = positionAttribute.getX(i);\n            const y = positionAttribute.getY(i);\n            const newY = y + Math.cos((x * Math.PI) / Coords.LEPTONS_PER_TILE) * Coords.ISO_WORLD_SCALE;\n            positionAttribute.setY(i, newY);\n        }\n        return geometry;\n    }\n    onCreate(renderableManager: any): void {\n        this.renderableManager = renderableManager;\n        this.plugins.forEach((plugin) => plugin.onCreate(renderableManager));\n        const isPrismSecondary = this.gameObject.fromObject?.name === this.rules.general.prism.type &&\n            this.gameObject.fromWeapon.type === WeaponType.Secondary;\n        let fireOffset: number[];\n        if (this.gameObject.fromObject) {\n            if (this.gameObject.fromWeapon.type === WeaponType.Primary ||\n                this.gameObject.fromWeapon.type === WeaponType.DeathWeapon ||\n                isPrismSecondary) {\n                fireOffset = this.gameObject.fromObject.art.primaryFirePixelOffset;\n            }\n            else {\n                fireOffset = this.gameObject.fromObject.art.secondaryFirePixelOffset;\n            }\n        }\n        else {\n            fireOffset = [];\n        }\n        const weaponRules = this.gameObject.fromWeapon.rules;\n        if (this.gameObject.fromWeapon.type !== WeaponType.DeathWeapon &&\n            !weaponRules.limboLaunch) {\n            const animList = this.gameObject.fromWeapon.rules.anim;\n            let animName: string | undefined;\n            if (animList.length) {\n                if (animList.length === 1) {\n                    animName = animList[0];\n                }\n                else {\n                    const direction = this.gameObject.direction;\n                    const index = Math.round((((45 - direction + 360) % 360) / 360) * 8) % 8;\n                    animName = animList[index];\n                }\n            }\n            else if (this.gameObject.fromWeapon.warhead.rules.nukeMaker) {\n                animName = this.rules.audioVisual.nukeTakeOff;\n            }\n            if (animName) {\n                renderableManager.createTransientAnim(animName, (anim: any) => {\n                    anim.setPosition(this.gameObject.position.worldPosition);\n                    if (fireOffset.length) {\n                        anim.extraOffset = { x: fireOffset[0], y: -fireOffset[1] / 2 };\n                    }\n                });\n            }\n        }\n        if (weaponRules.isLaser) {\n            const startPos = this.gameObject.position.worldPosition.clone();\n            const offsetVector = new THREE.Vector3();\n            if (fireOffset.length) {\n                const screenDistance = Coords.screenDistanceToWorld(fireOffset[0], 0);\n                offsetVector.x = 4 * screenDistance.x;\n                offsetVector.z = 4 * screenDistance.y;\n                offsetVector.y = 4 * Coords.tileHeightToWorld(-fireOffset[1] / (Coords.ISO_TILE_SIZE / 2));\n            }\n            const endPos = this.gameObject.target.getWorldCoords().clone();\n            if (this.gameObject.fromObject?.name === this.rules.general.prism.type &&\n                this.gameObject.fromWeapon.type === WeaponType.Secondary) {\n                offsetVector.y += this.gameObject.fromObject.art.primaryFireFlh.vertical;\n                endPos.add(offsetVector);\n            }\n            startPos.add(offsetVector);\n            const color = new THREE.Color(weaponRules.isHouseColor\n                ? this.gameObject.fromPlayer.color.asHex()\n                : 0xff0000);\n            const duration = weaponRules.laserDuration /\n                GameSpeed.BASE_TICKS_PER_SECOND /\n                this.gameSpeed.value;\n            const thickness = 2 * (this.gameObject.baseDamageMultiplier > 1 ? 2 : 1);\n            const laserFx = new LaserFx(this.camera, startPos, endPos, color, duration, thickness);\n            renderableManager.addEffect(laserFx);\n        }\n        if (weaponRules.isElectricBolt) {\n            const startPos = this.gameObject.position.worldPosition.clone();\n            if (this.gameObject.fromObject?.isBuilding()) {\n                startPos.y += Coords.tileHeightToWorld(1);\n            }\n            const endPos = this.gameObject.target.getWorldCoords();\n            const palette = this.specialPalette;\n            const innerColor = new THREE.Color(palette.getColorAsHex(weaponRules.isAlternateColor ? 5 : 10));\n            const outerColor = new THREE.Color(palette.getColorAsHex(15));\n            const duration = 1 / this.gameSpeed.value;\n            const teslaFx = new TeslaFx(startPos, endPos, innerColor, outerColor, duration);\n            renderableManager.addEffect(teslaFx);\n        }\n        if (weaponRules.isRadBeam) {\n            const startPos = this.gameObject.position.worldPosition.clone();\n            const endPos = this.gameObject.target.getWorldCoords().clone();\n            const color = this.gameObject.fromWeapon.warhead.rules.temporal\n                ? new THREE.Color(...this.rules.audioVisual.chronoBeamColor.map((c: number) => c / 255))\n                : new THREE.Color(...this.rules.radiation.radColor.map((c: number) => c / 255));\n            const duration = 1 / this.gameSpeed.value;\n            const radBeamFx = new RadBeamFx(this.camera, startPos, endPos, color, duration, 1);\n            renderableManager.addEffect(radBeamFx);\n        }\n        if (this.objectArt.useLineTrail) {\n            const color = new THREE.Color().fromArray(this.objectArt.lineTrailColor.map((c: number) => c / 255));\n            const colorDecrement = this.objectArt.lineTrailColorDecrement;\n            const lineTrailFx = new LineTrailFx(() => this.target, color, colorDecrement, this.gameSpeed, this.camera);\n            renderableManager.addEffect(lineTrailFx);\n            this.lineTrailFx = lineTrailFx;\n        }\n        if (weaponRules.useSparkParticles) {\n            const position = this.gameObject.position.worldPosition.clone();\n            const duration = 20 / GameSpeed.BASE_TICKS_PER_SECOND;\n            const sparkFx = new SparkFx(position, new THREE.Color(1, 1, 1), duration, this.gameSpeed);\n            renderableManager.addEffect(sparkFx);\n        }\n    }\n    onRemove(renderableManager: any): void {\n        this.renderableManager = undefined;\n        this.plugins.forEach((plugin) => plugin.onRemove(renderableManager));\n        if (this.gameObject.overshootTiles) {\n            this.lineTrailFx?.stopTracking();\n        }\n        this.lineTrailFx?.requestFinishAndDispose();\n    }\n    dispose(): void {\n        this.plugins.forEach((plugin) => plugin.dispose());\n        this.shpRenderable?.dispose();\n        this.vxlBuilder?.dispose();\n        this.blobShadow?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/RenderableFactory.ts",
    "content": "import { Building } from \"@/engine/renderable/entity/Building\";\nimport { Vehicle } from \"@/engine/renderable/entity/Vehicle\";\nimport { Terrain } from \"@/engine/renderable/entity/Terrain\";\nimport { Overlay } from \"@/engine/renderable/entity/Overlay\";\nimport { Smudge } from \"@/engine/renderable/entity/Smudge\";\nimport { AnimationType } from \"@/engine/renderable/entity/building/AnimationType\";\nimport { Infantry } from \"@/engine/renderable/entity/Infantry\";\nimport { PipOverlay } from \"@/engine/renderable/entity/PipOverlay\";\nimport { Aircraft } from \"@/engine/renderable/entity/Aircraft\";\nimport { TransientAnim } from \"@/engine/renderable/entity/TransientAnim\";\nimport { Projectile } from \"@/engine/renderable/entity/Projectile\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { HarvesterPlugin } from \"@/engine/renderable/entity/plugin/HarvesterPlugin\";\nimport { Anim } from \"@/engine/renderable/entity/Anim\";\nimport { MoveSoundFxPlugin } from \"@/engine/renderable/entity/plugin/MoveSoundFxPlugin\";\nimport { VehicleDisguisePlugin } from \"@/engine/renderable/entity/plugin/VehicleDisguisePlugin\";\nimport { ChronoSparkleFxPlugin } from \"@/engine/renderable/entity/plugin/ChronoSparkleFxPlugin\";\nimport { TntFxPlugin } from \"@/engine/renderable/entity/plugin/TntFxPlugin\";\nimport { MindControlLinkPlugin } from \"@/engine/renderable/entity/plugin/MindControlLinkPlugin\";\nimport { InfantryDisguisePlugin } from \"@/engine/renderable/entity/plugin/InfantryDisguisePlugin\";\nimport { PsychicDetectPlugin } from \"@/engine/renderable/entity/building/PsychicDetectPlugin\";\nimport { TrailerSmokePlugin } from \"@/engine/renderable/entity/plugin/TrailerSmokePlugin\";\nimport { DamageSmokePlugin } from \"@/engine/renderable/entity/plugin/DamageSmokePlugin\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\nimport { ShipWakeTrailPlugin } from \"@/engine/renderable/entity/plugin/ShipWakeTrailPlugin\";\nimport { ObjectCloakPlugin } from \"@/engine/renderable/entity/plugin/ObjectCloakPlugin\";\nimport { Debris } from \"@/engine/renderable/entity/Debris\";\nimport { ShpAggregator } from \"@/engine/renderable/builder/ShpAggregator\";\ninterface Position {\n    x: number;\n    y: number;\n}\ninterface GameEntity {\n    art: {\n        paletteType: string;\n        customPaletteName?: string;\n    };\n    rules: {\n        moveSound?: string;\n        damageParticleSystems: any[];\n        locomotor: LocomotorType;\n    };\n    type: string;\n    mindControllerTrait?: any;\n    psychicDetectorTrait?: any;\n    harvesterTrait?: any;\n    disguiseTrait?: any;\n    tntChargeTrait?: any;\n    isAircraft(): boolean;\n    isProjectile(): boolean;\n    isDebris(): boolean;\n    isTechno(): boolean;\n    isUnit(): boolean;\n    isBuilding(): boolean;\n    isVehicle(): boolean;\n    isInfantry(): boolean;\n    isTerrain(): boolean;\n    isOverlay(): boolean;\n    isSmudge(): boolean;\n}\ninterface LocalPlayer {\n}\ninterface UnitSelection {\n    getOrCreateSelectionModel(entity: GameEntity): any;\n}\ninterface Alliances {\n}\ninterface Rules {\n    general: {\n        paradrop: any;\n    };\n    audioVisual: {\n        chronoSparkle1: any;\n    };\n    combatDamage: {\n        ivanIconFlickerRate: number;\n    };\n}\ninterface Art {\n    getObject(name: string, type: ObjectType): any;\n}\ninterface MapRenderable {\n    terrainLayer?: any;\n    overlayLayer?: any;\n    smudgeLayer?: any;\n}\ninterface ImageFinder {\n}\ninterface Palettes {\n    get(name: string): any;\n}\ninterface Voxels {\n}\ninterface VoxelAnims {\n}\ninterface Theater {\n    getPalette(paletteType: string, customPaletteName?: string): any;\n    animPalette: any;\n    isoPalette: any;\n}\ninterface Camera {\n}\ninterface Lighting {\n}\ninterface LightingDirector {\n}\ninterface DebugWireframes {\n}\ninterface DebugText {\n}\ninterface GameSpeed {\n}\ninterface WorldSound {\n}\ninterface Strings {\n}\ninterface FlyerHelperOpt {\n}\ninterface HiddenObjectsOpt {\n}\ninterface VxlBuilderFactory {\n}\ninterface BuildingImageDataCache {\n}\ninterface Plugin {\n}\ninterface RenderableEntity {\n    registerPlugin(plugin: Plugin): void;\n}\nexport class RenderableFactory {\n    private localPlayer: LocalPlayer;\n    private unitSelection: UnitSelection;\n    private alliances: Alliances;\n    private rules: Rules;\n    private art: Art;\n    private mapRenderable: MapRenderable | null;\n    private imageFinder: ImageFinder;\n    private palettes: Palettes;\n    private voxels: Voxels;\n    private voxelAnims: VoxelAnims;\n    private theater: Theater;\n    private camera: Camera;\n    private lighting: Lighting;\n    private lightingDirector: LightingDirector;\n    private debugWireframes: DebugWireframes;\n    private debugText: DebugText;\n    private gameSpeed: GameSpeed;\n    private worldSound: WorldSound | null;\n    private strings: Strings;\n    private flyerHelperOpt: FlyerHelperOpt;\n    private hiddenObjectsOpt: HiddenObjectsOpt;\n    private vxlBuilderFactory: VxlBuilderFactory;\n    private buildingImageDataCache: BuildingImageDataCache;\n    private useSpriteBatching: boolean;\n    private useMeshInstancing: boolean;\n    private bridgeImageCache: Map<any, any>;\n    constructor(localPlayer: LocalPlayer, unitSelection: UnitSelection, alliances: Alliances, rules: Rules, art: Art, mapRenderable: MapRenderable | null, imageFinder: ImageFinder, palettes: Palettes, voxels: Voxels, voxelAnims: VoxelAnims, theater: Theater, camera: Camera, lighting: Lighting, lightingDirector: LightingDirector, debugWireframes: DebugWireframes, debugText: DebugText, gameSpeed: GameSpeed, worldSound: WorldSound | null, strings: Strings, flyerHelperOpt: FlyerHelperOpt, hiddenObjectsOpt: HiddenObjectsOpt, vxlBuilderFactory: VxlBuilderFactory, buildingImageDataCache: BuildingImageDataCache, useSpriteBatching: boolean = false, useMeshInstancing: boolean = false) {\n        this.localPlayer = localPlayer;\n        this.unitSelection = unitSelection;\n        this.alliances = alliances;\n        this.rules = rules;\n        this.art = art;\n        this.mapRenderable = mapRenderable;\n        this.imageFinder = imageFinder;\n        this.palettes = palettes;\n        this.voxels = voxels;\n        this.voxelAnims = voxelAnims;\n        this.theater = theater;\n        this.camera = camera;\n        this.lighting = lighting;\n        this.lightingDirector = lightingDirector;\n        this.debugWireframes = debugWireframes;\n        this.debugText = debugText;\n        this.gameSpeed = gameSpeed;\n        this.worldSound = worldSound;\n        this.strings = strings;\n        this.flyerHelperOpt = flyerHelperOpt;\n        this.hiddenObjectsOpt = hiddenObjectsOpt;\n        this.vxlBuilderFactory = vxlBuilderFactory;\n        this.buildingImageDataCache = buildingImageDataCache;\n        this.useSpriteBatching = useSpriteBatching;\n        this.useMeshInstancing = useMeshInstancing;\n        this.bridgeImageCache = new Map();\n    }\n    createTransientAnim(name: string, callback?: any): TransientAnim {\n        const artObject = this.art.getObject(name, ObjectType.Animation);\n        return new TransientAnim(name, artObject, { x: 0, y: 0 }, this.imageFinder, this.theater, this.camera, this.debugWireframes, this.gameSpeed, this.useSpriteBatching, callback, this.worldSound);\n    }\n    createAnim(name: string): Anim {\n        const artObject = this.art.getObject(name, ObjectType.Animation);\n        return new Anim(name, artObject, { x: 0, y: 0 }, this.imageFinder as any, this.theater, this.camera, this.debugWireframes as any, this.gameSpeed as any, this.useSpriteBatching, undefined, this.worldSound as any);\n    }\n    create(entity: GameEntity): RenderableEntity {\n        let palette = this.theater.getPalette(entity.art.paletteType, entity.art.customPaletteName);\n        const plugins: Plugin[] = [];\n        if (entity.isAircraft() ||\n            entity.isProjectile() ||\n            entity.isDebris()) {\n            plugins.push(new TrailerSmokePlugin(entity, this.art, this.theater, this.imageFinder, this.gameSpeed));\n        }\n        if (entity.isTechno()) {\n            palette = palette.clone();\n            const selectionModel = this.unitSelection.getOrCreateSelectionModel(entity);\n            const pipOverlay = new PipOverlay(this.rules.general.paradrop, this.rules.audioVisual, entity as any, this.localPlayer, this.alliances, selectionModel, this.imageFinder, this.palettes.get(\"palette.pal\"), this.camera as any, this.strings as any, this.flyerHelperOpt as any, this.hiddenObjectsOpt as any, this.debugText as any, (name: string) => this.createAnim(name), this.useSpriteBatching, this.useMeshInstancing);\n            if (entity.isUnit()) {\n                const moveSound = entity.rules.moveSound;\n                if (moveSound && this.worldSound) {\n                    plugins.push(new MoveSoundFxPlugin(entity as any, moveSound, this.worldSound));\n                }\n            }\n            plugins.push(new ChronoSparkleFxPlugin(entity, this.rules.audioVisual.chronoSparkle1));\n            if (entity.mindControllerTrait) {\n                plugins.push(new MindControlLinkPlugin(entity, selectionModel, this.alliances, this.localPlayer as any));\n            }\n            let renderable: RenderableEntity;\n            if (entity.isBuilding()) {\n                const animPalette = this.theater.animPalette;\n                const isoPalette = this.theater.isoPalette;\n                renderable = new Building(entity, selectionModel, this.rules, this.art, this.imageFinder, this.theater, this.voxels, this.voxelAnims, palette, animPalette, isoPalette, this.camera, this.lighting, this.debugWireframes, this.gameSpeed, this.vxlBuilderFactory, this.useSpriteBatching, new ShpAggregator(), this.buildingImageDataCache, pipOverlay, this.worldSound, AnimationType.BUILDUP);\n                if (entity.psychicDetectorTrait) {\n                    plugins.push(new PsychicDetectPlugin(entity, entity.psychicDetectorTrait, this.localPlayer as any, this.camera as any));\n                }\n            }\n            else if (entity.isVehicle()) {\n                renderable = new Vehicle(entity, this.rules, this.art, this.imageFinder, this.theater, this.voxels, this.voxelAnims, palette, this.camera, this.lighting, this.debugWireframes, this.gameSpeed, selectionModel, this.vxlBuilderFactory, this.useSpriteBatching, pipOverlay, this.worldSound);\n                if (entity.rules.damageParticleSystems.length) {\n                    plugins.push(new DamageSmokePlugin(entity, this.art, this.theater, this.imageFinder, this.gameSpeed));\n                }\n                if (entity.rules.locomotor === LocomotorType.Ship ||\n                    entity.rules.locomotor === LocomotorType.Hover) {\n                    plugins.push(new ShipWakeTrailPlugin(entity, this.rules, this.art, this.theater, this.imageFinder, this.gameSpeed));\n                }\n                if (entity.harvesterTrait && this.mapRenderable) {\n                    plugins.push(new HarvesterPlugin(entity, entity.harvesterTrait));\n                }\n                if (entity.disguiseTrait) {\n                    plugins.push(new VehicleDisguisePlugin(entity, entity.disguiseTrait, this.localPlayer, this.alliances, renderable, this.art, this.imageFinder, this.theater, this.camera, this.lighting, this.gameSpeed, this.useSpriteBatching));\n                }\n            }\n            else if (entity.isInfantry()) {\n                renderable = new Infantry(entity, this.rules, this.art, this.imageFinder, this.theater, palette, this.camera, this.lighting, this.debugWireframes, this.gameSpeed, selectionModel, this.useSpriteBatching, this.useMeshInstancing, pipOverlay, this.worldSound);\n                if (entity.disguiseTrait) {\n                    plugins.push(new InfantryDisguisePlugin(entity, entity.disguiseTrait, this.localPlayer, this.alliances, renderable, this.art, this.gameSpeed));\n                }\n            }\n            else if (entity.isAircraft()) {\n                renderable = new Aircraft(entity as any, this.rules as any, this.voxels as any, this.voxelAnims as any, palette, this.lighting as any, this.debugWireframes as any, this.gameSpeed, selectionModel, this.vxlBuilderFactory as any, this.useSpriteBatching, pipOverlay);\n            }\n            else {\n                throw new Error(\"Unhandled game object type \" + entity.type);\n            }\n            if (entity.tntChargeTrait) {\n                plugins.push(new TntFxPlugin(entity as any, entity.tntChargeTrait, this.rules.combatDamage.ivanIconFlickerRate, renderable, this.imageFinder, this.art, this.alliances, this.localPlayer, this.worldSound, (name: string) => this.createAnim(name)));\n            }\n            plugins.push(new ObjectCloakPlugin(entity, this.localPlayer, this.alliances, renderable));\n            plugins.forEach((plugin) => renderable.registerPlugin(plugin));\n            return renderable;\n        }\n        if (entity.isTerrain()) {\n            return new Terrain(entity, this.mapRenderable?.terrainLayer, this.imageFinder as any, palette, this.camera, this.lighting, this.debugWireframes, this.gameSpeed, this.useSpriteBatching) as any;\n        }\n        if (entity.isOverlay()) {\n            return new Overlay(entity as any, this.rules as any, this.art, this.imageFinder as any, palette, this.camera, this.lighting as any, this.debugWireframes as any, this.bridgeImageCache, this.mapRenderable?.overlayLayer, this.useSpriteBatching) as any;\n        }\n        if (entity.isProjectile()) {\n            const projectile = new Projectile(entity, this.rules, this.imageFinder, this.voxels, this.voxelAnims, this.theater, palette, this.palettes.get(\"palette.pal\"), this.camera, this.gameSpeed, this.lighting, this.lightingDirector, this.vxlBuilderFactory, this.useSpriteBatching, this.useMeshInstancing);\n            plugins.forEach((plugin) => projectile.registerPlugin(plugin));\n            return projectile;\n        }\n        if (entity.isSmudge()) {\n            return new Smudge(entity, this.imageFinder as any, palette, this.camera, this.lighting, this.debugWireframes, this.mapRenderable?.smudgeLayer) as any;\n        }\n        if (entity.isDebris()) {\n            const debris = new Debris(entity as any, this.rules as any, this.imageFinder as any, this.voxels as any, this.voxelAnims, palette, this.camera, this.lighting as any, this.gameSpeed, this.vxlBuilderFactory as any, this.useSpriteBatching);\n            plugins.forEach((plugin) => debris.registerPlugin(plugin as any));\n            return debris;\n        }\n        throw new Error(\"Not implemented\");\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/Smudge.ts",
    "content": "import { ShpBuilder } from \"@/engine/renderable/builder/ShpBuilder\";\nimport { WithPosition } from \"@/engine/renderable/WithPosition\";\nimport { ImageFinder } from \"@/engine/ImageFinder\";\nimport { DebugUtils } from \"@/engine/gfx/DebugUtils\";\nimport { MapSpriteTranslation } from \"@/engine/renderable/MapSpriteTranslation\";\nimport { Coords } from \"@/game/Coords\";\nimport * as THREE from \"three\";\nexport class Smudge {\n    private gameObject: any;\n    private imageFinder: ImageFinder;\n    private palette: any;\n    private camera: any;\n    private lighting: any;\n    private debugFrame: any;\n    private mapSmudgeLayer: any;\n    private objectArt: any;\n    private label: string;\n    private withPosition: WithPosition;\n    private extraLight: THREE.Vector3;\n    private target?: THREE.Object3D;\n    private builder?: ShpBuilder;\n    constructor(gameObject: any, imageFinder: ImageFinder, palette: any, camera: any, lighting: any, debugFrame: any, mapSmudgeLayer: any) {\n        this.gameObject = gameObject;\n        this.imageFinder = imageFinder;\n        this.palette = palette;\n        this.camera = camera;\n        this.lighting = lighting;\n        this.debugFrame = debugFrame;\n        this.mapSmudgeLayer = mapSmudgeLayer;\n        this.objectArt = gameObject.art;\n        this.label = \"smudge_\" + gameObject.name;\n        this.init();\n    }\n    private init(): void {\n        this.withPosition = new WithPosition();\n        this.extraLight = new THREE.Vector3();\n        this.updateLighting();\n    }\n    private updateLighting(): void {\n        this.extraLight\n            .copy(this.lighting.compute(this.objectArt.lightingType, this.gameObject.tile))\n            .addScalar(-1);\n    }\n    public get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    public create3DObject(): void {\n        let obj = this.get3DObject();\n        if (!obj) {\n            obj = new THREE.Object3D();\n            obj.name = this.label;\n            this.target = obj;\n            obj.matrixAutoUpdate = false;\n            this.withPosition.matrixUpdate = true;\n            this.withPosition.applyTo(this);\n            this.createObjects(obj);\n        }\n    }\n    public update(delta: number): void { }\n    public setPosition(pos: THREE.Vector3): void {\n        this.withPosition.setPosition(pos.x, pos.y, pos.z);\n    }\n    public getPosition(): THREE.Vector3 {\n        return this.withPosition.getPosition();\n    }\n    private createObjects(parent: THREE.Object3D): void {\n        const size = { width: 1, height: 1 };\n        const container = new THREE.Object3D();\n        container.matrixAutoUpdate = false;\n        if (this.debugFrame.value) {\n            const wireframe = DebugUtils.createWireframe(size, 0);\n            parent.add(wireframe);\n        }\n        if (this.mapSmudgeLayer?.shouldBeBatched(this.gameObject)) {\n            this.mapSmudgeLayer.addObject(this.gameObject);\n        }\n        else {\n            try {\n                const image = this.imageFinder.findByObjectArt(this.objectArt);\n                const translation = new MapSpriteTranslation(size.width, size.height);\n                const { spriteOffset, anchorPointWorld } = translation.compute();\n                const offset = spriteOffset.clone().add(this.objectArt.getDrawOffset());\n                this.builder = new ShpBuilder(image, this.palette, this.camera, Coords.ISO_WORLD_SCALE);\n                this.builder.setOffset(offset);\n                this.builder.flat = this.objectArt.flat;\n                this.builder.setExtraLight(this.extraLight);\n                const mesh = this.builder.build();\n                container.add(mesh);\n                container.position.x = anchorPointWorld.x;\n                container.position.z = anchorPointWorld.y;\n                container.updateMatrix();\n                parent.add(container);\n            }\n            catch (error) {\n                if (error instanceof ImageFinder.MissingImageError) {\n                    console.warn(error.message);\n                    return;\n                }\n                throw error;\n            }\n        }\n    }\n    public onRemove(): void {\n        if (this.mapSmudgeLayer?.hasObject(this.gameObject)) {\n            this.mapSmudgeLayer.removeObject(this.gameObject);\n        }\n    }\n    public dispose(): void {\n        this.builder?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/TargetLines.ts",
    "content": "import * as THREE from \"three\";\nimport { Coords } from \"@/game/Coords\";\nimport { cloneConfig, configsAreEqual, configHasTarget } from \"@/game/gameobject/task/system/TargetLinesConfig\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\ninterface LineObjects {\n    root: THREE.Object3D;\n    line: THREE.Line;\n    srcLineHead: THREE.Mesh;\n    destLineHead: THREE.Mesh;\n}\nexport class TargetLines {\n    private obj?: THREE.Object3D;\n    private unitPaths = new Map<any, any>();\n    private unitLines = new Map<any, LineObjects>();\n    private lineHeadGeometry: THREE.PlaneGeometry;\n    private attackLineMaterial?: THREE.LineBasicMaterial;\n    private moveLineMaterial?: THREE.LineBasicMaterial;\n    private attackLineHeadMaterial?: THREE.MeshBasicMaterial;\n    private moveLineHeadMaterial?: THREE.MeshBasicMaterial;\n    private selectionHash?: string;\n    private showStart?: number;\n    constructor(private currentPlayer: any, private unitSelection: any, private camera: any, private debugPaths: any, private enabled: any) {\n        this.lineHeadGeometry = new THREE.PlaneGeometry(3 * Coords.ISO_WORLD_SCALE, 3 * Coords.ISO_WORLD_SCALE);\n    }\n    create3DObject(): void {\n        if (this.obj) {\n            return;\n        }\n        this.obj = new THREE.Object3D();\n        this.obj.name = \"target_lines\";\n        this.obj.matrixAutoUpdate = false;\n        this.attackLineMaterial = new THREE.LineBasicMaterial({\n            color: 0xad0000,\n            transparent: true,\n            depthTest: false,\n            depthWrite: false,\n        });\n        this.moveLineMaterial = new THREE.LineBasicMaterial({\n            color: 0x00aa00,\n            transparent: true,\n            depthTest: false,\n            depthWrite: false,\n        });\n        this.attackLineHeadMaterial = new THREE.MeshBasicMaterial({\n            color: 0xad0000,\n            transparent: true,\n            depthTest: false,\n            depthWrite: false,\n        });\n        this.moveLineHeadMaterial = new THREE.MeshBasicMaterial({\n            color: 0x00aa00,\n            transparent: true,\n            depthTest: false,\n            depthWrite: false,\n        });\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.obj;\n    }\n    forceShow(): void {\n        this.selectionHash = undefined;\n    }\n    update(now: number): void {\n        if (this.obj) {\n            this.obj.visible = this.enabled.value;\n        }\n        if (!this.enabled.value) {\n            return;\n        }\n        const selectionHash = this.unitSelection.getHash();\n        if (this.selectionHash === undefined || this.selectionHash !== selectionHash) {\n            this.selectionHash = selectionHash;\n            this.hideAllLines();\n            this.unitPaths.clear();\n            this.disposeUnitLines();\n            this.unitSelection.getSelectedUnits().forEach((unit: any) => {\n                if (!unit.isUnit() || (this.currentPlayer && unit.owner !== this.currentPlayer)) {\n                    return;\n                }\n                this.unitPaths.set(unit, cloneConfig(unit.unitOrderTrait.targetLinesConfig));\n                this.updateLines(unit);\n                if (unit.zone === ZoneType.Air ||\n                    configHasTarget(unit.unitOrderTrait.targetLinesConfig)) {\n                    this.showLines(unit, now);\n                }\n            });\n            return;\n        }\n        let pathsChanged = false;\n        this.unitSelection.getSelectedUnits().forEach((unit: any) => {\n            if (!unit.isUnit() || (this.currentPlayer && unit.owner !== this.currentPlayer)) {\n                return;\n            }\n            const targetLinesConfig = unit.unitOrderTrait.targetLinesConfig;\n            const previousConfig = this.unitPaths.get(unit);\n            const configChanged = !this.unitPaths.has(unit) ||\n                !configsAreEqual(previousConfig, targetLinesConfig) ||\n                !!targetLinesConfig?.isRecalc;\n            if (configChanged) {\n                this.unitPaths.set(unit, cloneConfig(targetLinesConfig));\n                pathsChanged = true;\n                this.updateLines(unit);\n                if (configHasTarget(targetLinesConfig)) {\n                    this.showLines(unit, now);\n                }\n            }\n            this.updateLineEndpoints(unit);\n        });\n        if (pathsChanged) {\n            return;\n        }\n        if (this.showStart !== undefined && now - this.showStart >= 1000) {\n            this.hideAllLines();\n        }\n    }\n    showLines(unit: any, now: number): void {\n        const lineObjects = this.unitLines.get(unit);\n        if (!lineObjects) {\n            return;\n        }\n        this.showStart = now;\n        lineObjects.root.visible = true;\n    }\n    hideAllLines(): void {\n        this.showStart = undefined;\n        this.unitLines.forEach((objects) => {\n            objects.root.visible = false;\n        });\n    }\n    updateLines(unit: any): void {\n        let config = unit.unitOrderTrait.targetLinesConfig;\n        if (!config || !configHasTarget(config)) {\n            if (unit.zone !== ZoneType.Air) {\n                const existing = this.unitLines.get(unit);\n                if (existing) {\n                    this.obj?.remove(existing.root);\n                    this.disposeLineObjects(existing);\n                    this.unitLines.delete(unit);\n                }\n                return;\n            }\n            config = {\n                pathNodes: [\n                    { tile: unit.tile, onBridge: undefined },\n                    { tile: unit.tile, onBridge: undefined },\n                ],\n            };\n        }\n        const positions: number[] = [];\n        let pathNodes = config.pathNodes;\n        if (pathNodes.length) {\n            if (!this.debugPaths.value) {\n                pathNodes = [pathNodes[0], pathNodes[pathNodes.length - 1]];\n            }\n            pathNodes.forEach((node: any) => {\n                const position = Coords.tile3dToWorld(node.tile.rx + 0.5, node.tile.ry + 0.5, node.tile.z + (node.onBridge?.tileElevation ?? 0));\n                positions.push(position.x, position.y, position.z);\n            });\n            positions.splice(positions.length - 3, 3, unit.position.worldPosition.x, unit.position.worldPosition.y, unit.position.worldPosition.z);\n        }\n        else {\n            const target = config.target;\n            positions.push(target.position.worldPosition.x, target.position.worldPosition.y, target.position.worldPosition.z, unit.position.worldPosition.x, unit.position.worldPosition.y, unit.position.worldPosition.z);\n        }\n        const geometry = new THREE.BufferGeometry();\n        geometry.setAttribute(\"position\", new THREE.Float32BufferAttribute(positions, 3));\n        geometry.computeBoundingSphere();\n        const isAttack = !!config.isAttack;\n        const line = new THREE.Line(geometry, isAttack ? this.attackLineMaterial! : this.moveLineMaterial!);\n        line.matrixAutoUpdate = false;\n        const srcLineHead = this.createLineHead(isAttack);\n        const destLineHead = this.createLineHead(isAttack);\n        this.syncLineHeadPositions(line, srcLineHead, destLineHead);\n        line.renderOrder = 1000000;\n        srcLineHead.renderOrder = 1000000;\n        destLineHead.renderOrder = 1000000;\n        const root = new THREE.Object3D();\n        root.matrixAutoUpdate = false;\n        root.visible = false;\n        root.add(line);\n        root.add(srcLineHead);\n        root.add(destLineHead);\n        const existing = this.unitLines.get(unit);\n        if (existing) {\n            this.obj?.remove(existing.root);\n            this.disposeLineObjects(existing);\n        }\n        this.unitLines.set(unit, {\n            root,\n            line,\n            srcLineHead,\n            destLineHead,\n        });\n        this.obj?.add(root);\n    }\n    createLineHead(isAttack: boolean): THREE.Mesh {\n        const lineHead = new THREE.Mesh(this.lineHeadGeometry, isAttack ? this.attackLineHeadMaterial! : this.moveLineHeadMaterial!);\n        const rotation = new THREE.Quaternion().setFromEuler(this.camera.rotation);\n        lineHead.setRotationFromQuaternion(rotation);\n        lineHead.matrixAutoUpdate = false;\n        return lineHead;\n    }\n    disposeUnitLines(): void {\n        this.unitLines.forEach((lineObjects) => {\n            this.disposeLineObjects(lineObjects);\n        });\n        this.unitLines.clear();\n    }\n    disposeLineObjects(lineObjects: LineObjects): void {\n        lineObjects.line.geometry.dispose();\n    }\n    dispose(): void {\n        this.disposeUnitLines();\n        this.attackLineMaterial?.dispose();\n        this.attackLineHeadMaterial?.dispose();\n        this.moveLineMaterial?.dispose();\n        this.moveLineHeadMaterial?.dispose();\n        this.lineHeadGeometry.dispose();\n    }\n    private updateLineEndpoints(unit: any): void {\n        const lineObjects = this.unitLines.get(unit);\n        if (!lineObjects) {\n            return;\n        }\n        const worldPosition = unit.position.worldPosition;\n        const srcChanged = !worldPosition.equals(lineObjects.srcLineHead.position);\n        const target = unit.unitOrderTrait.targetLinesConfig?.target;\n        const targetPosition = target?.position.worldPosition;\n        const destChanged = !!targetPosition && !targetPosition.equals(lineObjects.destLineHead.position);\n        if (!srcChanged && !destChanged) {\n            return;\n        }\n        const positions = lineObjects.line.geometry.getAttribute(\"position\") as THREE.BufferAttribute | undefined;\n        if (!positions || positions.count < 2) {\n            return;\n        }\n        if (srcChanged) {\n            const srcIndex = positions.count - 1;\n            positions.setXYZ(srcIndex, worldPosition.x, worldPosition.y, worldPosition.z);\n            lineObjects.srcLineHead.position.copy(worldPosition);\n            lineObjects.srcLineHead.updateMatrix();\n        }\n        if (targetPosition && destChanged) {\n            positions.setXYZ(0, targetPosition.x, targetPosition.y, targetPosition.z);\n            lineObjects.destLineHead.position.copy(targetPosition);\n            lineObjects.destLineHead.updateMatrix();\n        }\n        positions.needsUpdate = true;\n        lineObjects.line.geometry.computeBoundingSphere();\n    }\n    private syncLineHeadPositions(line: THREE.Line, srcLineHead: THREE.Mesh, destLineHead: THREE.Mesh): void {\n        const positions = line.geometry.getAttribute(\"position\") as THREE.BufferAttribute;\n        const srcIndex = positions.count - 1;\n        srcLineHead.position.set(positions.getX(srcIndex), positions.getY(srcIndex), positions.getZ(srcIndex));\n        srcLineHead.updateMatrix();\n        destLineHead.position.set(positions.getX(0), positions.getY(0), positions.getZ(0));\n        destLineHead.updateMatrix();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/Terrain.ts",
    "content": "import { WithPosition } from \"@/engine/renderable/WithPosition\";\nimport { ImageFinder } from \"@/engine/ImageFinder\";\nimport { DebugUtils } from \"@/engine/gfx/DebugUtils\";\nimport { MapSpriteTranslation } from \"@/engine/renderable/MapSpriteTranslation\";\nimport { ShpRenderable } from \"@/engine/renderable/ShpRenderable\";\nimport { TiberiumTreeTrait, SpawnStatus } from \"@/game/gameobject/trait/TiberiumTreeTrait\";\nimport { SimpleRunner } from \"@/engine/animation/SimpleRunner\";\nimport { AnimProps } from \"@/engine/AnimProps\";\nimport { Animation, AnimationState } from \"@/engine/Animation\";\nimport { IniSection } from \"@/data/IniSection\";\nimport { AlphaRenderable } from \"@/engine/renderable/AlphaRenderable\";\nimport * as THREE from \"three\";\nexport class Terrain {\n    private gameObject: any;\n    private terrainLayer: any;\n    private imageFinder: ImageFinder;\n    private palette: any;\n    private camera: any;\n    private lighting: any;\n    private debugFrame: any;\n    private gameSpeed: any;\n    private useSpriteBatching: boolean;\n    private objectArt: any;\n    private label: string;\n    private tiberiumTreeTrait?: any;\n    private withPosition: WithPosition;\n    private extraLight: THREE.Vector3;\n    private target?: THREE.Object3D;\n    private mainObj?: ShpRenderable;\n    private animationRunner?: SimpleRunner;\n    private lastTiberiumSpawnStatus?: any;\n    constructor(gameObject: any, terrainLayer: any, imageFinder: ImageFinder, palette: any, camera: any, lighting: any, debugFrame: any, gameSpeed: any, useSpriteBatching: boolean) {\n        this.gameObject = gameObject;\n        this.terrainLayer = terrainLayer;\n        this.imageFinder = imageFinder;\n        this.palette = palette;\n        this.camera = camera;\n        this.lighting = lighting;\n        this.debugFrame = debugFrame;\n        this.gameSpeed = gameSpeed;\n        this.useSpriteBatching = useSpriteBatching;\n        this.objectArt = gameObject.art;\n        this.label = \"terrain_\" + gameObject.rules.name;\n        this.init();\n    }\n    private init(): void {\n        this.tiberiumTreeTrait = this.gameObject.traits.find(TiberiumTreeTrait);\n        this.withPosition = new WithPosition();\n        this.extraLight = new THREE.Vector3();\n        this.updateLighting();\n    }\n    private updateLighting(): void {\n        this.extraLight\n            .copy(this.lighting.compute(this.objectArt.lightingType, this.gameObject.tile))\n            .addScalar(-1);\n    }\n    public get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    public create3DObject(): void {\n        let obj = this.get3DObject();\n        if (!obj) {\n            obj = new THREE.Object3D();\n            obj.name = this.label;\n            this.target = obj;\n            obj.matrixAutoUpdate = false;\n            this.withPosition.matrixUpdate = true;\n            this.withPosition.applyTo(this);\n            this.createObjects(obj);\n        }\n    }\n    public setPosition(pos: THREE.Vector3): void {\n        this.withPosition.setPosition(pos.x, pos.y, pos.z);\n    }\n    public getPosition(): THREE.Vector3 {\n        return this.withPosition.getPosition();\n    }\n    public update(delta: number): void {\n        if (this.tiberiumTreeTrait) {\n            const status = this.tiberiumTreeTrait.status;\n            if (status !== this.lastTiberiumSpawnStatus && status === SpawnStatus.Spawning) {\n                this.lastTiberiumSpawnStatus = status;\n                this.animationRunner?.animation.reset();\n            }\n            if (this.animationRunner) {\n                this.animationRunner.tick(delta);\n                if (this.animationRunner.animation.getState() !== AnimationState.STOPPED) {\n                    this.mainObj?.setFrame(this.animationRunner.getCurrentFrame());\n                }\n                else {\n                    this.mainObj?.setFrame(0);\n                }\n            }\n        }\n    }\n    private createObjects(parent: THREE.Object3D): void {\n        const size = { width: 1, height: 1 };\n        if (this.debugFrame.value) {\n            const wireframe = DebugUtils.createWireframe(size, 2);\n            parent.add(wireframe);\n        }\n        let image;\n        try {\n            image = this.imageFinder.findByObjectArt(this.objectArt);\n        }\n        catch (e) {\n            if (e instanceof ImageFinder.MissingImageError) {\n                console.warn(e.message);\n                return;\n            }\n            throw e;\n        }\n        const alphaImage = this.gameObject.rules.alphaImage;\n        if (alphaImage) {\n            const alphaTexture = this.imageFinder.tryFind(alphaImage, false);\n            if (alphaTexture) {\n                const alphaRenderable = new AlphaRenderable(alphaTexture, this.camera, this.objectArt.getDrawOffset());\n                alphaRenderable.create3DObject();\n                parent.add(alphaRenderable.get3DObject());\n            }\n            else {\n                console.warn(`<${this.gameObject.name}>: Alpha image \"${alphaImage}\" not found`);\n            }\n        }\n        if (this.terrainLayer?.shouldBeBatched(this.gameObject)) {\n            this.terrainLayer.addObject(this.gameObject);\n        }\n        else {\n            const obj = new THREE.Object3D();\n            obj.matrixAutoUpdate = false;\n            const translation = new MapSpriteTranslation(size.width, size.height);\n            const { spriteOffset, anchorPointWorld } = translation.compute();\n            obj.position.x = anchorPointWorld.x;\n            obj.position.z = anchorPointWorld.y;\n            obj.updateMatrix();\n            const offset = spriteOffset.clone().add(this.objectArt.getDrawOffset());\n            const shpRenderable = ShpRenderable.factory(image, this.palette, this.camera, offset, this.objectArt.hasShadow);\n            shpRenderable.setBatched(this.useSpriteBatching);\n            if (this.useSpriteBatching) {\n                shpRenderable.setBatchPalettes([this.palette]);\n            }\n            shpRenderable.setFrame(0);\n            shpRenderable.setExtraLight(this.extraLight);\n            shpRenderable.create3DObject();\n            obj.add(shpRenderable.get3DObject());\n            this.mainObj = shpRenderable;\n            if (this.tiberiumTreeTrait) {\n                const iniSection = new IniSection(\"dummy\");\n                if (this.gameObject.rules.animationRate) {\n                    iniSection.set(\"Rate\", \"\" + 60 * this.gameObject.rules.animationRate);\n                    iniSection.set(\"Shadow\", \"yes\");\n                }\n                const animProps = new AnimProps(iniSection, image);\n                const animation = new Animation(animProps, this.gameSpeed);\n                this.animationRunner = new SimpleRunner();\n                this.animationRunner.animation = animation;\n                animation.stop();\n            }\n            parent.add(obj);\n        }\n    }\n    public onRemove(): void {\n        if (this.terrainLayer?.hasObject(this.gameObject)) {\n            this.terrainLayer.removeObject(this.gameObject);\n        }\n    }\n    public dispose(): void {\n        this.mainObj?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/TransientAnim.ts",
    "content": "import { Anim } from './Anim';\nexport class TransientAnim extends Anim {\n    private container: any;\n    constructor(e: any, t: any, i: any, r: any, s: any, a: any, n: any, o: any, l: any, c: any, h: any) {\n        super(e, t, i, r, s, a, n, o, l, undefined, h);\n        this.container = c;\n    }\n    update(e: number): void {\n        if (this.isAnimNotStarted()) {\n            const report = this.objectArt.report;\n            if (report) {\n                this.worldSound?.playEffect(report, this.getPosition());\n            }\n        }\n        super.update(e);\n        if (this.isAnimFinished()) {\n            this.remove();\n            this.dispose();\n        }\n    }\n    remove(): void {\n        this.container.remove(this);\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/Vehicle.ts",
    "content": "import * as LiangBarsky from \"liang-barsky\";\nimport * as GameVehicle from \"@/game/gameobject/Vehicle\";\nimport * as Coords from \"@/game/Coords\";\nimport * as WithPosition from \"@/engine/renderable/WithPosition\";\nimport * as DebugUtils from \"@/engine/gfx/DebugUtils\";\nimport * as ShpRenderable from \"@/engine/renderable/ShpRenderable\";\nimport * as ImageFinder from \"@/engine/ImageFinder\";\nimport { MissingImageError } from \"@/engine/ImageFinder\";\nimport * as MapSpriteTranslation from \"@/engine/renderable/MapSpriteTranslation\";\nimport * as Animation from \"@/engine/Animation\";\nimport * as AnimProps from \"@/engine/AnimProps\";\nimport * as IniSection from \"@/data/IniSection\";\nimport * as SimpleRunner from \"@/engine/animation/SimpleRunner\";\nimport * as MathUtils from \"@/util/math\";\nimport * as ObjectType from \"@/engine/type/ObjectType\";\nimport * as SpeedType from \"@/game/type/SpeedType\";\nimport * as HighlightAnimRunner from \"@/engine/renderable/entity/HighlightAnimRunner\";\nimport * as VeteranLevel from \"@/game/gameobject/unit/VeteranLevel\";\nimport * as HarvesterTrait from \"@/game/gameobject/trait/HarvesterTrait\";\nimport * as DeathType from \"@/game/gameobject/common/DeathType\";\nimport * as FacingUtil from \"@/game/gameobject/unit/FacingUtil\";\nimport * as ZoneType from \"@/game/gameobject/unit/ZoneType\";\nimport * as InvulnerableAnimRunner from \"@/engine/renderable/entity/InvulnerableAnimRunner\";\nimport * as GameSpeed from \"@/game/GameSpeed\";\nimport * as BoxIntersectObject3D from \"@/engine/renderable/entity/BoxIntersectObject3D\";\nimport * as RotorHelper from \"@/engine/renderable/entity/unit/RotorHelper\";\nimport * as ExtraLightHelper from \"@/engine/renderable/entity/unit/ExtraLightHelper\";\nimport * as DebugRenderable from \"@/engine/renderable/DebugRenderable\";\nimport * as GfxMathUtils from \"@/engine/gfx/MathUtils\";\nimport * as THREE from \"three\";\nconst o = LiangBarsky;\nconst a = GameVehicle;\nconst S = Coords;\nconst y = WithPosition;\nconst n = DebugUtils;\nconst p = ShpRenderable;\nconst m = ImageFinder;\nconst f = MapSpriteTranslation;\nconst w = Animation;\nconst C = AnimProps;\nconst E = IniSection;\nconst x = SimpleRunner;\nconst h = MathUtils;\nconst i = ObjectType;\nconst s = SpeedType;\nconst T = HighlightAnimRunner;\nconst O = VeteranLevel;\nconst r = HarvesterTrait;\nconst l = DeathType;\nconst c = FacingUtil;\nconst M = ZoneType;\nconst v = InvulnerableAnimRunner;\nconst u = GameSpeed;\nconst d = BoxIntersectObject3D;\nconst g = RotorHelper;\nconst A = ExtraLightHelper;\nconst b = DebugRenderable;\nconst R = GfxMathUtils;\nlet P: number;\nlet I: any;\nlet k: any;\nP = Math.PI / 4;\nenum SquidGrabAnimType {\n    Grab = 0,\n    Shake1 = 1,\n    Shake2 = 2\n}\nI = SquidGrabAnimType;\ninterface GameObjectInterface {\n    id: string;\n    rules: any;\n    art: any;\n    owner: {\n        color: any;\n    };\n    tile: any;\n    tileElevation: number;\n    direction: number;\n    spinVelocity: number;\n    zone: any;\n    isMoving: boolean;\n    isFiring: boolean;\n    isDestroyed: boolean;\n    position: {\n        worldPosition: any;\n    };\n    moveTrait: {\n        velocity: any;\n    };\n    invulnerableTrait: {\n        isActive(): boolean;\n    };\n    warpedOutTrait: {\n        isActive(): boolean;\n    };\n    cloakableTrait?: {\n        isCloaked(): boolean;\n    };\n    submergibleTrait?: {\n        isSubmerged(): boolean;\n    };\n    rocking?: {\n        facing: number;\n        factor: number;\n    };\n    parasiteableTrait?: {\n        isInfested(): boolean;\n        getParasite(): {\n            rules: {\n                organic: boolean;\n            };\n            owner: {\n                color: any;\n            };\n        } | null;\n    };\n    tilterTrait?: {\n        tilt: {\n            yaw: number;\n            pitch: number;\n        };\n    };\n    turretTrait?: {\n        facing: number;\n    };\n    turretNo: number;\n    veteranLevel: any;\n    harvesterTrait?: any;\n    airSpawnTrait?: {\n        availableSpawns: number;\n    };\n    explodes: boolean;\n    deathType: any;\n    isSinker: boolean;\n    isVehicle(): boolean;\n    getUiName(): string;\n    name: string;\n}\nexport class Vehicle {\n    gameObject: GameObjectInterface;\n    rules: any;\n    imageFinder: any;\n    voxels: any;\n    voxelAnims: any;\n    palette: any;\n    camera: any;\n    lighting: any;\n    debugFrame: any;\n    gameSpeed: any;\n    selectionModel: any;\n    vxlBuilderFactory: any;\n    useSpriteBatching: boolean;\n    pipOverlay: any;\n    worldSound: any;\n    rotorSpeeds: number[] = [];\n    vxlBuilders: any[] = [];\n    highlightAnimRunner: any;\n    invulnAnimRunner: any;\n    plugins: any[] = [];\n    objectRules: any;\n    objectArt: any;\n    label: string;\n    paletteRemaps: any[];\n    lastOwnerColor: any;\n    baseVxlExtraLight: any;\n    baseShpExtraLight: any;\n    vxlExtraLight: any;\n    shpExtraLight: any;\n    withPosition: any;\n    target?: any;\n    posObj?: any;\n    tiltObj?: any;\n    dirWrapObj?: any;\n    mainObj?: any;\n    rockingTiltObj?: any;\n    bodyVxlBuilder?: any;\n    mainVxl?: any;\n    noSpawnAltVxl?: any;\n    harvesterAltVxl?: any;\n    turret?: any;\n    allTurrets?: any[];\n    barrel?: any;\n    rotors?: any[];\n    shpRenderable?: any;\n    placeholder?: any;\n    shpAnimRunner?: any;\n    chargeTurretRunner?: any;\n    currentTurretIdx: number = 0;\n    lastDirection?: number;\n    lastDirectionDelta?: number;\n    lastVeteranLevel?: any;\n    lastElevation?: number;\n    lastInvulnerable?: boolean;\n    lastWarpedOut?: boolean;\n    lastCloaked?: boolean;\n    lastSubmerged?: boolean;\n    lastRockingFacing?: number;\n    lastSquidGrabbed?: boolean;\n    lastTilt?: {\n        yaw: number;\n        pitch: number;\n    };\n    lastTurretFacing?: number;\n    lastMoving?: boolean;\n    lastFiring?: boolean;\n    destroyStartTime?: number;\n    sinkWakeAnims: any[] = [];\n    squidGrabAnim?: any;\n    rockingStartTime?: number;\n    rockingFactor?: number;\n    rockingPoint?: any;\n    rockingAxis?: any;\n    renderableManager?: any;\n    ambientSound?: any;\n    resolveObjectRemove?: () => void;\n    constructor(e, t, i, r, s, a, n, o, l, c, h, u, d, g, p, m, f) {\n        (this.gameObject = e),\n            (this.rules = t),\n            (this.imageFinder = r),\n            (this.voxels = a),\n            (this.voxelAnims = n),\n            (this.palette = o),\n            (this.camera = l),\n            (this.lighting = c),\n            (this.debugFrame = h),\n            (this.gameSpeed = u),\n            (this.selectionModel = d),\n            (this.vxlBuilderFactory = g),\n            (this.useSpriteBatching = p),\n            (this.pipOverlay = m),\n            (this.worldSound = f),\n            (this.rotorSpeeds = []),\n            (this.vxlBuilders = []),\n            (this.highlightAnimRunner = new T.HighlightAnimRunner(this.gameSpeed)),\n            (this.invulnAnimRunner = new v.InvulnerableAnimRunner(this.gameSpeed)),\n            (this.plugins = []),\n            (this.objectRules = e.rules),\n            (this.objectArt = e.art),\n            (this.label = \"vehicle_\" + this.objectRules.name),\n            (this.paletteRemaps = [...this.rules.colors.values()].map((e) => this.palette.clone().remap(e))),\n            this.palette.remap(this.gameObject.owner.color),\n            (this.lastOwnerColor = this.gameObject.owner.color),\n            this.updateBaseLight(),\n            (this.vxlExtraLight = new THREE.Vector3().copy(this.baseVxlExtraLight)),\n            (this.shpExtraLight = new THREE.Vector3().copy(this.baseShpExtraLight)),\n            (this.withPosition = new y.WithPosition());\n    }\n    updateBaseLight() {\n        (this.baseShpExtraLight = this.lighting\n            .compute(this.objectArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation)\n            .addScalar(-1)\n            .addScalar(this.rules.audioVisual.extraUnitLight)),\n            (this.baseVxlExtraLight = new THREE.Vector3().setScalar(Math.PI * 1.5 + this.lighting.computeNoAmbient(this.objectArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation) + this.rules.audioVisual.extraUnitLight));\n    }\n    registerPlugin(e) {\n        this.plugins.push(e);\n    }\n    updateLighting() {\n        this.plugins.forEach((e) => e.updateLighting?.()),\n            this.updateBaseLight(),\n            this.vxlExtraLight.copy(this.baseVxlExtraLight),\n            this.shpExtraLight.copy(this.baseShpExtraLight);\n    }\n    get3DObject() {\n        return this.target;\n    }\n    create3DObject() {\n        let e = this.get3DObject();\n        e ||\n            ((e = new d.BoxIntersectObject3D(new THREE.Vector3(1, 1 / 3, 1).multiplyScalar(S.Coords.LEPTONS_PER_TILE))),\n                (e.name = this.label),\n                (e.userData.id = this.gameObject.id),\n                (this.target = e),\n                (e.matrixAutoUpdate = !1),\n                (this.withPosition.matrixUpdate = !0),\n                this.withPosition.applyTo(this),\n                this.createObjects(e),\n                this.vxlBuilders.forEach((e) => e.setExtraLight(this.vxlExtraLight)),\n                this.shpRenderable?.setExtraLight(this.shpExtraLight),\n                this.pipOverlay &&\n                    (this.pipOverlay.create3DObject(),\n                        this.posObj?.add(this.pipOverlay.get3DObject())));\n    }\n    updateClippingPlanes(e, t = !1) {\n        if (t ||\n            (this.objectRules.naval && !this.objectRules.underwater)) {\n            var i = S.Coords.tileHeightToWorld(e);\n            let t = [new THREE.Plane(new THREE.Vector3(0, 1, 0), -i)];\n            this.vxlBuilders.forEach((e) => e.setClippingPlanes(t));\n        }\n    }\n    getIntersectTarget() {\n        return this.target;\n    }\n    getUiName() {\n        var e = this.plugins.reduce((e, t) => t.getUiNameOverride?.() ?? e, void 0);\n        return void 0 !== e ? e : this.gameObject.getUiName();\n    }\n    setPosition(e) {\n        this.withPosition.setPosition(e.x, e.y, e.z);\n    }\n    getPosition() {\n        return this.withPosition.getPosition();\n    }\n    highlight() {\n        this.plugins.some((e) => e.shouldDisableHighlight?.()) ||\n            (this.highlightAnimRunner.animation.getState() !==\n                w.AnimationState.RUNNING &&\n                this.highlightAnimRunner.animate(2));\n    }\n    update(i, r = 0) {\n        this.plugins.forEach((e) => e.update(i)),\n            this.pipOverlay?.update(i),\n            this.gameObject.veteranLevel !== this.lastVeteranLevel &&\n                (this.gameObject.veteranLevel === O.VeteranLevel.Elite &&\n                    void 0 !== this.lastVeteranLevel &&\n                    this.highlightAnimRunner.animate(30),\n                    (this.lastVeteranLevel = this.gameObject.veteranLevel));\n        var e = this.gameObject.tile.z + this.gameObject.tileElevation, t = void 0 === this.lastElevation || this.lastElevation !== e;\n        t &&\n            ((this.lastElevation = e),\n                this.updateBaseLight(),\n                this.updateClippingPlanes(this.gameObject.tile.z));\n        var s = this.gameObject.invulnerableTrait.isActive(), a = s !== this.lastInvulnerable;\n        this.lastInvulnerable = s;\n        var n = this.highlightAnimRunner.shouldUpdate();\n        s && a && this.invulnAnimRunner.animate(),\n            this.invulnAnimRunner.shouldUpdate() &&\n                this.invulnAnimRunner.tick(i);\n        var o = this.gameObject.warpedOutTrait.isActive(), l = o !== this.lastWarpedOut;\n        this.lastWarpedOut = o;\n        var c = this.gameObject.cloakableTrait?.isCloaked(), h = c !== this.lastCloaked;\n        this.lastCloaked = c;\n        let u = this.gameObject.submergibleTrait?.isSubmerged();\n        e = u !== this.lastSubmerged;\n        if (((this.lastSubmerged = u), l || h || e)) {\n            let t = o || c || u ? 0.5 : 1;\n            this.shpRenderable?.setOpacity(t),\n                this.shpRenderable?.setFlat(!!u),\n                this.vxlBuilders.forEach((e) => {\n                    e.setOpacity(t), e.setShadow(!u);\n                }),\n                this.placeholder?.setOpacity(t);\n        }\n        if ((t || a || s || n) &&\n            (n && this.highlightAnimRunner.tick(i),\n                (p = s ? this.invulnAnimRunner.getValue() : 0),\n                (P = (n ? this.highlightAnimRunner.getValue() : 0) || p),\n                A.ExtraLightHelper.multiplyVxl(this.vxlExtraLight, this.baseVxlExtraLight, this.lighting.getAmbientIntensity(), P as any),\n                A.ExtraLightHelper.multiplyShp(this.shpExtraLight, this.baseShpExtraLight, P as any),\n            this.gameObject.isDestroyed && this.resolveObjectRemove)) {\n            if ((this.squidGrabAnim &&\n                (this.posObj?.remove(this.squidGrabAnim.get3DObject()),\n                    this.squidGrabAnim.dispose(),\n                    (this.squidGrabAnim = void 0)),\n                this.destroyStartTime || (this.destroyStartTime = i),\n                this.isSinker())) {\n                var d: any = (i - this.destroyStartTime) / 3e3, g: any = 1 <= d;\n                g\n                    ? (this.mainObj.visible = !1)\n                    : (this.objectRules.naval &&\n                        (this.mainObj.rotation.x = (Math.PI / 4) * d),\n                        (this.mainObj.position.y =\n                            -16 * S.Coords.ISO_WORLD_SCALE * d),\n                        (this.mainObj.position.z =\n                            8 * S.Coords.ISO_WORLD_SCALE * d),\n                        this.mainObj.updateMatrix());\n                let e = !1;\n                this.sinkWakeAnims.forEach((e) => e.update(i)),\n                    this.sinkWakeAnims.filter((e) => !e.isAnimFinished())\n                        .length ||\n                        (this.sinkWakeAnims.forEach((e) => this.get3DObject().remove(e.get3DObject())),\n                            (this.sinkWakeAnims.length = 0),\n                            (e = !0)),\n                    g && e && this.resolveObjectRemove();\n            }\n        }\n        else if (!this.gameObject.warpedOutTrait.isActive()) {\n            let e = (Math.floor(this.gameObject.direction +\n                this.gameObject.spinVelocity * r) +\n                360) %\n                360;\n            var p = e !== this.lastDirection;\n            p &&\n                void 0 !== this.lastDirection &&\n                this.objectArt.isVoxel &&\n                this.gameObject.zone === M.ZoneType.Air &&\n                ((T = (e - this.lastDirection) as any),\n                    void 0 !== this.lastDirectionDelta &&\n                        Math.abs(T as any) < 2 &&\n                        Math.abs(this.lastDirectionDelta) < 2 &&\n                        Math.sign(T as any) !== Math.sign(this.lastDirectionDelta)\n                        ? (e = this.lastDirection)\n                        : (this.lastDirectionDelta = T as any)),\n                (this.lastDirection = e);\n            var m = this.gameObject.owner.color;\n            this.lastOwnerColor !== m &&\n                (this.palette.remap(m),\n                    (this.lastOwnerColor = m),\n                    this.vxlBuilders.forEach((e) => e.setPalette(this.palette)),\n                    this.shpRenderable?.setPalette(this.palette),\n                    this.placeholder?.setPalette(this.palette));\n            var f, y = this.gameObject.isMoving ||\n                (!this.objectArt.isVoxel &&\n                    !!this.gameObject.spinVelocity), d = this.gameObject.isFiring as any, g = (void 0 === this.lastMoving || this.lastMoving !== y) as any, T = (void 0 === this.lastFiring || this.lastFiring !== (d as any)) as any;\n            if (0 < r && (y || g)) {\n                let e = this.gameObject.moveTrait.velocity.clone(), t = e.multiplyScalar(r);\n                m = t.add(this.gameObject.position.worldPosition);\n                this.setPosition(m);\n            }\n            (g || T) &&\n                ((this.lastMoving = y),\n                    (this.lastFiring = d as any),\n                    this.objectArt.isVoxel ||\n                        this.updateShapeAnimation(y, d));\n            let t;\n            if (this.gameObject.rules.isChargeTurret) {\n                if (T && d) {\n                    this.chargeTurretRunner = new x.SimpleRunner();\n                    let e = new C.AnimProps(new E.IniSection(\"dummy\"), this.gameObject.rules.turretCount);\n                    (e.reverse = !0), (e.rate = 5);\n                    var v = new w.Animation(e, this.gameSpeed);\n                    this.chargeTurretRunner.animation = v;\n                }\n                this.chargeTurretRunner?.tick(i);\n                var b = this.chargeTurretRunner?.getCurrentFrame() ?? 0;\n                (t = b !== this.currentTurretIdx),\n                    (this.currentTurretIdx = b),\n                    this.chargeTurretRunner?.animation.getState() ===\n                        w.AnimationState.STOPPED &&\n                        (this.chargeTurretRunner = void 0);\n            }\n            else\n                (t = this.gameObject.turretNo !== this.currentTurretIdx),\n                    (this.currentTurretIdx = this.gameObject.turretNo);\n            this.objectArt.isVoxel\n                ? (this.updateVxlRotation(e, p),\n                    this.updateBodyVxl(),\n                    (v =\n                        ((T = this.gameObject.rocking?.facing as any) !==\n                            this.lastRockingFacing) as any),\n                    (this.lastRockingFacing = T as any),\n                    !v ||\n                        void 0 === T ||\n                        (0 < (f = this.gameObject.rocking.factor) &&\n                            this.startRocking(T, f, i)),\n                    (f =\n                        (b = !(!this.gameObject.parasiteableTrait?.isInfested() ||\n                            !this.gameObject.parasiteableTrait.getParasite()\n                                ?.rules.organic)) !== this.lastSquidGrabbed),\n                    (this.lastSquidGrabbed = b),\n                    this.updateRocking(i, b),\n                    this.gameObject.turretTrait &&\n                        1 < this.objectRules.turretCount &&\n                        t &&\n                        this.updateActiveTurret(this.currentTurretIdx),\n                    this.updateSquidGrab(i, b, f, p, e, T, v))\n                : this.shpAnimRunner &&\n                    (this.shpAnimRunner.tick(i),\n                        this.updateShapeFrame(e, y, d));\n        }\n    }\n    updateVxlRotation(e: number, t: boolean) {\n        const r_tilt = this.gameObject.tilterTrait?.tilt ?? {\n            yaw: 0,\n            pitch: 0,\n        };\n        if (!this.lastTilt ||\n            r_tilt.pitch !== this.lastTilt.pitch ||\n            r_tilt.yaw !== this.lastTilt.yaw ||\n            t) {\n            this.lastTilt = r_tilt;\n            this.tiltObj.rotation.y = THREE.MathUtils.degToRad(r_tilt.yaw);\n            this.tiltObj.rotation.x = THREE.MathUtils.degToRad(r_tilt.pitch);\n            this.tiltObj.updateMatrix();\n            this.dirWrapObj.rotation.y = THREE.MathUtils.degToRad(e - r_tilt.yaw);\n            this.dirWrapObj.updateMatrix();\n        }\n        if (this.turret && this.gameObject.turretTrait) {\n            const i = Math.floor(this.gameObject.turretTrait.facing);\n            const turretChanged = i !== this.lastTurretFacing;\n            this.lastTurretFacing = i;\n            if (turretChanged || t) {\n                const turretRotation = THREE.MathUtils.degToRad(i - e);\n                this.turret.rotation.y = turretRotation;\n                this.turret.updateMatrix();\n                if (this.barrel) {\n                    this.barrel.rotation.y = turretRotation;\n                    this.barrel.updateMatrix();\n                }\n            }\n        }\n        this.rotors?.forEach((rotor, rotorIndex) => {\n            (this.rotorSpeeds[rotorIndex] =\n                g.RotorHelper.computeRotationStep(this.gameObject, this.rotorSpeeds[rotorIndex] ?? 0, this.objectArt.rotors[rotorIndex])),\n                this.rotorSpeeds[rotorIndex] &&\n                    (rotor.rotateOnAxis(this.objectArt.rotors[rotorIndex].axis, this.rotorSpeeds[rotorIndex]),\n                        rotor.updateMatrix());\n        });\n    }\n    startRocking(i, r, s) {\n        if (this.bodyVxlBuilder) {\n            (this.rockingStartTime = s), (this.rockingFactor = r);\n            const aabb = this.bodyVxlBuilder.getLocalBoundingBox();\n            if (!aabb)\n                return;\n            let e = new THREE.Box2(new THREE.Vector2(aabb.min.x, aabb.min.y), new THREE.Vector2(aabb.max.x, aabb.max.y));\n            var n = THREE.MathUtils.degToRad(c.FacingUtil.toWorldDeg(i));\n            let tmp = new THREE.Vector2();\n            let t = new THREE.Vector2(10, 0)\n                .rotateAround(new THREE.Vector2(), n)\n                .setLength(e.getSize(tmp).length() + 1);\n            let arr: any = t.toArray();\n            const clip: any = (o as any).default || (o as any);\n            try {\n                clip([0, 0], arr, [e.min.x, e.min.y, e.max.x, e.max.y]);\n            }\n            catch { }\n            this.rockingPoint = new THREE.Vector3(arr[0], 0, arr[1]);\n            const perp = t\n                .clone()\n                .rotateAround(new THREE.Vector2(), -Math.PI / 2)\n                .normalize();\n            this.rockingAxis = new THREE.Vector3(perp.x, 0, perp.y);\n        }\n    }\n    updateRocking(t, i) {\n        if (this.rockingStartTime) {\n            var r = t - this.rockingStartTime;\n            let e = this.rockingFactor;\n            i ||\n                (e *= 1 - Math.min(1, this.gameObject.rules.weight / 5));\n            var s = r || e\n                ? Math.min(1, ((r /\n                    ((a.ROCKING_TICKS /\n                        u.GameSpeed.BASE_TICKS_PER_SECOND) *\n                        1e3)) *\n                    this.gameSpeed.value) /\n                    e)\n                : 0, r = P * e * (1 - Math.pow(2 * (s - 0.5), 2));\n            this.rockingTiltObj.position.set(0, 0, 0),\n                this.rockingTiltObj.rotation.set(0, 0, 0),\n                this.rockingTiltObj.scale.set(1, 1, 1),\n                R.MathUtils.rotateObjectAboutPoint(this.rockingTiltObj, this.rockingPoint, this.rockingAxis, r),\n                this.rockingTiltObj.updateMatrix(),\n                (1 !== s && 0 !== e) || (this.rockingStartTime = void 0);\n        }\n    }\n    updateSquidGrab(e, t, i, r, s, a, n) {\n        var o;\n        if ((i &&\n            (this.squidGrabAnim &&\n                (this.posObj?.remove(this.squidGrabAnim.get3DObject()),\n                    this.squidGrabAnim.dispose(),\n                    (this.squidGrabAnim = void 0)),\n                t &&\n                    ((this.squidGrabAnim =\n                        this.renderableManager.createAnim(\"SQDG\", (e) => {\n                            (e.extraOffset = {\n                                x: 0,\n                                y: -S.Coords.ISO_TILE_SIZE / 4,\n                            }),\n                                e.setExtraLight(this.shpExtraLight);\n                        }, !0)),\n                        this.squidGrabAnim.remapColor(this.gameObject.parasiteableTrait.getParasite().owner\n                            .color),\n                        this.squidGrabAnim.create3DObject(),\n                        this.posObj?.add(this.squidGrabAnim.get3DObject()))),\n            t &&\n                (r || i) &&\n                this.updateSquidGrabAnim(this.squidGrabAnim.getAnimProps(), s, I.Grab),\n            t &&\n                n &&\n                a &&\n                ((o = 0 < a ? I.Shake1 : I.Shake2),\n                    this.updateSquidGrabAnim(this.squidGrabAnim.getAnimProps(), s, o),\n                    this.squidGrabAnim.reset()),\n            t && n && !a)) {\n            var l = this.rules.combatDamage.splashList;\n            for (let e = 0; e < 3; e++) {\n                var c = l[h.getRandomInt(0, l.length - 1)];\n                this.renderableManager.createTransientAnim(c, (e) => {\n                    let t = this.withPosition.getPosition().clone();\n                    var i = {\n                        x: h.getRandomInt(-S.Coords.ISO_TILE_SIZE / 2, S.Coords.ISO_TILE_SIZE / 2) * S.Coords.ISO_WORLD_SCALE,\n                        y: h.getRandomInt(-S.Coords.ISO_TILE_SIZE / 2, S.Coords.ISO_TILE_SIZE / 2) * S.Coords.ISO_WORLD_SCALE,\n                    };\n                    e.setPosition(t.add(new THREE.Vector3(i.x, 0, i.y)));\n                });\n            }\n        }\n        this.squidGrabAnim?.update(e);\n    }\n    updateShapeAnimation(t, i) {\n        if (this.shpAnimRunner) {\n            let e = this.shpAnimRunner.animation.props;\n            var r;\n            i\n                ? ((e.loopEnd = this.objectArt.firingFrames - 1),\n                    (e.rate = C.AnimProps.defaultRate / 2))\n                : t || this.objectRules.naval\n                    ? ((e.loopEnd = this.objectArt.walkFrames - 1),\n                        (r =\n                            this.objectRules.naval && !t\n                                ? this.objectRules.idleRate\n                                : this.objectRules.walkRate),\n                        (e.rate = C.AnimProps.defaultRate / r))\n                    : (e.loopEnd = this.objectArt.standingFrames - 1),\n                this.shpAnimRunner.animation.rewind();\n        }\n    }\n    updateShapeFrame(t, i, r) {\n        if (this.shpRenderable && this.shpAnimRunner) {\n            let e;\n            var s = this.objectArt.facings, a = Math.round((((t - 45 + 360) % 360) / 360) * s) % s, s = this.shpAnimRunner.animation.getCurrentFrame();\n            (e = r\n                ? this.objectArt.startFiringFrame +\n                    this.objectArt.firingFrames * a +\n                    s\n                : i || this.objectRules.naval\n                    ? this.objectArt.startWalkFrame +\n                        this.objectArt.walkFrames * a +\n                        s\n                    : this.objectArt.startStandFrame +\n                        this.objectArt.standingFrames * a +\n                        s),\n                this.shpRenderable.setFrame(e);\n        }\n    }\n    updateSquidGrabAnim(e, t, i) {\n        var r = Math.round((((360 - t) % 360) / 360) * 8) % 8;\n        (e.start = 10 * r + 80 * i),\n            (e.end = 10 * r + 9 + 80 * i),\n            (e.loopStart = e.start),\n            (e.loopEnd = e.end),\n            (e.loopCount = 0),\n            (e.rate =\n                10 /\n                    (a.ROCKING_TICKS /\n                        u.GameSpeed.BASE_TICKS_PER_SECOND /\n                        (this.rockingFactor ?? 1)));\n    }\n    createObjects(t) {\n        if (this.debugFrame.value) {\n            let e = n.DebugUtils.createWireframe({ width: 1, height: 1 }, 1);\n            e.translateX(-S.Coords.getWorldTileSize() / 2),\n                e.translateZ(-S.Coords.getWorldTileSize() / 2),\n                t.add(e);\n        }\n        let e = (this.tiltObj = new THREE.Object3D());\n        (e.matrixAutoUpdate = !1), (e.rotation.order = \"YXZ\");\n        let i = (this.dirWrapObj = new THREE.Object3D());\n        i.matrixAutoUpdate = !1;\n        var r = (this.mainObj = this.createMainObject());\n        let s = (this.rockingTiltObj = new THREE.Object3D());\n        (s.matrixAutoUpdate = !1),\n            (s.rotation.order = \"YXZ\"),\n            s.add(r),\n            i.add(s),\n            e.add(i);\n        let a = (this.posObj = new THREE.Object3D());\n        (a.matrixAutoUpdate = !1), a.add(e), t.add(a);\n    }\n    computeSpriteAnchorOffset(e) {\n        var t = this.objectArt.getDrawOffset();\n        return { x: e.x + t.x, y: e.y + t.y };\n    }\n    createMainObject() {\n        let n = new THREE.Object3D();\n        if (((n.matrixAutoUpdate = !1), this.objectArt.isVoxel)) {\n            var o = !this.objectArt.noHva, e = this.objectArt.imageName.toLowerCase(), t = e + \".vxl\", r = this.voxels.get(t);\n            if (r) {\n                var s = o ? this.voxelAnims.get(e + \".hva\") : void 0;\n                let i = (this.bodyVxlBuilder =\n                    this.vxlBuilderFactory.create(r, s, this.paletteRemaps, this.palette));\n                this.vxlBuilders.push(i);\n                s = this.mainVxl = i.build();\n                n.add(s),\n                    this.objectArt.rotors &&\n                        (this.rotors = this.objectArt.rotors.map((e) => {\n                            var t = i.getSection(e.name);\n                            if (!t)\n                                throw new Error(`Vehicle \"${this.objectRules.name}\" VXL section \"${e.name}\" not found`);\n                            return t;\n                        }));\n            }\n            else\n                console.warn(`VXL missing for vehicle ${this.objectRules.name}. Vxl file ${t} not found. `),\n                    n.add(this.createPlaceholder());\n            if (this.objectRules.spawns &&\n                this.objectRules.noSpawnAlt) {\n                let i = e + \"wo.vxl\";\n                var a = this.voxels.get(i);\n                if (a) {\n                    var l = o\n                        ? this.voxelAnims.get(i.replace(\".vxl\", \".hva\"))\n                        : void 0;\n                    let e = this.vxlBuilderFactory.create(a, l, this.paletteRemaps, this.palette);\n                    this.vxlBuilders.push(e);\n                    let t = (this.noSpawnAltVxl = e.build());\n                    (t.visible = !1), n.add(t);\n                }\n                else\n                    console.warn(`<${this.gameObject.name}>: Couldn't find noSpawnAlt image \"${i}\"`);\n            }\n            if (this.gameObject.harvesterTrait &&\n                this.objectRules.unloadingClass) {\n                var c = this.rules.hasObject(this.objectRules.unloadingClass, i.ObjectType.Vehicle)\n                    ? this.rules\n                        .getObject(this.objectRules.unloadingClass, i.ObjectType.Vehicle)\n                        .imageName.toLowerCase()\n                    : void 0, a = c ? this.voxels.get(c + \".vxl\") : void 0;\n                if (a) {\n                    l = o ? this.voxelAnims.get(c + \".hva\") : void 0;\n                    let e = this.vxlBuilderFactory.create(a, l, this.paletteRemaps, this.palette);\n                    this.vxlBuilders.push(e);\n                    var g = (this.harvesterAltVxl = e.build());\n                    (g.visible = !1), n.add(g);\n                }\n                else\n                    console.warn(`<${this.gameObject.name}>: Couldn't find UnloadingClass image \"${c}.vxl\"`);\n            }\n            if (this.gameObject.turretTrait) {\n                c = this.objectArt.turretOffset;\n                let t = n;\n                if (c) {\n                    let e = new THREE.Object3D();\n                    (e.matrixAutoUpdate = !1),\n                        (e.position.z = -c),\n                        e.updateMatrix(),\n                        n.add(e),\n                        (t = e);\n                }\n                let r = [];\n                for (let a = 0; a < this.objectRules.turretCount; ++a) {\n                    let i = e + `tur${a || \"\"}.vxl`;\n                    var h = this.voxels.get(i);\n                    if (h) {\n                        var u = o\n                            ? this.voxelAnims.get(i.replace(\".vxl\", \".hva\"))\n                            : void 0;\n                        let e = this.vxlBuilderFactory.create(h, u, this.paletteRemaps, this.palette);\n                        this.vxlBuilders.push(e);\n                        let t = e.build();\n                        (t.visible = a === this.gameObject.turretNo),\n                            r.push(t);\n                    }\n                    else\n                        console.warn(`<${this.gameObject.name}>: Missing turret file \"${i}\"`),\n                            r.push(void 0);\n                }\n                (this.currentTurretIdx = this.gameObject.turretNo),\n                    (this.allTurrets = r);\n                let i;\n                1 < r.length\n                    ? ((i = this.turret = new THREE.Object3D()),\n                        (i.matrixAutoUpdate = !1),\n                        r.forEach((e) => e && i.add(e)))\n                    : (i = this.turret = r[0]),\n                    i && t.add(i);\n                let s = e + \"barl.vxl\";\n                c = this.voxels.get(s);\n                if (c) {\n                    var d = o\n                        ? this.voxelAnims.get(s.replace(\".vxl\", \".hva\"))\n                        : void 0;\n                    let e = this.vxlBuilderFactory.create(c, d, this.paletteRemaps, this.palette);\n                    this.vxlBuilders.push(e);\n                    var g = (this.barrel = e.build());\n                    t.add(g);\n                }\n            }\n        }\n        else {\n            let e = new f.MapSpriteTranslation(1, 1);\n            var { spriteOffset: d, anchorPointWorld: g } = e.compute() as any, d = this.computeSpriteAnchorOffset(d) as any;\n            let i;\n            try {\n                i = this.imageFinder.findByObjectArt(this.objectArt);\n            }\n            catch (e) {\n                if (!(e instanceof MissingImageError))\n                    throw e;\n                console.warn(`<${this.gameObject.name}>: ` + e.message);\n            }\n            if (i) {\n                let e = (this.shpRenderable = p.ShpRenderable.factory(i, this.palette, this.camera, d, this.objectArt.hasShadow));\n                e.setBatched(this.useSpriteBatching),\n                    this.useSpriteBatching &&\n                        e.setBatchPalettes(this.paletteRemaps),\n                    e.create3DObject(),\n                    n.add(e.get3DObject()),\n                    (n.position.x = g.x),\n                    (n.position.z = g.y),\n                    n.updateMatrix();\n                let t = new C.AnimProps(new E.IniSection(\"dummy\"), i);\n                t.loopCount = -1;\n                g = new w.Animation(t, this.gameSpeed);\n                (this.shpAnimRunner = new x.SimpleRunner()),\n                    (this.shpAnimRunner.animation = g);\n            }\n            else\n                n.add(this.createPlaceholder());\n        }\n        return n;\n    }\n    createPlaceholder() {\n        return ((this.placeholder = new b.DebugRenderable({ width: 0.5, height: 0.5 }, this.objectArt.height, this.palette, { centerFoundation: !0 })),\n            this.placeholder.setBatched(this.useSpriteBatching),\n            this.useSpriteBatching &&\n                this.placeholder.setBatchPalettes(this.paletteRemaps),\n            this.placeholder.create3DObject(),\n            this.placeholder.get3DObject());\n    }\n    updateActiveTurret(i) {\n        this.allTurrets.forEach((e, t) => {\n            e && (e.visible = t === i);\n        });\n    }\n    updateBodyVxl() {\n        var e = !!this.noSpawnAltVxl &&\n            !this.gameObject.airSpawnTrait.availableSpawns, t = !!this.harvesterAltVxl &&\n            !!this.gameObject.harvesterTrait &&\n            [\n                r.HarvesterStatus.PreparingToUnload,\n                r.HarvesterStatus.Unloading,\n            ].includes(this.gameObject.harvesterTrait.status);\n        this.noSpawnAltVxl && (this.noSpawnAltVxl.visible = e),\n            this.harvesterAltVxl && (this.harvesterAltVxl.visible = t),\n            this.mainVxl && (this.mainVxl.visible = !e && !t);\n    }\n    isSinker() {\n        return (this.gameObject.zone === M.ZoneType.Water &&\n            this.gameObject.isSinker);\n    }\n    onCreate(t) {\n        (this.renderableManager = t),\n            this.plugins.forEach((e) => e.onCreate(t)),\n            this.objectRules.ambientSound &&\n                (this.ambientSound = this.worldSound?.playEffect(this.objectRules.ambientSound, this.gameObject));\n    }\n    onRemove(t) {\n        if (((this.renderableManager = void 0),\n            this.plugins.forEach((e) => e.onRemove(t)),\n            this.ambientSound?.stop(),\n            this.gameObject.isDestroyed &&\n                this.gameObject.isVehicle() &&\n                this.get3DObject())) {\n            if (this.gameObject.deathType === l.DeathType.Temporal)\n                return;\n            if (this.gameObject.deathType === l.DeathType.None)\n                return;\n            if (this.gameObject.deathType === l.DeathType.Crush)\n                return;\n            if (!this.isSinker() ||\n                this.objectRules.underwater ||\n                (this.gameObject.deathType !== l.DeathType.Sink &&\n                    this.objectRules.speedType === s.SpeedType.Hover)) {\n                if (this.objectRules.underwater &&\n                    this.objectRules.organic)\n                    return;\n                if (!this.objectRules.explosion.length)\n                    return;\n                if (this.gameObject.explodes &&\n                    this.objectRules.deathWeapon)\n                    return;\n                var e = this.objectRules.explosion, e = e[h.getRandomInt(0, e.length - 1)];\n                return void t.createTransientAnim(e, (e) => e.setPosition(this.withPosition.getPosition()));\n            }\n            if (this.isSinker()) {\n                var i = this.rules.audioVisual.wake;\n                this.sinkWakeAnims = [];\n                for (let e = 0; e < 5; e++) {\n                    let e = t.createAnim(i, void 0, !0);\n                    var r = {\n                        x: h.getRandomInt(-15, 15) * S.Coords.ISO_WORLD_SCALE,\n                        y: h.getRandomInt(-15, 15) * S.Coords.ISO_WORLD_SCALE,\n                    };\n                    e.setPosition(new THREE.Vector3(r.x, 0, r.y)),\n                        this.sinkWakeAnims.push(e),\n                        e.create3DObject(),\n                        this.get3DObject().add(e.get3DObject());\n                }\n                this.gameObject.rules.naval ||\n                    this.updateClippingPlanes(this.gameObject.tile.z, !0);\n            }\n            return new Promise((e) => (this.resolveObjectRemove = e as any));\n        }\n    }\n    dispose() {\n        this.plugins.forEach((e) => e.dispose()),\n            this.pipOverlay?.dispose(),\n            this.shpRenderable?.dispose(),\n            this.vxlBuilders.forEach((e) => e.dispose()),\n            this.sinkWakeAnims?.forEach((e) => e.dispose()),\n            this.squidGrabAnim?.dispose(),\n            this.placeholder?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/WaypointLine.ts",
    "content": "import * as THREE from 'three';\nimport { MeshLine, MeshLineMaterial } from 'three.meshline';\nimport { Coords } from '@/game/Coords';\nimport { getMeshLineResolution } from '@/engine/renderable/fx/MeshLineResolution';\ninterface WaypointVertex {\n    enabled: boolean;\n    position: THREE.Vector3;\n    lineHead?: boolean;\n}\ninterface LinePath {\n    color: string | number;\n    bgColor: string | number;\n    vertices: WaypointVertex[];\n    verticesNeedUpdate: boolean;\n}\ninterface Camera extends THREE.Camera {\n    top: number;\n    right: number;\n    rotation: THREE.Euler;\n}\nexport class WaypointLine {\n    private linePath: LinePath;\n    private camera: Camera;\n    private lastColor: string | number;\n    private lastBgColor: string | number;\n    private lineHeadMaterial: THREE.PointsMaterial;\n    private lineHeadBgMaterial: THREE.PointsMaterial;\n    private wrapper?: THREE.Object3D;\n    private meshLine?: MeshLine;\n    private fgLineMesh?: THREE.Mesh;\n    private bgLineMesh?: THREE.Mesh;\n    private lineHeadMeshes?: THREE.Points[];\n    private lastUpdateMillis?: number;\n    private cameraHash?: string;\n    constructor(linePath: LinePath, camera: Camera) {\n        this.linePath = linePath;\n        this.camera = camera;\n        this.lastColor = this.linePath.color;\n        this.lastBgColor = this.linePath.bgColor;\n        this.lineHeadMaterial = new THREE.PointsMaterial({\n            size: 6,\n            sizeAttenuation: false,\n            color: linePath.color,\n            depthTest: false,\n            depthWrite: false,\n            transparent: true,\n        });\n        this.lineHeadBgMaterial = new THREE.PointsMaterial({\n            size: 8,\n            sizeAttenuation: false,\n            color: linePath.bgColor,\n            depthTest: false,\n            depthWrite: false,\n            transparent: true,\n        });\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.wrapper;\n    }\n    create3DObject(): void {\n        if (!this.wrapper) {\n            this.wrapper = new THREE.Object3D();\n            this.wrapper.name = \"waypoint_line\";\n            const vertices = this.getEnabledVertices();\n            const { geometry, lineLength, visible } = this.createLineGeometry(vertices);\n            this.fgLineMesh = new THREE.Mesh(geometry, this.createFgLineMaterial(new THREE.Color(this.linePath.color), lineLength));\n            this.fgLineMesh.renderOrder = 1000002;\n            this.fgLineMesh.visible = visible;\n            this.wrapper.add(this.fgLineMesh);\n            this.bgLineMesh = new THREE.Mesh(geometry, this.createBgLineMaterial(new THREE.Color(this.linePath.bgColor)));\n            this.bgLineMesh.renderOrder = 1000001;\n            this.bgLineMesh.visible = visible;\n            this.wrapper.add(this.bgLineMesh);\n            this.lineHeadMeshes = this.createLineHeads(this.getEnabledLineHeadVertices());\n            this.lineHeadMeshes.forEach((mesh) => this.wrapper!.add(mesh));\n        }\n    }\n    update(timestamp: number): void {\n        this.lastUpdateMillis = this.lastUpdateMillis || timestamp;\n        const deltaTime = (timestamp - this.lastUpdateMillis) / (1000 / 120);\n        this.lastUpdateMillis = timestamp;\n        const cameraHash = this.camera.top + \"_\" + this.camera.right;\n        if (cameraHash !== this.cameraHash) {\n            this.cameraHash = cameraHash;\n            [this.fgLineMesh!, this.bgLineMesh!].forEach((mesh) => {\n                (mesh.material as any).uniforms.resolution.value.copy(this.computeResolution(this.camera));\n            });\n        }\n        if (this.linePath.verticesNeedUpdate) {\n            this.linePath.verticesNeedUpdate = false;\n            const vertices = this.getEnabledVertices();\n            const lineLength = this.updateLineGeometry(vertices);\n            [this.fgLineMesh!].forEach((mesh) => {\n                const material = mesh.material as any;\n                material.uniforms.dashArray.value = this.computeDashArray(lineLength);\n            });\n            this.updateLineHeads(this.getEnabledLineHeadVertices());\n        }\n        if (this.linePath.color !== this.lastColor) {\n            this.lastColor = this.linePath.color;\n            (this.fgLineMesh!.material as any).uniforms.color.value = new THREE.Color(this.linePath.color);\n            this.lineHeadMaterial.color.set(this.linePath.color);\n        }\n        if (this.linePath.bgColor !== this.lastBgColor) {\n            this.lastBgColor = this.linePath.bgColor;\n            (this.bgLineMesh!.material as any).uniforms.color.value = new THREE.Color(this.linePath.bgColor);\n            this.lineHeadBgMaterial.color.set(this.linePath.bgColor);\n        }\n        [this.fgLineMesh!].forEach((mesh) => {\n            const material = mesh.material as any;\n            material.uniforms.dashOffset.value -= (material.uniforms.dashArray.value / 50) * deltaTime;\n        });\n    }\n    private computeLineLength(vertices: THREE.Vector3[]): number {\n        let length = 0;\n        for (let i = 1, len = vertices.length; i < len; i++) {\n            length += vertices[i].distanceTo(vertices[i - 1]);\n        }\n        return length;\n    }\n    private getEnabledVertices(): THREE.Vector3[] {\n        return this.linePath.vertices\n            .filter((vertex) => vertex.enabled)\n            .map((vertex) => vertex.position);\n    }\n    private getEnabledLineHeadVertices(): THREE.Vector3[] {\n        return this.linePath.vertices\n            .filter((vertex) => vertex.enabled && vertex.lineHead)\n            .map((vertex) => vertex.position);\n    }\n    private createLineGeometry(vertices: THREE.Vector3[]): {\n        geometry: THREE.BufferGeometry;\n        lineLength: number;\n        visible: boolean;\n    } {\n        const meshLine = this.meshLine = new MeshLine();\n        const visible = vertices.length >= 2;\n        const lineVertices = visible\n            ? vertices\n            : vertices.length === 1\n                ? [vertices[0], vertices[0]]\n                : [new THREE.Vector3(), new THREE.Vector3()];\n        meshLine.setPoints(lineVertices.map((pos) => [pos.x, pos.y, pos.z]).flat());\n        return {\n            geometry: meshLine.geometry,\n            lineLength: this.computeLineLength(vertices),\n            visible,\n        };\n    }\n    private updateLineGeometry(vertices: THREE.Vector3[]): number {\n        const previousGeometry = this.fgLineMesh?.geometry;\n        const { geometry, lineLength, visible } = this.createLineGeometry(vertices);\n        if (this.fgLineMesh) {\n            this.fgLineMesh.geometry = geometry;\n            this.fgLineMesh.visible = visible;\n        }\n        if (this.bgLineMesh) {\n            this.bgLineMesh.geometry = geometry;\n            this.bgLineMesh.visible = visible;\n        }\n        if (previousGeometry && previousGeometry !== geometry) {\n            previousGeometry.dispose();\n        }\n        return lineLength;\n    }\n    private createFgLineMaterial(color: THREE.Color, lineLength: number): MeshLineMaterial {\n        return new MeshLineMaterial({\n            color: color,\n            lineWidth: 2,\n            resolution: this.computeResolution(this.camera),\n            transparent: true,\n            sizeAttenuation: 0,\n            dashArray: this.computeDashArray(lineLength),\n            depthTest: false,\n        });\n    }\n    private createBgLineMaterial(color: THREE.Color): MeshLineMaterial {\n        return new MeshLineMaterial({\n            color: color,\n            lineWidth: 4,\n            resolution: this.computeResolution(this.camera),\n            transparent: true,\n            sizeAttenuation: 0,\n            depthTest: false,\n        });\n    }\n    private computeDashArray(lineLength: number): number {\n        return Math.min(1, 5 / lineLength) * Coords.ISO_WORLD_SCALE;\n    }\n    private computeResolution(camera: Camera): THREE.Vector2 {\n        return getMeshLineResolution(camera);\n    }\n    private createLineHeads(positions: THREE.Vector3[]): THREE.Points[] {\n        const geometry = new THREE.BufferGeometry();\n        geometry.setAttribute(\"position\", new THREE.BufferAttribute(new Float32Array(positions.map((pos) => [pos.x, pos.y, pos.z]).flat()), 3));\n        const foregroundPoints = new THREE.Points(geometry, this.lineHeadMaterial);\n        foregroundPoints.renderOrder = 1000004;\n        const backgroundPoints = new THREE.Points(geometry, this.lineHeadBgMaterial);\n        backgroundPoints.renderOrder = 1000003;\n        return [foregroundPoints, backgroundPoints];\n    }\n    private updateLineHeads(positions: THREE.Vector3[]): void {\n        const flatPositions = positions.map((pos) => [pos.x, pos.y, pos.z]).flat();\n        const geometry = this.lineHeadMeshes![0].geometry;\n        const positionAttribute = geometry.getAttribute(\"position\") as THREE.BufferAttribute;\n        if (positionAttribute.array.length !== flatPositions.length) {\n            geometry.setAttribute(\"position\", new THREE.BufferAttribute(new Float32Array(flatPositions), 3));\n        }\n        else {\n            const array = positionAttribute.array as Float32Array;\n            for (let i = 0, len = array.length; i < len; i++) {\n                array[i] = flatPositions[i];\n            }\n            positionAttribute.needsUpdate = true;\n        }\n    }\n    dispose(): void {\n        const disposedGeometries = new Set<THREE.BufferGeometry>();\n        [this.fgLineMesh, this.bgLineMesh].forEach((mesh) => {\n            if (!mesh) {\n                return;\n            }\n            if (!disposedGeometries.has(mesh.geometry)) {\n                mesh.geometry.dispose();\n                disposedGeometries.add(mesh.geometry);\n            }\n            const material = mesh.material;\n            if (Array.isArray(material)) {\n                material.forEach((entry) => entry.dispose());\n            }\n            else {\n                material.dispose();\n            }\n        });\n        this.lineHeadMeshes?.forEach((mesh) => {\n            if (!disposedGeometries.has(mesh.geometry)) {\n                mesh.geometry.dispose();\n                disposedGeometries.add(mesh.geometry);\n            }\n        });\n        this.lineHeadMaterial.dispose();\n        this.lineHeadBgMaterial.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/WaypointLines.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport type { TargetLinesConfig } from \"@/game/gameobject/task/system/TargetLinesConfig\";\nimport { configHasTarget } from \"@/game/gameobject/task/system/TargetLinesConfig\";\nimport { equals } from \"@/util/array\";\nimport { WaypointLine } from \"@/engine/renderable/entity/WaypointLine\";\nimport * as THREE from \"three\";\nenum VertexType {\n    Source = 0,\n    InitialTarget = 1,\n    Waypoint = 2\n}\ninterface Unit {\n    isSpawned: boolean;\n    owner: Player;\n    position: {\n        worldPosition: THREE.Vector3;\n    };\n    unitOrderTrait: {\n        targetLinesConfig?: TargetLinesConfig;\n        waypointPath?: WaypointPath;\n        currentWaypoint?: Waypoint;\n    };\n    isUnit(): boolean;\n}\ninterface Player {\n}\ninterface Waypoint {\n    original?: Waypoint;\n    draft?: boolean;\n    target: {\n        getWorldCoords(): THREE.Vector3;\n    };\n}\ninterface WaypointPath {\n    units: Set<Unit>;\n    waypoints: Waypoint[];\n}\ninterface PathNode {\n    tile: {\n        rx: number;\n        ry: number;\n        z: number;\n    };\n    onBridge?: {\n        tileElevation: number;\n    };\n}\ninterface Target {\n    position: {\n        worldPosition: THREE.Vector3;\n    };\n}\ninterface UnitSelection {\n    getHash(): string;\n    getSelectedUnits(): Unit[];\n    isSelected(unit: Unit): boolean;\n}\ninterface Camera {\n}\ninterface LineVertex {\n    type: VertexType;\n    enabled: boolean;\n    lineHead: boolean;\n    obj?: Unit;\n    waypoint?: Waypoint;\n    position: THREE.Vector3;\n}\ninterface LinePath {\n    vertices: LineVertex[];\n    verticesNeedUpdate: boolean;\n    color: number;\n    bgColor: number;\n    lineObj?: WaypointLine;\n}\nexport class WaypointLines {\n    private unitSelection: UnitSelection;\n    private currentPlayer: Player;\n    private selectedPaths: WaypointPath[];\n    private paths: WaypointPath[];\n    private camera: Camera;\n    private lastPathWaypoints: Map<WaypointPath, Waypoint[]> = new Map();\n    private sourceLinePaths: Map<Unit, LinePath> = new Map();\n    private waypointLinePaths: Map<WaypointPath, LinePath> = new Map();\n    private obj?: THREE.Object3D;\n    private selectionHash?: string;\n    private lastPaths?: WaypointPath[];\n    constructor(unitSelection: UnitSelection, currentPlayer: Player, selectedPaths: WaypointPath[], paths: WaypointPath[], camera: Camera) {\n        this.unitSelection = unitSelection;\n        this.currentPlayer = currentPlayer;\n        this.selectedPaths = selectedPaths;\n        this.paths = paths;\n        this.camera = camera;\n    }\n    create3DObject(): void {\n        if (!this.obj) {\n            this.obj = new THREE.Object3D();\n            this.obj.name = \"waypoint_lines\";\n            this.obj.matrixAutoUpdate = false;\n        }\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.obj;\n    }\n    update(deltaTime: number): void {\n        const currentHash = this.unitSelection.getHash();\n        const selectionChanged = this.selectionHash === undefined || this.selectionHash !== currentHash;\n        if (selectionChanged) {\n            this.selectionHash = currentHash;\n        }\n        let pathsChanged = !this.lastPaths || !equals(this.lastPaths, this.paths);\n        if (pathsChanged) {\n            this.lastPaths = [...this.paths];\n        }\n        else {\n            for (const path of this.paths) {\n                const lastWaypoints = this.lastPathWaypoints.get(path);\n                if (!lastWaypoints || !equals(path.waypoints, lastWaypoints)) {\n                    pathsChanged = true;\n                    this.lastPathWaypoints.set(path, [...path.waypoints]);\n                    break;\n                }\n            }\n        }\n        if (selectionChanged || pathsChanged) {\n            let relevantUnits: Unit[] = [];\n            let selectedUnits = this.unitSelection.getSelectedUnits();\n            if (selectedUnits.length === 1 && selectedUnits[0].owner !== this.currentPlayer) {\n                selectedUnits = [];\n            }\n            relevantUnits = this.paths.length\n                ? [\n                    ...new Set([\n                        ...this.paths.map(path => [...path.units]).flat(),\n                        ...selectedUnits,\n                    ]),\n                ]\n                : selectedUnits;\n            relevantUnits = relevantUnits.filter(unit => unit.isSpawned);\n            [\n                ...this.sourceLinePaths.values(),\n                ...this.waypointLinePaths.values(),\n            ].forEach(linePath => {\n                const lineObj = linePath.lineObj;\n                if (lineObj && this.obj) {\n                    this.obj.remove(lineObj.get3DObject());\n                    lineObj.dispose();\n                }\n            });\n            this.sourceLinePaths.clear();\n            this.waypointLinePaths.clear();\n            for (const unit of relevantUnits) {\n                if (unit.isUnit()) {\n                    const linePath = this.createSourceLinePath(unit, this.paths.find(path => path.units.has(unit)));\n                    this.sourceLinePaths.set(unit, linePath);\n                    linePath.lineObj = new WaypointLine(linePath, this.camera as any);\n                    linePath.lineObj.create3DObject();\n                    if (this.obj) {\n                        this.obj.add(linePath.lineObj.get3DObject());\n                    }\n                    linePath.lineObj.update(deltaTime);\n                }\n            }\n            for (const path of this.paths) {\n                const linePath = this.createWaypointLinePath(path, this.selectedPaths.includes(path));\n                this.waypointLinePaths.set(path, linePath);\n                linePath.lineObj = new WaypointLine(linePath, this.camera as any);\n                linePath.lineObj.create3DObject();\n                if (this.obj) {\n                    this.obj.add(linePath.lineObj.get3DObject());\n                }\n                linePath.lineObj.update(deltaTime);\n            }\n        }\n        else {\n            this.sourceLinePaths.forEach((linePath, unit) => {\n                this.updateSourceLinePath(linePath, unit, this.paths.find(path => path.units.has(unit)));\n                linePath.lineObj?.update(deltaTime);\n            });\n            this.waypointLinePaths.forEach(linePath => {\n                this.updateWaypointLinePath(linePath);\n                linePath.lineObj?.update(deltaTime);\n            });\n        }\n    }\n    private createSourceLinePath(unit: Unit, path?: WaypointPath): LinePath {\n        const hasTarget = !!(unit.unitOrderTrait.targetLinesConfig &&\n            configHasTarget(unit.unitOrderTrait.targetLinesConfig));\n        const currentWaypoint = unit.unitOrderTrait.waypointPath\n            ? path?.waypoints.find(waypoint => waypoint.original === (unit.unitOrderTrait.currentWaypoint ??\n                unit.unitOrderTrait.waypointPath?.waypoints[0]))\n            : path?.waypoints.find(waypoint => waypoint.draft);\n        const linePath: LinePath = {\n            vertices: [],\n            verticesNeedUpdate: false,\n            color: 0xA5CF3F,\n            bgColor: this.unitSelection.isSelected(unit) ? 0xFFFFFF : 0x000000,\n        };\n        const sourceVertex: LineVertex = {\n            type: VertexType.Source,\n            enabled: hasTarget || !!currentWaypoint,\n            lineHead: true,\n            obj: unit,\n            position: unit.position.worldPosition.clone(),\n        };\n        const initialTargetVertex: LineVertex = {\n            type: VertexType.InitialTarget,\n            enabled: hasTarget &&\n                (!unit.unitOrderTrait.waypointPath || !unit.unitOrderTrait.currentWaypoint),\n            lineHead: true,\n            obj: unit,\n            position: hasTarget\n                ? this.computeInitialTargetPosition(unit.unitOrderTrait.targetLinesConfig!).clone()\n                : new THREE.Vector3(),\n        };\n        linePath.vertices.push(sourceVertex, initialTargetVertex);\n        if (currentWaypoint) {\n            const waypointVertex: LineVertex = {\n                type: VertexType.Waypoint,\n                enabled: true,\n                lineHead: false,\n                waypoint: currentWaypoint,\n                position: currentWaypoint.target.getWorldCoords().clone(),\n            };\n            linePath.vertices.push(waypointVertex);\n        }\n        return linePath;\n    }\n    private updateSourceLinePath(linePath: LinePath, unit: Unit, path?: WaypointPath): void {\n        const currentWaypoint = unit.unitOrderTrait.waypointPath\n            ? path?.waypoints.find(waypoint => waypoint.original === (unit.unitOrderTrait.currentWaypoint ??\n                unit.unitOrderTrait.waypointPath?.waypoints[0]))\n            : path?.waypoints.find(waypoint => waypoint.draft);\n        const hasWaypoint = !!currentWaypoint;\n        const hasTarget = !!(unit.unitOrderTrait.targetLinesConfig &&\n            configHasTarget(unit.unitOrderTrait.targetLinesConfig));\n        for (const vertex of linePath.vertices) {\n            let enabled: boolean | undefined;\n            let position: THREE.Vector3 | undefined;\n            if (vertex.type === VertexType.Source) {\n                enabled = hasWaypoint || hasTarget;\n                position = unit.position.worldPosition;\n            }\n            else if (vertex.type === VertexType.InitialTarget) {\n                enabled = hasTarget &&\n                    (!unit.unitOrderTrait.waypointPath || !unit.unitOrderTrait.currentWaypoint);\n                if (hasTarget && vertex.obj) {\n                    position = this.computeInitialTargetPosition(vertex.obj.unitOrderTrait.targetLinesConfig!);\n                }\n            }\n            else {\n                enabled = hasWaypoint;\n                if (currentWaypoint && vertex.waypoint) {\n                    vertex.waypoint = currentWaypoint;\n                }\n                position = vertex.waypoint?.target.getWorldCoords();\n            }\n            if (position && !position.equals(vertex.position)) {\n                linePath.verticesNeedUpdate = true;\n                vertex.position.copy(position);\n            }\n            if (enabled !== undefined && enabled !== vertex.enabled) {\n                linePath.verticesNeedUpdate = true;\n                vertex.enabled = enabled;\n            }\n        }\n    }\n    private createWaypointLinePath(path: WaypointPath, isSelected: boolean): LinePath {\n        return {\n            vertices: path.waypoints.map(waypoint => ({\n                type: VertexType.Waypoint,\n                enabled: true,\n                lineHead: true,\n                waypoint,\n                position: waypoint.target.getWorldCoords().clone(),\n            })),\n            verticesNeedUpdate: false,\n            color: 0xA5CF3F,\n            bgColor: isSelected ? 0xFFFFFF : 0x000000,\n        };\n    }\n    private updateWaypointLinePath(linePath: LinePath): void {\n        for (const vertex of linePath.vertices) {\n            if (vertex.waypoint) {\n                const position = vertex.waypoint.target.getWorldCoords();\n                if (!position.equals(vertex.position)) {\n                    vertex.position.copy(position);\n                    linePath.verticesNeedUpdate = true;\n                }\n            }\n        }\n    }\n    private computeInitialTargetPosition(config: TargetLinesConfig): THREE.Vector3 {\n        if (config.pathNodes && config.pathNodes.length) {\n            const node = config.pathNodes[0];\n            return Coords.tile3dToWorld(node.tile.rx + 0.5, node.tile.ry + 0.5, node.tile.z + (node.onBridge?.tileElevation ?? 0));\n        }\n        if (config.target) {\n            return config.target.position.worldPosition;\n        }\n        throw new Error(\"No target and no pathNodes found\");\n    }\n    dispose(): void {\n        this.sourceLinePaths.forEach(linePath => linePath.lineObj?.dispose());\n        this.waypointLinePaths.forEach(linePath => linePath.lineObj?.dispose());\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/building/AnimationType.ts",
    "content": "export enum AnimationType {\n    IDLE = 0,\n    PRODUCTION = 1,\n    ACTIVE = 2,\n    SPECIAL = 3,\n    SUPER = 4,\n    BUILDUP = 5,\n    UNBUILD = 6,\n    FACTORY_DEPLOYING = 7,\n    FACTORY_ROOF_DEPLOYING = 8,\n    SUPER_IDLE = 9,\n    SUPER_CHARGE_START = 10,\n    SUPER_CHARGE_LOOP = 11,\n    SUPER_CHARGE_END = 12,\n    SPECIAL_DOCKING = 13,\n    SPECIAL_REPAIR_START = 14,\n    SPECIAL_REPAIR_LOOP = 15,\n    SPECIAL_REPAIR_END = 16,\n    SPECIAL_SHOOT = 17,\n    FACTORY_UNDER_DOOR = 18,\n    FACTORY_UNDER_ROOF_DOOR = 19\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/building/BuildingAnimArtProps.ts",
    "content": "import { AnimationType } from \"./AnimationType\";\nimport { IniSection } from \"@/data/IniSection\";\nimport { BuildingAnimData } from \"./BuildingAnimData\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nconst ANIM_PROP_NAMES = new Map<AnimationType, string[]>([\n    [AnimationType.IDLE, [\"IdleAnim\"]],\n    [AnimationType.PRODUCTION, [\"ProductionAnim\"]],\n    [AnimationType.SUPER, [\n            \"SuperAnim\",\n            \"SuperAnimTwo\",\n            \"SuperAnimThree\",\n            \"SuperAnimFour\"\n        ]],\n    [AnimationType.ACTIVE, [\n            \"ActiveAnim\",\n            \"ActiveAnimTwo\",\n            \"ActiveAnimThree\",\n            \"ActiveAnimFour\"\n        ]],\n    [AnimationType.SPECIAL, [\n            \"SpecialAnim\",\n            \"SpecialAnimTwo\",\n            \"SpecialAnimThree\",\n            \"SpecialAnimFour\"\n        ]],\n    [AnimationType.FACTORY_DEPLOYING, [\n            \"DeployingAnim\",\n            \"UnderDoorAnim\"\n        ]],\n    [AnimationType.FACTORY_ROOF_DEPLOYING, [\n            \"RoofDeployingAnim\",\n            \"UnderRoofDoorAnim\"\n        ]],\n    [AnimationType.BUILDUP, [\"Buildup\"]],\n    [AnimationType.UNBUILD, [\"Buildup\"]]\n]);\nexport class BuildingAnimArtProps {\n    private animsByType: Map<AnimationType, BuildingAnimData[]>;\n    constructor() {\n        this.animsByType = new Map();\n    }\n    read(config: IniSection, objectManager: any): void {\n        ANIM_PROP_NAMES.forEach((propNames, type) => {\n            const anims: BuildingAnimData[] = [];\n            propNames.forEach((propName) => {\n                const animName = config.getString(propName);\n                if (animName) {\n                    const animData = new BuildingAnimData();\n                    animData.name = animName;\n                    animData.type = type;\n                    let art: IniSection | undefined;\n                    let animObject: any;\n                    if (objectManager.hasObject(animName, ObjectType.Animation)) {\n                        animObject = objectManager.getObject(animName, ObjectType.Animation);\n                        art = animObject.art;\n                    }\n                    if (type === AnimationType.BUILDUP || type === AnimationType.UNBUILD) {\n                        art = art ? art.clone() : new IniSection(animName);\n                        if (!art.has(\"Shadow\")) {\n                            art.set(\"Shadow\", \"yes\");\n                        }\n                        if (type === AnimationType.UNBUILD) {\n                            art.set(\"Reverse\", \"yes\");\n                        }\n                    }\n                    else if (!art) {\n                        console.warn(`[BuildingAnimArtProps] Missing building anim section \"${animName}\", skipping.`);\n                        return;\n                    }\n                    animData.art = art;\n                    animData.pauseWhenUnpowered = config.getBool(propName + \"Powered\", true);\n                    animData.showWhenUnpowered = !config.getBool(propName + \"PoweredLight\", false);\n                    const damagedAnimName = config.getString(propName + \"Damaged\");\n                    if (damagedAnimName && objectManager.hasObject(damagedAnimName, ObjectType.Animation)) {\n                        animData.damagedArt = objectManager.getObject(damagedAnimName, ObjectType.Animation).art;\n                    }\n                    animData.offset = {\n                        x: config.getNumber(propName + \"X\"),\n                        y: config.getNumber(propName + \"Y\")\n                    };\n                    let image = art.getString(\"Image\");\n                    image = image || animName;\n                    animData.image = image;\n                    animData.flat = propName === \"UnderDoorAnim\" ||\n                        propName === \"UnderRoofDoorAnim\" ||\n                        art.getBool(\"Flat\");\n                    if (animObject) {\n                        animData.translucent = animObject.translucent;\n                        animData.translucency = animObject.translucency;\n                    }\n                    anims.push(animData);\n                }\n            });\n            this.animsByType.set(type, anims);\n        });\n    }\n    getByType(type: AnimationType): BuildingAnimData[] {\n        if (!this.animsByType.has(type)) {\n            throw new Error(`Animation type \"${AnimationType[type]}\" has no data`);\n        }\n        return this.animsByType.get(type)!;\n    }\n    getAll(): Map<AnimationType, BuildingAnimData[]> {\n        return this.animsByType;\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/building/BuildingAnimData.ts",
    "content": "export class BuildingAnimData {\n    [key: string]: any;\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/building/BuildingShpHelper.ts",
    "content": "import { AnimProps } from \"@/engine/AnimProps\";\nimport { ImageFinder, MissingImageError } from \"@/engine/ImageFinder\";\nimport { ShpAggregator } from \"@/engine/renderable/builder/ShpAggregator\";\nexport class BuildingShpHelper {\n    constructor(private imageFinder: ImageFinder) { }\n    getShpFrameInfos(building: {\n        hasShadow: boolean;\n    }, mainShp: string | undefined, turretShp: string | undefined, animShps: Map<{\n        art: any;\n    }, string>): Map<string, any> {\n        const frameInfos = new Map<string, any>();\n        if (mainShp) {\n            frameInfos.set(mainShp, ShpAggregator.getShpFrameInfo(mainShp as any, building.hasShadow));\n        }\n        if (turretShp) {\n            frameInfos.set(turretShp, ShpAggregator.getShpFrameInfo(turretShp as any, building.hasShadow));\n        }\n        for (const [anim, shpName] of animShps) {\n            const animProps = new AnimProps(anim.art, shpName as any);\n            const frameInfo = ShpAggregator.getShpFrameInfo(shpName as any, animProps.shadow);\n            frameInfos.set(shpName, frameInfo);\n        }\n        return frameInfos;\n    }\n    collectAnimShpFiles(anims: {\n        getAll(): Map<string, Array<{\n            image: string;\n        }>>;\n    }, options: {\n        useTheaterExtension: boolean;\n    }): Map<{\n        image: string;\n    }, any> {\n        const shpFiles = new Map<{\n            image: string;\n        }, any>();\n        anims.getAll().forEach((animList) => {\n            for (const anim of animList) {\n                let shpFile;\n                try {\n                    shpFile = this.imageFinder.find(anim.image, options.useTheaterExtension);\n                }\n                catch (error) {\n                    if (error instanceof MissingImageError) {\n                        console.warn(error.message);\n                        continue;\n                    }\n                    throw error;\n                }\n                shpFiles.set(anim, shpFile);\n            }\n        });\n        return shpFiles;\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/building/DamageType.ts",
    "content": "export enum DamageType {\n    NORMAL = 0,\n    CONDITION_YELLOW = 1,\n    CONDITION_RED = 2,\n    DESTROYED = 3\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/building/PsychicDetectPlugin.ts",
    "content": "import { DetectionLineFx } from \"@/engine/renderable/fx/DetectionLineFx\";\nimport * as THREE from \"three\";\nexport class PsychicDetectPlugin {\n    private gameObject: any;\n    private psychicDetectorTrait: any;\n    private localPlayer: {\n        value: any;\n    };\n    private camera: THREE.Camera;\n    private lineEffects: Map<string, DetectionLineFx>;\n    private renderableManager?: any;\n    private lastDetectionLines?: any[];\n    constructor(gameObject: any, psychicDetectorTrait: any, localPlayer: {\n        value: any;\n    }, camera: THREE.Camera) {\n        this.gameObject = gameObject;\n        this.psychicDetectorTrait = psychicDetectorTrait;\n        this.localPlayer = localPlayer;\n        this.camera = camera;\n        this.lineEffects = new Map();\n    }\n    onCreate(renderableManager: any): void {\n        this.renderableManager = renderableManager;\n    }\n    update(delta: number): void {\n        const localPlayer = this.localPlayer?.value ?? this.localPlayer;\n        if (localPlayer === this.gameObject.owner) {\n            const detectionLines = this.psychicDetectorTrait.detectionLines;\n            const hasChanged = detectionLines !== this.lastDetectionLines;\n            this.lastDetectionLines = detectionLines;\n            const lines = detectionLines.map((line: any) => ({\n                hash: line.source.id + \"_\" + (line.target.obj?.id ?? line.target.tile.id),\n                line: line,\n            }));\n            if (hasChanged) {\n                for (const hash of this.lineEffects.keys()) {\n                    if (!lines.find(({ hash: h }) => h === hash)) {\n                        this.disposeLine(this.lineEffects.get(hash)!);\n                        this.lineEffects.delete(hash);\n                    }\n                }\n                for (const { line, hash } of lines) {\n                    if (!this.lineEffects.has(hash)) {\n                        const sourcePos = line.source.position.worldPosition.clone();\n                        const targetPos = line.target.getWorldCoords().clone();\n                        const color = new THREE.Color(line.source.owner.color.asHex());\n                        const effect = new DetectionLineFx(this.camera as any, sourcePos, targetPos, color, 1e6);\n                        this.lineEffects.set(hash, effect);\n                        this.renderableManager.addEffect(effect);\n                    }\n                }\n            }\n            for (const { line, hash } of lines) {\n                const effect = this.lineEffects.get(hash);\n                if (!effect)\n                    throw new Error(\"Line hash should have been found\");\n                const sourcePos = line.source.position.worldPosition.clone();\n                const targetPos = line.target.getWorldCoords().clone();\n                const color = new THREE.Color(line.source.owner.color.asHex());\n                if (!effect.color.equals(color)) {\n                    effect.color.copy(color);\n                    effect.needsUpdate = true;\n                }\n                if (!effect.sourcePos.equals(sourcePos)) {\n                    effect.sourcePos.copy(sourcePos);\n                    effect.needsUpdate = true;\n                }\n                if (!effect.targetPos.equals(targetPos)) {\n                    effect.targetPos.copy(targetPos);\n                    effect.needsUpdate = true;\n                }\n            }\n        }\n        else {\n            this.lineEffects.forEach((effect) => this.disposeLine(effect));\n        }\n    }\n    onRemove(): void {\n        this.renderableManager = undefined;\n        this.dispose();\n    }\n    dispose(): void {\n        this.lineEffects.forEach((effect) => this.disposeLine(effect));\n    }\n    private disposeLine(effect: DetectionLineFx): void {\n        effect.remove();\n        effect.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/map/MapBounds.ts",
    "content": "import * as THREE from \"three\";\nimport * as IsoCoords from \"@/engine/IsoCoords\";\nimport { WithVisibility } from \"@/engine/renderable/WithVisibility\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\ninterface Point {\n    x: number;\n    y: number;\n}\ninterface Size {\n    width: number;\n    height: number;\n}\ninterface BoundsInfo {\n    getClampedFullSize(): Point & Size;\n    getLocalSize(): Point & Size;\n    onLocalResize: {\n        subscribe: (callback: () => void) => void;\n        unsubscribe: (callback: () => void) => void;\n    };\n}\ninterface Map {\n    mapBounds: BoundsInfo;\n}\nexport class MapBounds {\n    private map: Map;\n    private withVisibility: WithVisibility;\n    private disposables: CompositeDisposable;\n    private target?: THREE.Object3D;\n    private wrapperObj?: THREE.Object3D;\n    constructor(map: Map) {\n        this.map = map;\n        this.withVisibility = new WithVisibility();\n        this.disposables = new CompositeDisposable();\n        const handleResize = () => {\n            if (this.target && this.wrapperObj) {\n                this.target.remove(this.wrapperObj);\n                this.wrapperObj = this.build();\n                this.target.add(this.wrapperObj);\n            }\n        };\n        map.mapBounds.onLocalResize.subscribe(handleResize);\n        this.disposables.add(() => map.mapBounds.onLocalResize.unsubscribe(handleResize));\n    }\n    private build(): THREE.Object3D {\n        const fullSize = this.map.mapBounds.getClampedFullSize();\n        const localSize = this.map.mapBounds.getLocalSize();\n        const fullRect = this.createBoundRect({ x: fullSize.x, y: fullSize.y }, { x: fullSize.x + fullSize.width, y: fullSize.y + fullSize.height }, 0xFF0000);\n        fullRect.matrixAutoUpdate = false;\n        const localRect = this.createBoundRect({ x: localSize.x, y: localSize.y }, { x: localSize.x + localSize.width, y: localSize.y + localSize.height - 1 }, 0x0000FF);\n        localRect.matrixAutoUpdate = false;\n        const container = new THREE.Object3D();\n        container.matrixAutoUpdate = false;\n        container.add(fullRect);\n        container.add(localRect);\n        return container;\n    }\n    private createBoundRect(start: Point, end: Point, color: number): THREE.Line {\n        const topLeft = IsoCoords.IsoCoords.screenTileToWorld(start.x, start.y);\n        const bottomRight = IsoCoords.IsoCoords.screenTileToWorld(end.x, end.y);\n        const bottomLeft = IsoCoords.IsoCoords.screenTileToWorld(end.x, start.y);\n        const topRight = IsoCoords.IsoCoords.screenTileToWorld(start.x, end.y);\n        const material = new THREE.LineBasicMaterial({\n            color: color,\n            transparent: true,\n            depthTest: false,\n            depthWrite: false,\n        });\n        const geometry = new THREE.BufferGeometry();\n        const verts = new Float32Array([\n            topLeft.x, 0, topLeft.y,\n            topRight.x, 0, topRight.y,\n            bottomRight.x, 0, bottomRight.y,\n            bottomLeft.x, 0, bottomLeft.y,\n            topLeft.x, 0, topLeft.y,\n        ]);\n        geometry.setAttribute('position', new THREE.BufferAttribute(verts, 3));\n        this.disposables.add(geometry, material);\n        const line = new THREE.Line(geometry, material);\n        line.renderOrder = 1000000;\n        return line;\n    }\n    public get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    public create3DObject(): void {\n        if (!this.target) {\n            const container = new THREE.Object3D();\n            container.matrixAutoUpdate = false;\n            container.name = \"map_bounds\";\n            container.visible = this.withVisibility.isVisible();\n            this.target = container;\n            if (!this.wrapperObj && container.visible) {\n                this.wrapperObj = this.build();\n                this.target.add(this.wrapperObj);\n            }\n        }\n    }\n    public update(): void { }\n    public setVisible(visible: boolean): void {\n        if (visible !== this.withVisibility.isVisible()) {\n            this.withVisibility.setVisible(visible);\n            if (this.target) {\n                this.target.visible = visible;\n                if (visible) {\n                    if (!this.wrapperObj) {\n                        this.wrapperObj = this.build();\n                    }\n                    this.target.add(this.wrapperObj);\n                }\n                else if (this.wrapperObj) {\n                    this.target.remove(this.wrapperObj);\n                }\n            }\n        }\n    }\n    public dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/map/MapGrid.ts",
    "content": "import * as Coords from \"@/game/Coords\";\nimport * as IsoCoords from \"@/engine/IsoCoords\";\nimport * as THREE from \"three\";\ninterface Size {\n    width: number;\n    height: number;\n}\nexport class MapGrid {\n    private size: Size;\n    private target: THREE.Object3D;\n    constructor(size: Size) {\n        this.size = size;\n        this.build();\n    }\n    private build(): void {\n        const size = this.size;\n        const tileSize = Coords.Coords.getWorldTileSize();\n        const topLeft = IsoCoords.IsoCoords.screenTileToWorld(0, 0);\n        const bottomRight = IsoCoords.IsoCoords.screenTileToWorld(size.width, size.height);\n        const bottomLeft = IsoCoords.IsoCoords.screenTileToWorld(0, size.height);\n        const topRight = IsoCoords.IsoCoords.screenTileToWorld(size.width, 0);\n        const width = bottomRight.x - topLeft.x;\n        const height = bottomLeft.y - topRight.y;\n        const geometry = new THREE.PlaneGeometry(width, height, width / tileSize, height / tileSize);\n        const material = new THREE.MeshBasicMaterial({\n            color: 9474192,\n            wireframe: true,\n            side: THREE.DoubleSide,\n        });\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.matrixAutoUpdate = false;\n        mesh.rotation.x = Math.PI / 2;\n        mesh.updateMatrix();\n        const container = new THREE.Object3D();\n        container.matrixAutoUpdate = false;\n        container.add(mesh);\n        container.position.x = width / 2;\n        container.position.z = height / 2;\n        container.position.y = -1 * Coords.Coords.ISO_WORLD_SCALE;\n        container.updateMatrix();\n        this.target = container;\n    }\n    public get3DObject(): THREE.Object3D {\n        return this.target;\n    }\n    public create3DObject(): void { }\n    public update(): void { }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/map/MapRenderable.ts",
    "content": "import { MapTileLayer } from \"@/engine/renderable/entity/map/MapTileLayer\";\nimport { MapTileLayerDebug } from \"@/engine/renderable/entity/map/MapTileLayerDebug\";\nimport { MapSurface } from \"@/engine/renderable/entity/map/MapSurface\";\nimport { MapBounds } from \"@/engine/renderable/entity/map/MapBounds\";\nimport { MapShroudLayer } from \"@/engine/renderable/entity/map/MapShroudLayer\";\nimport { ShpAggregator } from \"@/engine/renderable/builder/ShpAggregator\";\nimport { MapSpriteBatchLayer } from \"@/engine/renderable/entity/map/MapSpriteBatchLayer\";\nimport { BridgeOverlayTypes } from \"@/game/map/BridgeOverlayTypes\";\nimport * as THREE from \"three\";\nexport class MapRenderable {\n    private gameObj: any;\n    private mapShroud: any;\n    private mapRadiation: any;\n    private lighting: any;\n    private theater: any;\n    private rules: any;\n    private art: any;\n    private imageFinder: any;\n    private camera: any;\n    private debugWireframe: any;\n    private gameSpeed: any;\n    private worldSound: any;\n    private useSpriteBatching: boolean;\n    private lastDebugValue: boolean = false;\n    private invalidatedRadTiles: Set<any> = new Set();\n    private radTileLights: Map<any, any> = new Map();\n    private _objects: any[] = [];\n    private target: any;\n    private tileLayer: any;\n    private debugLayer: any;\n    private mapSurface: any;\n    private mapBounds: any;\n    private shroudLayer: any;\n    public terrainLayer: any;\n    public overlayLayer: any;\n    public smudgeLayer: any;\n    private handleRadChange = (tiles: any) => {\n        for (const tile of tiles) {\n            this.invalidatedRadTiles.add(tile);\n        }\n    };\n    constructor(gameObj: any, mapShroud: any, mapRadiation: any, lighting: any, theater: any, rules: any, art: any, imageFinder: any, camera: any, debugWireframe: any, gameSpeed: any, worldSound: any, useSpriteBatching: boolean) {\n        this.gameObj = gameObj;\n        this.mapShroud = mapShroud;\n        this.mapRadiation = mapRadiation;\n        this.lighting = lighting;\n        this.theater = theater;\n        this.rules = rules;\n        this.art = art;\n        this.imageFinder = imageFinder;\n        this.camera = camera;\n        this.debugWireframe = debugWireframe;\n        this.gameSpeed = gameSpeed;\n        this.worldSound = worldSound;\n        this.useSpriteBatching = useSpriteBatching;\n        this.init();\n    }\n    get3DObject() {\n        return this.target;\n    }\n    getGameObject() {\n        return this.gameObj;\n    }\n    init() {\n        const gameObject = this.getGameObject();\n        this.tileLayer = new MapTileLayer(gameObject, this.theater, this.art, this.imageFinder, this.camera, this.debugWireframe, this.gameSpeed, this.worldSound, this.lighting, this.useSpriteBatching);\n        this.addObject(this.tileLayer);\n        this.debugLayer = new MapTileLayerDebug(gameObject, this.theater, this.camera);\n        this.debugLayer.setVisible(false);\n        this.addObject(this.debugLayer);\n        this.mapSurface = new MapSurface(gameObject, this.theater);\n        this.addObject(this.mapSurface);\n        this.mapBounds = new MapBounds(gameObject);\n        this.mapBounds.setVisible(false);\n        if (this.mapShroud) {\n            this.shroudLayer = new MapShroudLayer(this.mapShroud, this.imageFinder, this.camera);\n            this.addObject(this.shroudLayer);\n        }\n        this.addObject(this.mapBounds);\n        const shpAggregator = new ShpAggregator();\n        this.terrainLayer = new MapSpriteBatchLayer(\"map_terrain_layer\", [...this.rules.terrainRules.values()].filter((rule: any) => !rule.isAnimated && this.art.hasObject(rule.name, rule.type)), () => false as any, this.theater, this.art, this.imageFinder, this.camera, this.lighting, shpAggregator);\n        this.addObject(this.terrainLayer);\n        this.overlayLayer = new MapSpriteBatchLayer(\"map_overlay_layer\", [...this.rules.overlayRules.values()].filter((rule: any) => this.art.hasObject(rule.name, rule.type) &&\n            !BridgeOverlayTypes.isBridge(this.rules.getOverlayId(rule.name))), (rule: any) => rule.rules.wall, this.theater, this.art, this.imageFinder, this.camera, this.lighting, shpAggregator);\n        this.overlayLayer.meshRenderOrder = -1;\n        this.overlayLayer.meshNoDepth = true;\n        this.addObject(this.overlayLayer);\n        this.smudgeLayer = new MapSpriteBatchLayer(\"map_smudge_layer\", [...this.rules.smudgeRules.values()].filter((rule: any) => this.art.hasObject(rule.name, rule.type)), () => false as any, this.theater, this.art, this.imageFinder, this.camera, this.lighting, shpAggregator);\n        this.smudgeLayer.meshRenderOrder = -1;\n        this.smudgeLayer.meshNoDepth = true;\n        this.addObject(this.smudgeLayer);\n        this.mapRadiation.onChange.subscribe(this.handleRadChange);\n    }\n    setShroud(shroud: any) {\n        if (shroud !== this.mapShroud) {\n            if (!shroud && this.shroudLayer) {\n                this.removeObject(this.shroudLayer);\n                this.shroudLayer.dispose();\n                this.shroudLayer = undefined;\n            }\n            this.mapShroud = shroud;\n            if (this.mapShroud) {\n                if (this.shroudLayer) {\n                    this.shroudLayer.setShroud(this.mapShroud);\n                }\n                else {\n                    this.shroudLayer = new MapShroudLayer(this.mapShroud, this.imageFinder, this.camera);\n                    this.addObject(this.shroudLayer);\n                }\n            }\n        }\n    }\n    addObject(obj: any) {\n        this._objects.push(obj);\n        if (this.target) {\n            obj.create3DObject();\n            this.target.add(obj.get3DObject());\n        }\n    }\n    removeObject(obj: any) {\n        const index = this._objects.indexOf(obj);\n        if (index !== -1) {\n            this._objects.splice(index, 1);\n            if (this.target && obj.get3DObject()) {\n                this.target.remove(obj.get3DObject());\n            }\n        }\n    }\n    create3DObject() {\n        let target = this.get3DObject();\n        if (!target) {\n            target = new (THREE as any).Object3D();\n            target.name = \"map\";\n            target.matrixAutoUpdate = false;\n            this.target = target;\n            for (let i = 0, length = this._objects.length; i < length; ++i) {\n                this._objects[i].create3DObject();\n                target.add(this._objects[i].get3DObject());\n            }\n        }\n    }\n    update(deltaTime: number, ...args: any[]) {\n        const gameTime = args[0];\n        this.create3DObject();\n        if (this.debugWireframe.value !== this.lastDebugValue) {\n            this.lastDebugValue = this.debugWireframe.value;\n            this.debugLayer.setVisible(this.debugWireframe.value);\n            this.mapBounds.setVisible(this.debugWireframe.value);\n        }\n        this._objects.forEach((obj: any) => obj.update(deltaTime, gameTime));\n        if (this.invalidatedRadTiles.size) {\n            for (const tile of this.invalidatedRadTiles) {\n                const radLevel = this.mapRadiation.getRadLevel(tile);\n                if (radLevel) {\n                    const intensity = Math.min(1, radLevel / this.rules.radiation.radLevelMax);\n                    if (this.radTileLights.has(tile)) {\n                        this.lighting.removeTileLight(tile, this.radTileLights.get(tile));\n                    }\n                    const radColor = this.rules.radiation.radColor;\n                    const lightData = {\n                        intensity: this.rules.radiation.radLightFactor * intensity,\n                        red: (radColor[0] / 255) * intensity,\n                        green: (radColor[1] / 255) * intensity,\n                        blue: (radColor[2] / 255) * intensity,\n                    };\n                    this.lighting.addTileLight(tile, lightData);\n                    this.radTileLights.set(tile, lightData);\n                }\n                else {\n                    this.lighting.removeTileLight(tile, this.radTileLights.get(tile));\n                    this.radTileLights.delete(tile);\n                }\n            }\n            this.lighting.forceUpdate([...this.invalidatedRadTiles]);\n            this.invalidatedRadTiles.clear();\n        }\n    }\n    updateLighting(lightingData: any) {\n        this.tileLayer.updateLighting(lightingData);\n        this.terrainLayer.updateLighting();\n        this.overlayLayer.updateLighting();\n        this.smudgeLayer.updateLighting();\n    }\n    dispose() {\n        this.mapRadiation.onChange.unsubscribe(this.handleRadChange);\n        this.tileLayer.dispose();\n        this.debugLayer.dispose();\n        this.terrainLayer.dispose();\n        this.overlayLayer.dispose();\n        this.smudgeLayer.dispose();\n        this.shroudLayer?.dispose();\n        this.mapBounds.dispose();\n        this.mapSurface.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/map/MapShroudLayer.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { TextureUtils } from \"@/engine/gfx/TextureUtils\";\nimport { SpriteUtils } from \"@/engine/gfx/SpriteUtils\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { ShpTextureAtlas } from \"@/engine/renderable/builder/ShpTextureAtlas\";\nimport { Palette } from \"@/data/Palette\";\nimport { Color } from \"@/util/Color\";\nimport { MapShroud, ShroudType } from \"@/game/map/MapShroud\";\nimport { BufferGeometryUtils } from \"@/engine/gfx/BufferGeometryUtils\";\nimport { PaletteBasicMaterial } from \"@/engine/gfx/material/PaletteBasicMaterial\";\nimport { Engine } from \"@/engine/Engine\";\nimport * as THREE from \"three\";\nconst edgeFrameMap = [\n    [1, 32],\n    [4, 33],\n    [8, 34],\n    [2, 35],\n    [5, 36],\n    [12, 37],\n    [10, 38],\n    [3, 39],\n    [13, 40],\n    [14, 41],\n    [11, 42],\n    [7, 43],\n    [9, 44],\n    [6, 45],\n    [15, 46],\n].reduce((map, [key, value]) => ((map[key] = value), map), new Array(16).fill(undefined));\nconst cornerFrameMap = [\n    [24, 16],\n    [34, 17],\n    [50, 18],\n    [65, 19],\n    [97, 20],\n    [132, 21],\n    [152, 22],\n    [196, 23],\n    [18, 24],\n    [33, 25],\n    [68, 26],\n    [136, 27],\n    [26, 28],\n    [35, 29],\n    [69, 30],\n    [140, 31],\n].reduce((map, [key, value]) => ((map[key] = value), map), new Array(256).fill(undefined));\nconst edgeMaskTable = [0, 5, 12, 13, 10, 15, 14, 15, 3, 7, 15, 15, 11, 15, 15, 15];\nexport class MapShroudLayer {\n    private shroud: any;\n    private imageFinder: any;\n    private camera: any;\n    private disposables: CompositeDisposable;\n    private needsIncrementalUpdate: any[] = [];\n    private needsFullUpdate: boolean | string = false;\n    private target: any;\n    private uvAttribute: any;\n    private uvElemsPerPiece: number;\n    private uvLookup: Float32Array;\n    constructor(shroud: any, imageFinder: any, camera: any) {\n        this.shroud = shroud;\n        this.imageFinder = imageFinder;\n        this.camera = camera;\n        this.disposables = new CompositeDisposable();\n        this.needsIncrementalUpdate = [];\n        this.needsFullUpdate = false;\n        this.onShroudChange = (event: any) => {\n            if (event.type === \"incremental\") {\n                this.needsIncrementalUpdate.push(...event.coords);\n            }\n            else {\n                this.needsFullUpdate = event.type;\n            }\n        };\n        this.camera = camera;\n    }\n    private onShroudChange: (event: any) => void;\n    get3DObject() {\n        return this.target;\n    }\n    create3DObject() {\n        let object3D = this.get3DObject();\n        if (!object3D) {\n            object3D = new (THREE as any).Object3D();\n            object3D.name = \"map_shroud_layer\";\n            object3D.matrixAutoUpdate = false;\n            this.target = object3D;\n            this.createTileObjects(object3D);\n            this.shroud.onChange.subscribe(this.onShroudChange);\n            this.disposables.add(() => this.shroud.onChange.unsubscribe(this.onShroudChange));\n        }\n    }\n    setShroud(shroud: any) {\n        this.shroud.onChange.unsubscribe(this.onShroudChange);\n        this.shroud = shroud;\n        this.shroud.onChange.subscribe(this.onShroudChange);\n        this.needsFullUpdate = \"full\";\n    }\n    createTileObjects(parent: any) {\n        const shroudFile = this.imageFinder.find(Engine.shroudFileName.split(\".\")[0], false);\n        let textureAtlas = new ShpTextureAtlas().fromShpFile(shroudFile);\n        this.disposables.add(textureAtlas);\n        let palette = new Palette();\n        let colors = [new Color(0, 0, 0), new Color(0, 0, 0)];\n        for (let i = 0; i < 254; i++) {\n            const alpha = Math.min(255, Math.floor((i / 125) * 255));\n            colors.push(new Color(alpha, alpha, alpha));\n        }\n        palette.setColors(colors);\n        const paletteTexture = TextureUtils.textureFromPalette(palette);\n        let geometries = [];\n        let tileCount = 0;\n        const mapSize = this.shroud.getSize();\n        for (let y = 0; y < mapSize.height; y++) {\n            for (let x = 0; x < mapSize.width; x++) {\n                const shroudCoords = { sx: x, sy: y };\n                const frameNo = this.getFrameNo(shroudCoords);\n                const tileGeometry = this.createTileGeometry(shroudCoords, textureAtlas, frameNo);\n                geometries.push(tileGeometry);\n                tileCount++;\n            }\n        }\n        const material = new PaletteBasicMaterial({\n            map: textureAtlas.getTexture(),\n            palette: paletteTexture,\n            alphaTest: 0.01,\n            flatShading: true,\n            transparent: true,\n            premultipliedAlpha: true,\n            depthTest: false,\n            blending: (THREE as any).MultiplyBlending,\n        });\n        let mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);\n        if (mergedGeometry.getAttribute(\"position\").count !==\n            SpriteUtils.VERTICES_PER_SPRITE * tileCount) {\n            throw new Error(\"Vertex count mismatch\");\n        }\n        this.uvAttribute = mergedGeometry.getAttribute(\"uv\");\n        this.uvElemsPerPiece =\n            (this.uvAttribute.count * this.uvAttribute.itemSize) / tileCount;\n        this.uvLookup = new Float32Array(47 * this.uvElemsPerPiece);\n        for (let frameIndex = 0; frameIndex < 47; frameIndex++) {\n            let spriteGeometry = SpriteUtils.createSpriteGeometry(this.getTileGeometryOptions(textureAtlas, frameIndex));\n            this.uvLookup.set(spriteGeometry.getAttribute(\"uv\").array, frameIndex * this.uvElemsPerPiece);\n        }\n        geometries.forEach((geometry) => geometry.dispose());\n        let mesh = new (THREE as any).Mesh(mergedGeometry, material);\n        mesh.renderOrder = 999999;\n        mesh.matrixAutoUpdate = false;\n        mesh.frustumCulled = false;\n        parent.add(mesh);\n        this.disposables.add(mergedGeometry, material);\n    }\n    createTileGeometry(shroudCoords: any, textureAtlas: any, frameNo: number) {\n        const { rx, ry } = this.shroud.shroudCoordsToWorld(shroudCoords);\n        const worldPos = Coords.tile3dToWorld(rx, ry, 0);\n        let spriteGeometry = SpriteUtils.createSpriteGeometry(this.getTileGeometryOptions(textureAtlas, frameNo));\n        spriteGeometry.applyMatrix4(new (THREE as any).Matrix4().makeTranslation(worldPos.x, worldPos.y, worldPos.z));\n        return spriteGeometry;\n    }\n    getTileGeometryOptions(textureAtlas: any, frameNo: number) {\n        return {\n            texture: textureAtlas.getTexture(),\n            textureArea: textureAtlas.getTextureArea(frameNo),\n            flat: true,\n            align: { x: 0, y: -1 },\n            camera: this.camera,\n            scale: Coords.ISO_WORLD_SCALE,\n        };\n    }\n    update(deltaTime: number) {\n        if (this.needsFullUpdate) {\n            if (this.needsFullUpdate === \"cover\" || this.needsFullUpdate === \"clear\") {\n                this.toggleAllTiles(this.needsFullUpdate === \"cover\"\n                    ? ShroudType.Unexplored\n                    : ShroudType.Explored);\n            }\n            else {\n                this.updateAllTiles();\n                this.needsIncrementalUpdate = [];\n            }\n            this.uvAttribute.needsUpdate = true;\n            this.needsFullUpdate = false;\n        }\n        if (this.needsIncrementalUpdate.length) {\n            const tilesToUpdate = this.extendToAdjacentTiles(this.needsIncrementalUpdate);\n            this.updateTiles(tilesToUpdate);\n            this.uvAttribute.needsUpdate = true;\n            this.needsIncrementalUpdate.length = 0;\n        }\n    }\n    extendToAdjacentTiles(coords: any[]) {\n        let tileMap = new Map();\n        const mapSize = this.shroud.getSize();\n        for (const coord of coords) {\n            for (let dx = -1; dx <= 1; dx++) {\n                for (let dy = -1; dy <= 1; dy++) {\n                    const x = coord.sx + dx;\n                    const y = coord.sy + dy;\n                    if (x >= 0 && y >= 0 && x < mapSize.width && y < mapSize.height) {\n                        tileMap.set(x + \"_\" + y, { sx: x, sy: y });\n                    }\n                }\n            }\n        }\n        return [...tileMap.values()];\n    }\n    updateTiles(coords: any[]) {\n        const mapSize = this.shroud.getSize();\n        for (const coord of coords) {\n            const tileIndex = coord.sx + coord.sy * mapSize.width;\n            this.updateTilePiece(tileIndex, this.getFrameNo(coord));\n        }\n    }\n    updateAllTiles() {\n        const mapSize = this.shroud.getSize();\n        for (let y = 0; y < mapSize.height; y++) {\n            for (let x = 0; x < mapSize.width; x++) {\n                const shroudCoords = { sx: x, sy: y };\n                const tileIndex = shroudCoords.sx + shroudCoords.sy * mapSize.width;\n                this.updateTilePiece(tileIndex, this.getFrameNo(shroudCoords));\n            }\n        }\n    }\n    toggleAllTiles(shroudType: any) {\n        const frameNo = shroudType === ShroudType.Unexplored ? 15 : 0;\n        const uvData = this.uvLookup.subarray(frameNo * this.uvElemsPerPiece, (1 + frameNo) * this.uvElemsPerPiece);\n        let uvArray = this.uvAttribute.array;\n        const mapSize = this.shroud.getSize();\n        for (let i = 0, total = mapSize.width * mapSize.height; i < total; i++) {\n            uvArray.set(uvData, i * this.uvElemsPerPiece);\n        }\n    }\n    updateTilePiece(tileIndex: number, frameNo: number) {\n        this.uvAttribute.array.set(this.uvLookup.subarray(frameNo * this.uvElemsPerPiece, (frameNo + 1) * this.uvElemsPerPiece), tileIndex * this.uvElemsPerPiece);\n    }\n    getFrameNo(shroudCoords: any): number {\n        if (this.shroud.getShroudTypeByShroudCoords(shroudCoords) ===\n            ShroudType.Unexplored) {\n            return 15;\n        }\n        let edgeValue = 0;\n        if (this.hasShroudedNeighbour(shroudCoords, 0, -1))\n            edgeValue += 1;\n        if (this.hasShroudedNeighbour(shroudCoords, 1, 0))\n            edgeValue += 2;\n        if (this.hasShroudedNeighbour(shroudCoords, 0, 1))\n            edgeValue += 4;\n        if (this.hasShroudedNeighbour(shroudCoords, -1, 0))\n            edgeValue += 8;\n        let cornerValue = 0;\n        for (let dx = -1; dx <= 1; dx += 2) {\n            for (let dy = -1; dy <= 1; dy += 2) {\n                if (this.hasShroudedNeighbour(shroudCoords, dx, dy)) {\n                    const bitIndex = dx + 1 + ((dy + 1) >> 1);\n                    cornerValue += 1 << bitIndex;\n                }\n            }\n        }\n        if (cornerValue > 0) {\n            if (edgeValue === 0) {\n                edgeValue = edgeFrameMap[cornerValue];\n            }\n            else {\n                const maskedCornerValue = cornerValue & ~edgeMaskTable[edgeValue];\n                if (maskedCornerValue > 0) {\n                    const mappedFrame = cornerFrameMap[maskedCornerValue + (edgeValue << 4)];\n                    if (mappedFrame === undefined) {\n                        throw new Error(`Missing mapped corner frame number for cornerValue \"${cornerValue}\",` +\n                            \"edgeFrameNo=\" +\n                            edgeValue);\n                    }\n                    edgeValue = mappedFrame;\n                }\n            }\n        }\n        return edgeValue;\n    }\n    hasShroudedNeighbour({ sx, sy }: any, dx: number, dy: number): boolean {\n        return (this.shroud.getShroudTypeByShroudCoords({\n            sx: sx + dx,\n            sy: sy + dy,\n        }) === ShroudType.Unexplored);\n    }\n    dispose() {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/map/MapSpriteBatchLayer.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { ImageFinder } from \"@/engine/ImageFinder\";\nimport { BatchShpBuilder } from \"@/engine/renderable/builder/BatchShpBuilder\";\nimport { ShpAggregator } from \"@/engine/renderable/builder/ShpAggregator\";\nimport { MapSpriteTranslation } from \"@/engine/renderable/MapSpriteTranslation\";\nimport { ShadowRenderable } from \"@/engine/renderable/ShadowRenderable\";\nimport { isNotNullOrUndefined } from \"@/util/typeGuard\";\nimport * as THREE from \"three\";\ninterface BatchShpSpec {\n    shpFile: any;\n    frameNo: number;\n    depth: number;\n    flat: boolean;\n    position: THREE.Vector3;\n    offset: THREE.Vector2;\n    lightMult?: THREE.Color;\n}\ninterface ObjectSpecs {\n    main: BatchShpSpec;\n    shadow?: BatchShpSpec;\n}\nexport class MapSpriteBatchLayer {\n    private label: string;\n    private spriteUseDepth: (obj: any) => number;\n    private theater: any;\n    private art: any;\n    private imageFinder: ImageFinder;\n    private camera: any;\n    private lighting: any;\n    private shpAggregator: ShpAggregator;\n    private textureCache: Map<string, any>;\n    private batchShpSpecsByObject: Map<any, ObjectSpecs>;\n    private batchShpBuilders: Map<string, BatchShpBuilder[]>;\n    private shadowBatchShpBuilders: BatchShpBuilder[];\n    private batchedObjectRules: Set<any>;\n    private aggregatedImageData: any;\n    private target?: THREE.Object3D;\n    public meshRenderOrder: number = 0;\n    public meshNoDepth: boolean = false;\n    constructor(label: string, batchedObjectRules: any[], spriteUseDepth: (obj: any) => number, theater: any, art: any, imageFinder: ImageFinder, camera: any, lighting: any, shpAggregator: ShpAggregator) {\n        this.label = label;\n        this.spriteUseDepth = spriteUseDepth;\n        this.theater = theater;\n        this.art = art;\n        this.imageFinder = imageFinder;\n        this.camera = camera;\n        this.lighting = lighting;\n        this.shpAggregator = shpAggregator;\n        this.textureCache = new Map();\n        this.batchShpSpecsByObject = new Map();\n        this.batchShpBuilders = new Map();\n        this.shadowBatchShpBuilders = [];\n        this.batchedObjectRules = new Set(batchedObjectRules);\n        this.aggregatedImageData = this.createAggregatedShpFile(`agg_${label}.shp`);\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    create3DObject(): void {\n        let obj = this.get3DObject();\n        if (!obj) {\n            obj = new THREE.Object3D();\n            obj.name = this.label;\n            obj.matrixAutoUpdate = false;\n            this.target = obj;\n        }\n    }\n    private createAggregatedShpFile(filename: string): any {\n        const shpFrameInfos = [...this.batchedObjectRules.values()]\n            .map((rule) => {\n            const objectArt = this.art.getObject(rule.name, rule.type);\n            let imageData;\n            try {\n                imageData = this.imageFinder.findByObjectArt(objectArt);\n            }\n            catch (error) {\n                if (error instanceof ImageFinder.MissingImageError)\n                    return;\n                throw error;\n            }\n            return ShpAggregator.getShpFrameInfo(imageData, objectArt.hasShadow);\n        })\n            .filter(isNotNullOrUndefined);\n        return this.shpAggregator.aggregate(shpFrameInfos, filename);\n    }\n    update(deltaTime: number): void { }\n    updateLighting(): void {\n        this.batchShpSpecsByObject.forEach((specs, obj) => {\n            specs.main.lightMult?.copy(this.lighting.compute(obj.art.lightingType, obj.tile));\n        });\n        [...this.batchShpBuilders.values()]\n            .flat()\n            .forEach((builder) => builder.updateLighting());\n    }\n    shouldBeBatched(obj: any): boolean {\n        return this.batchedObjectRules.has(obj.rules);\n    }\n    private getBatchKey(obj: any): string {\n        return obj.art.paletteType + \"_\" + obj.art.customPaletteName;\n    }\n    addObject(obj: any): void {\n        const batchKey = this.getBatchKey(obj);\n        let builders = this.batchShpBuilders.get(batchKey);\n        if (!builders) {\n            builders = [];\n            this.batchShpBuilders.set(batchKey, builders);\n        }\n        let availableBuilder = builders.find((builder) => !builder.isFull());\n        if (!availableBuilder) {\n            if (!this.get3DObject())\n                throw new Error(\"Not implemented\");\n            const palette = this.theater.getPalette(obj.art.paletteType, obj.art.customPaletteName);\n            const newBuilder = new BatchShpBuilder(this.aggregatedImageData.file, palette, this.camera, this.textureCache, undefined, undefined, undefined, Coords.ISO_WORLD_SCALE);\n            builders.push(newBuilder);\n            const mesh = newBuilder.build();\n            mesh.renderOrder = this.meshRenderOrder;\n            if (this.meshNoDepth) {\n                const mat = mesh.material as THREE.Material;\n                mat.depthTest = false;\n                mat.depthWrite = false;\n            }\n            this.get3DObject()!.add(mesh);\n            availableBuilder = newBuilder;\n        }\n        const mainSpec = this.buildBatchShpSpec(obj, this.aggregatedImageData);\n        availableBuilder.add(mainSpec as any);\n        let shadowSpec: BatchShpSpec | undefined;\n        if (obj.art.hasShadow) {\n            let shadowBuilder = this.shadowBatchShpBuilders.find((builder) => !builder.isFull());\n            if (!shadowBuilder) {\n                if (!this.get3DObject())\n                    throw new Error(\"Not implemented\");\n                const newShadowBuilder = new BatchShpBuilder(this.aggregatedImageData.file, ShadowRenderable.getOrCreateShadowPalette(), this.camera, this.textureCache, 0.5, true, undefined, Coords.ISO_WORLD_SCALE);\n                this.shadowBatchShpBuilders.push(newShadowBuilder);\n                const shadowMesh = newShadowBuilder.build();\n                shadowMesh.renderOrder = this.meshRenderOrder;\n                if (this.meshNoDepth) {\n                    const mat = shadowMesh.material as THREE.Material;\n                    mat.depthTest = false;\n                    mat.depthWrite = false;\n                }\n                this.get3DObject()!.add(shadowMesh);\n                shadowBuilder = newShadowBuilder;\n            }\n            shadowSpec = this.buildShadowBatchShpSpec(mainSpec, this.aggregatedImageData);\n            shadowBuilder.add(shadowSpec as any);\n        }\n        this.batchShpSpecsByObject.set(obj, { main: mainSpec, shadow: shadowSpec });\n    }\n    private buildBatchShpSpec(obj: any, aggregatedData: any): BatchShpSpec {\n        const foundation = obj.getFoundation();\n        const spriteTranslation = new MapSpriteTranslation(foundation.width, foundation.height);\n        const worldPosition = obj.position.worldPosition.clone();\n        const { spriteOffset, anchorPointWorld } = spriteTranslation.compute();\n        worldPosition.x += anchorPointWorld.x;\n        worldPosition.z += anchorPointWorld.y;\n        const imageData = this.imageFinder.findByObjectArt(obj.art);\n        const imageIndex = aggregatedData.imageIndexes.get(imageData);\n        if (imageIndex === undefined) {\n            throw new Error(\"SHP file not found in aggregated image data\");\n        }\n        return {\n            shpFile: imageData,\n            frameNo: imageIndex,\n            depth: this.spriteUseDepth(obj),\n            flat: obj.art.flat,\n            position: worldPosition,\n            offset: spriteOffset.clone().add(obj.art.getDrawOffset()),\n            lightMult: this.lighting.compute(obj.art.lightingType, obj.tile),\n        };\n    }\n    private buildShadowBatchShpSpec(mainSpec: BatchShpSpec, aggregatedData: any): BatchShpSpec {\n        const imageIndex = aggregatedData.imageIndexes.get(mainSpec.shpFile);\n        if (imageIndex === undefined) {\n            throw new Error(\"SHP file not found in aggregated image data\");\n        }\n        return {\n            ...mainSpec,\n            position: mainSpec.position.clone().add(new THREE.Vector3(0, 0.1, 0)),\n            flat: true,\n            frameNo: imageIndex + aggregatedData.file.numImages / 2,\n            lightMult: undefined,\n        };\n    }\n    removeObject(obj: any): void {\n        const specs = this.batchShpSpecsByObject.get(obj);\n        if (!specs)\n            return;\n        const batchKey = this.getBatchKey(obj);\n        const builders = this.batchShpBuilders.get(batchKey);\n        const mainBuilder = builders?.find((builder) => builder.has(specs.main as any));\n        if (mainBuilder) {\n            mainBuilder.remove(specs.main as any);\n            if (mainBuilder.isEmpty() && builders!.length > 1) {\n                this.get3DObject()?.remove(mainBuilder.build());\n                mainBuilder.dispose();\n                builders?.splice(builders.indexOf(mainBuilder), 1);\n            }\n            if (specs.shadow) {\n                const shadowBuilder = this.shadowBatchShpBuilders.find((builder) => builder.has(specs.shadow as any));\n                shadowBuilder?.remove(specs.shadow as any);\n                if (shadowBuilder?.isEmpty() && this.shadowBatchShpBuilders.length > 1) {\n                    this.get3DObject()?.remove(shadowBuilder.build());\n                    shadowBuilder.dispose();\n                    this.shadowBatchShpBuilders.splice(this.shadowBatchShpBuilders.indexOf(shadowBuilder), 1);\n                }\n            }\n            this.batchShpSpecsByObject.delete(obj);\n        }\n    }\n    hasObject(obj: any): boolean {\n        return this.batchShpSpecsByObject.has(obj);\n    }\n    getObjectFrameCount(obj: any): number {\n        const specs = this.batchShpSpecsByObject.get(obj);\n        if (!specs) {\n            throw new Error(`Batch SHP spec for object \"${obj.name}\" not found`);\n        }\n        return specs.main.shpFile.numImages * (specs.shadow ? 0.5 : 1);\n    }\n    setObjectFrame(obj: any, frameIndex: number): void {\n        const specs = this.batchShpSpecsByObject.get(obj);\n        if (!specs) {\n            throw new Error(`Batch SHP spec for object \"${obj.name}\" not found`);\n        }\n        if (frameIndex >= specs.main.shpFile.numImages * (specs.shadow ? 0.5 : 1)) {\n            return;\n        }\n        const baseImageIndex = this.aggregatedImageData.imageIndexes.get(specs.main.shpFile);\n        specs.main.frameNo = baseImageIndex + frameIndex;\n        if (specs.shadow) {\n            specs.shadow.frameNo = specs.main.frameNo + this.aggregatedImageData.file.numImages / 2;\n        }\n        const batchKey = this.getBatchKey(obj);\n        const mainBuilder = this.batchShpBuilders\n            .get(batchKey)\n            ?.find((builder) => builder.has(specs.main as any));\n        mainBuilder?.update(specs.main as any);\n        if (specs.shadow) {\n            const shadowBuilder = this.shadowBatchShpBuilders.find((builder) => builder.has(specs.shadow as any));\n            shadowBuilder?.update(specs.shadow as any);\n        }\n    }\n    dispose(): void {\n        [\n            ...this.batchShpBuilders.values(),\n            ...this.shadowBatchShpBuilders,\n        ]\n            .flat()\n            .forEach((builder) => builder.dispose());\n        [...this.textureCache.values()].forEach((texture) => texture.dispose());\n        this.textureCache.clear();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/map/MapSurface.ts",
    "content": "import * as Coords from \"@/game/Coords\";\nimport * as rampHeights from \"@/game/theater/rampHeights\";\nimport * as BufferGeometryUtils from \"@/engine/gfx/BufferGeometryUtils\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport * as THREE from \"three\";\nexport const MAGIC_OFFSET = 0.05;\nexport class MapSurface {\n    private visible: boolean = true;\n    private disposables: CompositeDisposable;\n    private map: any;\n    private theater: any;\n    private target?: THREE.Object3D;\n    constructor(map: any, theater: any) {\n        this.disposables = new CompositeDisposable();\n        this.map = map;\n        this.theater = theater;\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    create3DObject(): void {\n        let obj = this.get3DObject();\n        if (!obj) {\n            obj = this.createObject();\n            obj.name = \"map_surface_shadow\";\n            obj.matrixAutoUpdate = false;\n            obj.visible = this.visible;\n            this.target = obj;\n        }\n    }\n    update(): void { }\n    setVisible(visible: boolean): void {\n        this.visible = visible;\n        if (this.target) {\n            this.target.visible = visible;\n        }\n    }\n    private createObject(): THREE.Mesh {\n        const geometries: THREE.BufferGeometry[] = [];\n        const tiles = this.map.tiles;\n        tiles.forEach((tile: any) => {\n            const pos = Coords.Coords.tile3dToWorld(tile.rx, tile.ry, tile.z);\n            const geometry = this.createRectGeometry(tile.rampType);\n            geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(pos.x, pos.y + MAGIC_OFFSET, pos.z));\n            geometries.push(geometry);\n        });\n        const mergedGeometry = BufferGeometryUtils.BufferGeometryUtils.mergeBufferGeometries(geometries);\n        const material = new THREE.ShadowMaterial();\n        material.transparent = true;\n        material.opacity = 0.5;\n        const mesh = new THREE.Mesh(mergedGeometry, material);\n        mesh.receiveShadow = true;\n        mesh.renderOrder = 5;\n        mesh.frustumCulled = false;\n        this.disposables.add(mergedGeometry, material);\n        return mesh;\n    }\n    private createRectGeometry(rampType: number): THREE.BufferGeometry {\n        const tileSize = Coords.Coords.getWorldTileSize();\n        const heights = rampHeights.rampHeights[rampType];\n        const geometry = new THREE.BufferGeometry();\n        const positions = new Float32Array([\n            0, Coords.Coords.tileHeightToWorld(heights[0]), tileSize,\n            tileSize, Coords.Coords.tileHeightToWorld(heights[3]), tileSize,\n            0, Coords.Coords.tileHeightToWorld(heights[1]), 0,\n            tileSize, Coords.Coords.tileHeightToWorld(heights[2]), 0,\n        ]);\n        const indices = new Uint16Array([0, 1, 2, 3, 2, 1]);\n        geometry.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3));\n        geometry.setIndex(new THREE.BufferAttribute(indices, 1));\n        return geometry;\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/map/MapTileLayer.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { TextureUtils } from \"@/engine/gfx/TextureUtils\";\nimport { TmpDrawable } from \"@/engine/gfx/drawable/TmpDrawable\";\nimport { TextureAtlas } from \"@/engine/gfx/TextureAtlas\";\nimport { SpriteUtils } from \"@/engine/gfx/SpriteUtils\";\nimport { Anim } from \"@/engine/renderable/entity/Anim\";\nimport { LightingType } from \"@/engine/type/LightingType\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { BufferGeometryUtils } from \"@/engine/gfx/BufferGeometryUtils\";\nimport { PaletteBasicMaterial } from \"@/engine/gfx/material/PaletteBasicMaterial\";\nimport { getRandomInt } from \"@/util/math\";\nimport * as THREE from \"three\";\nexport class MapTileLayer {\n    private theater: any;\n    private art: any;\n    private imageFinder: any;\n    private camera: any;\n    private debugFrame: any;\n    private gameSpeed: any;\n    private worldSound: any;\n    private lighting: any;\n    private useSpriteBatching: any;\n    private tileIndexes: Map<any, any>;\n    private tileAnimLightMultsByTile: Map<any, any>;\n    private disposables: CompositeDisposable;\n    private allTiles: any[];\n    private target: any;\n    private colorMultAttribute: any;\n    private anims: any[];\n    constructor(mapData: any, theater: any, art: any, imageFinder: any, camera: any, debugFrame: any, gameSpeed: any, worldSound: any, lighting: any, useSpriteBatching: any) {\n        this.theater = theater;\n        this.art = art;\n        this.imageFinder = imageFinder;\n        this.camera = camera;\n        this.debugFrame = debugFrame;\n        this.gameSpeed = gameSpeed;\n        this.worldSound = worldSound;\n        this.lighting = lighting;\n        this.useSpriteBatching = useSpriteBatching;\n        this.tileIndexes = new Map();\n        this.tileAnimLightMultsByTile = new Map();\n        this.disposables = new CompositeDisposable();\n        this.allTiles = mapData.tiles.getAll();\n    }\n    get3DObject(): any {\n        return this.target;\n    }\n    create3DObject(): void {\n        let object3D = this.get3DObject();\n        if (!object3D) {\n            object3D = new (THREE as any).Object3D();\n            object3D.name = \"map_tile_layer\";\n            object3D.matrixAutoUpdate = false;\n            this.target = object3D;\n            this.createTileObjects(object3D);\n        }\n    }\n    createTileObjects(parent: any): void {\n        try {\n            console.log('[MapTileLayer] createTileObjects start');\n        }\n        catch { }\n        const tmpImageMap = new Map();\n        const tileImageMap = new Map();\n        const isoPalette = this.theater.isoPalette;\n        const paletteTexture = TextureUtils.textureFromPalette(isoPalette);\n        const tileSets = this.theater.tileSets;\n        const validTiles: any[] = [];\n        for (const tile of this.allTiles) {\n            const tileNum = tile.tileNum;\n            const tileData = tileSets.getTile(tileNum);\n            if (!tileData) {\n                try {\n                    console.warn('[MapTileLayer] missing tileData for tile', tile);\n                }\n                catch { }\n                ;\n                continue;\n            }\n            const tmpFile = tileData.getTmpFile(tile.subTile, getRandomInt);\n            if (!tmpFile || tile.subTile >= tmpFile.images.length) {\n                try {\n                    console.warn('[MapTileLayer] bad tmpFile or subTile', { tile, tmpFileExists: !!tmpFile });\n                }\n                catch { }\n                ;\n                continue;\n            }\n            const tmpImage = tmpFile.images[tile.subTile];\n            tileImageMap.set(tile, tmpImage);\n            validTiles.push(tile);\n            if (!tmpImageMap.get(tmpImage)) {\n                const drawable = new TmpDrawable().draw(tmpImage, tmpFile.blockWidth, tmpFile.blockHeight);\n                tmpImageMap.set(tmpImage, drawable);\n            }\n        }\n        const textureAtlas = new TextureAtlas();\n        const drawables: any[] = [];\n        tmpImageMap.forEach((drawable) => {\n            drawables.push(drawable);\n        });\n        textureAtlas.pack(drawables);\n        try {\n            console.log('[MapTileLayer] textureAtlas packed', { drawables: drawables.length });\n        }\n        catch { }\n        this.disposables.add(textureAtlas);\n        const geometries: any[] = [];\n        const lightingData: number[] = [];\n        for (let i = 0; i < validTiles.length; i++) {\n            const tile = validTiles[i];\n            const tmpImage = tileImageMap.get(tile)!;\n            let offsetX = 0;\n            let offsetY = 0;\n            if (tmpImage.hasExtraData) {\n                offsetX += Math.max(0, tmpImage.x - tmpImage.extraX);\n                offsetY += Math.max(0, tmpImage.y - tmpImage.extraY);\n            }\n            const worldPos = Coords.tile3dToWorld(tile.rx, tile.ry, tile.z);\n            const drawable = tmpImageMap.get(tmpImage);\n            const spriteGeometry = SpriteUtils.createSpriteGeometry({\n                texture: textureAtlas.getTexture(),\n                textureArea: textureAtlas.getImageRect(drawable),\n                align: { x: 0, y: -1 },\n                offset: { x: -offsetX, y: -offsetY },\n                camera: this.camera,\n                scale: Coords.ISO_WORLD_SCALE,\n            });\n            spriteGeometry.applyMatrix4(new (THREE as any).Matrix4().makeTranslation(worldPos.x, worldPos.y, worldPos.z));\n            geometries.push(spriteGeometry);\n            const { x, y, z } = this.lighting.compute(LightingType.Full, tile);\n            lightingData.push(x, y, z);\n            this.tileIndexes.set(tile, i);\n        }\n        const material = new PaletteBasicMaterial({\n            map: textureAtlas.getTexture(),\n            palette: paletteTexture,\n            alphaTest: 0.5,\n            flatShading: true,\n            useVertexColorMult: true,\n        });\n        const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);\n        try {\n            console.log('[MapTileLayer] mergedGeometry', { geometries: geometries.length, vertexCount: mergedGeometry.getAttribute(\"position\").count });\n        }\n        catch { }\n        const vertexCount = mergedGeometry.getAttribute(\"position\").count;\n        const positionAttribute = mergedGeometry.getAttribute(\"position\");\n        const uvAttribute = mergedGeometry.getAttribute(\"uv\");\n        let invalidPositionValues = 0;\n        for (let i = 0; i < positionAttribute.array.length; i++) {\n            if (!Number.isFinite(positionAttribute.array[i])) {\n                invalidPositionValues++;\n            }\n        }\n        let invalidUvValues = 0;\n        for (let i = 0; i < uvAttribute.array.length; i++) {\n            if (!Number.isFinite(uvAttribute.array[i])) {\n                invalidUvValues++;\n            }\n        }\n        console.log('[MapTileLayer] geometry sanity', {\n            invalidPositionValues,\n            invalidUvValues,\n        });\n        if (vertexCount !== (SpriteUtils.VERTICES_PER_SPRITE * lightingData.length) / 3) {\n            throw new Error(\"Vertex count mismatch\");\n        }\n        const colorMultBuffer = new Float32Array(4 * vertexCount);\n        this.updateColorMultBuffer(lightingData, colorMultBuffer);\n        const colorMultAttribute = new (THREE as any).BufferAttribute(colorMultBuffer, 4);\n        mergedGeometry.setAttribute(\"vertexColorMult\", colorMultAttribute);\n        this.colorMultAttribute = colorMultAttribute;\n        geometries.forEach((geometry) => geometry.dispose());\n        const mesh = new (THREE as any).Mesh(mergedGeometry, material);\n        mesh.matrixAutoUpdate = false;\n        mesh.frustumCulled = false;\n        mesh.renderOrder = -2;\n        try {\n            const mapTex: any = (material as any).map;\n            const palTex: any = (material as any).uniforms?.palette?.value;\n            const uvAttr: any = mergedGeometry.getAttribute(\"uv\");\n            console.log('[MapTileLayer] material debug', {\n                materialType: (material as any).type,\n                hasMap: !!mapTex,\n                mapSize: mapTex && mapTex.image ? { w: mapTex.image.width, h: mapTex.image.height } : null,\n                mapFlipY: mapTex ? mapTex.flipY : undefined,\n                paletteReady: !!palTex,\n                paletteSize: palTex && palTex.image ? { w: palTex.image.width, h: palTex.image.height } : null,\n                paletteFlipY: palTex ? palTex.flipY : undefined,\n                hasUV: !!uvAttr,\n                uvCount: uvAttr ? uvAttr.count : 0,\n                defines: (material as any).defines,\n            });\n        }\n        catch { }\n        parent.add(mesh);\n        this.disposables.add(mergedGeometry, material);\n        const animations: any[] = [];\n        for (const tile of validTiles) {\n            const tileNum = tile.tileNum;\n            const tileData = tileSets.getTile(tileNum);\n            if (!tileData)\n                return;\n            const animData = tileData.getAnimation();\n            if (animData && tile.subTile === animData.subTile) {\n                const lightMult = this.lighting\n                    .compute(LightingType.Full, tile)\n                    .addScalar(-1);\n                this.tileAnimLightMultsByTile.set(tile, lightMult);\n                const anim = new Anim(animData.name, this.art.getAnimation(animData.name), {\n                    x: animData.offsetX,\n                    y: animData.offsetY + (Coords.ISO_TILE_SIZE + 1) / 2,\n                }, this.imageFinder, this.theater, this.camera, this.debugFrame, this.gameSpeed, this.useSpriteBatching, lightMult, this.worldSound, isoPalette);\n                const worldPos = Coords.tile3dToWorld(tile.rx, tile.ry, tile.z);\n                anim.setPosition(worldPos);\n                anim.create3DObject();\n                animations.push(anim);\n                parent.add(anim.get3DObject());\n                this.disposables.add(anim);\n            }\n        }\n        this.anims = animations;\n    }\n    update(deltaTime: number): void {\n        for (const anim of this.anims) {\n            anim.update(deltaTime);\n        }\n    }\n    updateLighting(tiles?: any[]): void {\n        if (tiles) {\n            for (const tile of tiles) {\n                const tileIndex = this.tileIndexes.get(tile);\n                if (tileIndex !== undefined) {\n                    const { x, y, z } = this.lighting.compute(LightingType.Full, tile);\n                    this.updateColorMultBufferAtIndex(tileIndex, x, y, z, this.colorMultAttribute.array);\n                }\n                const animLightMult = this.tileAnimLightMultsByTile.get(tile);\n                if (animLightMult) {\n                    animLightMult.copy(this.lighting.compute(LightingType.Full, tile));\n                }\n            }\n            this.colorMultAttribute.needsUpdate = true;\n        }\n        else {\n            const lightingData: number[] = [];\n            for (const tile of this.allTiles) {\n                const { x, y, z } = this.lighting.compute(LightingType.Full, tile);\n                lightingData.push(x, y, z);\n            }\n            this.updateColorMultBuffer(lightingData, this.colorMultAttribute.array);\n            this.colorMultAttribute.needsUpdate = true;\n            this.tileAnimLightMultsByTile.forEach((lightMult, tile) => {\n                lightMult.copy(this.lighting.compute(LightingType.Full, tile));\n            });\n        }\n    }\n    private updateColorMultBuffer(lightingData: number[], buffer: Float32Array): void {\n        const verticesPerSprite = SpriteUtils.VERTICES_PER_SPRITE;\n        const tileCount = lightingData.length / 3;\n        let bufferIndex = 0;\n        for (let i = 0; i < tileCount; i++) {\n            const r = lightingData[3 * i];\n            const g = lightingData[3 * i + 1];\n            const b = lightingData[3 * i + 2];\n            for (let j = 0; j < verticesPerSprite; j++) {\n                buffer[bufferIndex++] = r;\n                buffer[bufferIndex++] = g;\n                buffer[bufferIndex++] = b;\n                buffer[bufferIndex++] = 1;\n            }\n        }\n    }\n    private updateColorMultBufferAtIndex(tileIndex: number, r: number, g: number, b: number, buffer: Float32Array): void {\n        const verticesPerSprite = SpriteUtils.VERTICES_PER_SPRITE;\n        let bufferIndex = tileIndex * verticesPerSprite * 4;\n        for (let i = 0; i < verticesPerSprite; i++) {\n            buffer[bufferIndex++] = r;\n            buffer[bufferIndex++] = g;\n            buffer[bufferIndex++] = b;\n            buffer[bufferIndex++] = 1;\n        }\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/map/MapTileLayerDebug.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { rampHeights } from \"@/game/theater/rampHeights\";\nimport { SpriteUtils } from \"@/engine/gfx/SpriteUtils\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { BufferGeometryUtils } from \"@/engine/gfx/BufferGeometryUtils\";\nimport { IsoCoords } from \"@/engine/IsoCoords\";\nimport * as THREE from \"three\";\nexport class MapTileLayerDebug {\n    private _textureTilesNo: number = 20;\n    public visible: boolean = true;\n    public needsLinesUpdate: boolean = false;\n    private disposables: CompositeDisposable;\n    private map: any;\n    private theater: any;\n    private camera: any;\n    private target?: any;\n    private tileOverlay?: any;\n    private lines?: any;\n    private static textureCache?: any;\n    constructor(map: any, theater: any, camera: any) {\n        this.disposables = new CompositeDisposable();\n        this.map = map;\n        this.theater = theater;\n        this.camera = camera;\n    }\n    private handleTileOccupationChanged = () => {\n        this.needsLinesUpdate = true;\n    };\n    get3DObject(): any {\n        return this.target;\n    }\n    create3DObject(): void {\n        let target = this.get3DObject();\n        if (!target) {\n            target = new (THREE as any).Object3D();\n            target.name = \"map_tile_layer_debug\";\n            target.visible = this.visible;\n            target.matrixAutoUpdate = false;\n            if (this.visible) {\n                if (!this.tileOverlay) {\n                    const overlay = this.tileOverlay = this.createTileOverlay();\n                    overlay.matrixAutoUpdate = false;\n                    overlay.frustumCulled = false;\n                    target.add(overlay);\n                }\n                this.setupLines(target);\n            }\n            this.target = target;\n        }\n    }\n    update(): void {\n        if (this.needsLinesUpdate && this.visible) {\n            this.needsLinesUpdate = false;\n            this.destroyLines();\n            this.setupLines(this.target);\n        }\n    }\n    setVisible(visible: boolean): void {\n        if (visible !== this.visible) {\n            this.visible = visible;\n            if (this.target) {\n                this.target.visible = visible;\n                if (this.visible) {\n                    if (!this.tileOverlay) {\n                        const overlay = this.tileOverlay = this.createTileOverlay();\n                        overlay.matrixAutoUpdate = false;\n                        this.target.add(overlay);\n                    }\n                    this.setupLines(this.target);\n                }\n                else {\n                    this.destroyLines();\n                }\n            }\n        }\n    }\n    private setupLines(target: any): void {\n        this.lines = new (THREE as any).Object3D();\n        this.lines.matrixAutoUpdate = false;\n        this.lines.add(this.createConnectivityLines(SpeedType.Foot, false, 0x00ff00));\n        const floatLines = this.createConnectivityLines(SpeedType.Float, false, 0x0000ff);\n        floatLines.position.y = 1;\n        floatLines.updateMatrix();\n        this.lines.add(floatLines);\n        target.add(this.lines);\n        this.map.tileOccupation.onChange.subscribe(this.handleTileOccupationChanged);\n    }\n    private destroyLines(): void {\n        if (this.lines) {\n            this.target.remove(this.lines);\n            this.lines = undefined;\n            this.map.tileOccupation.onChange.unsubscribe(this.handleTileOccupationChanged);\n        }\n    }\n    private createTileOverlay(): any {\n        const geometries: any[] = [];\n        const tiles = this.map.tiles;\n        tiles.forEach((tile: any) => {\n            const worldPos = Coords.tile3dToWorld(tile.rx, tile.ry, tile.z + 1);\n            const tileSize = IsoCoords.getScreenTileSize();\n            const geometry = SpriteUtils.createSpriteGeometry({\n                texture: this.getTileTexture(),\n                textureArea: {\n                    x: tile.z * tileSize.width,\n                    y: 2 * tile.rampType * tileSize.height,\n                    width: tileSize.width,\n                    height: 2 * tileSize.height,\n                },\n                align: { x: 0, y: -1 },\n                camera: this.camera,\n                scale: Coords.ISO_WORLD_SCALE,\n            });\n            geometry.applyMatrix4(new (THREE as any).Matrix4().makeTranslation(worldPos.x, worldPos.y, worldPos.z));\n            geometries.push(geometry);\n        });\n        const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);\n        const material = new (THREE as any).MeshBasicMaterial({\n            map: this.getTileTexture(),\n            alphaTest: 0.5,\n            transparent: true,\n            opacity: 0.7,\n        });\n        this.disposables.add(mergedGeometry, material);\n        return new (THREE as any).Mesh(mergedGeometry, material);\n    }\n    private getTileTexture(): any {\n        let texture = MapTileLayerDebug.textureCache;\n        if (!texture) {\n            const tileSize = IsoCoords.getScreenTileSize();\n            const tilesNo = this._textureTilesNo;\n            const canvas = document.createElement(\"canvas\");\n            const ctx = canvas.getContext(\"2d\");\n            if (!ctx) {\n                throw new Error(\"Could not acquire canvas 2d context\");\n            }\n            canvas.width = tileSize.width * tilesNo;\n            canvas.height = 2 * tileSize.height * rampHeights.length;\n            const screenPos = IsoCoords.tileToScreen(0, 0);\n            screenPos.x += -tileSize.width / 2;\n            const halfTileSize = Coords.ISO_TILE_SIZE / 2;\n            for (let a = 0; a < tilesNo; ++a) {\n                for (let i = 0; i < rampHeights.length; ++i) {\n                    const heights = rampHeights[i];\n                    const corners = [\n                        [0, 1],\n                        [0, 0],\n                        [1, 0],\n                        [1, 1],\n                    ];\n                    const color = 0xff0000 - (a << 11) - (a << 7);\n                    ctx.beginPath();\n                    const firstCorner = IsoCoords.tileToScreen.apply(this, corners[0]);\n                    ctx.moveTo(-screenPos.x + firstCorner.x + a * tileSize.width, -screenPos.y + firstCorner.y + (1 - heights[0]) * halfTileSize + 2 * i * tileSize.height);\n                    for (let t = 1; t < corners.length; ++t) {\n                        const corner = IsoCoords.tileToScreen.apply(this, corners[t]);\n                        ctx.lineTo(-screenPos.x + corner.x + a * tileSize.width, -screenPos.y + corner.y + (1 - heights[t]) * halfTileSize + 2 * i * tileSize.height);\n                    }\n                    ctx.closePath();\n                    ctx.lineWidth = 1;\n                    ctx.fillStyle = \"#\" + color.toString(16);\n                    ctx.fill();\n                    ctx.strokeStyle = \"#\" + (0xffffff - color).toString(16);\n                    ctx.stroke();\n                }\n            }\n            texture = new (THREE as any).Texture(canvas);\n            texture.minFilter = (THREE as any).NearestFilter;\n            texture.magFilter = (THREE as any).NearestFilter;\n            texture.needsUpdate = true;\n            MapTileLayerDebug.textureCache = texture;\n        }\n        return texture;\n    }\n    private createConnectivityLines(speedType: any, includeT: boolean, color: number): any {\n        const graph = this.map.terrain.computePassabilityGraph(speedType, includeT);\n        const points: THREE.Vector3[] = [];\n        const processedConnections = new Set<string>();\n        graph.forEachNode((node: any) => {\n            const sourceNode = node;\n            node.neighbors.forEach((neighbor: any) => {\n                const targetNode = neighbor;\n                const connectionId = sourceNode.id + \"->\" + targetNode.id;\n                if (!processedConnections.has(connectionId)) {\n                    processedConnections.add(connectionId);\n                    points.push(Coords.tile3dToWorld(sourceNode.data.tile.rx + 0.5, sourceNode.data.tile.ry + 0.5, sourceNode.data.tile.z + (sourceNode.data.onBridge?.tileElevation ?? 0)), Coords.tile3dToWorld(targetNode.data.tile.rx + 0.5, targetNode.data.tile.ry + 0.5, targetNode.data.tile.z + (targetNode.data.onBridge?.tileElevation ?? 0)));\n                }\n            });\n        });\n        const geometry = new THREE.BufferGeometry().setFromPoints(points);\n        const material = new (THREE as any).LineBasicMaterial({\n            color: color,\n            transparent: true,\n            depthTest: false,\n            depthWrite: false,\n        });\n        const lineSegments = new (THREE as any).LineSegments(geometry, material);\n        lineSegments.matrixAutoUpdate = false;\n        this.disposables.add(geometry, material);\n        return lineSegments;\n    }\n    onRemove(): void {\n        if (this.lines) {\n            this.destroyLines();\n        }\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/map/MinimapModel.ts",
    "content": "import * as THREE from \"three\";\nimport { MapShroud, ShroudType, ShroudFlag } from \"@/game/map/MapShroud\";\nconst DEFAULT_COLOR = new THREE.Color(\"rgb(173, 170, 132)\");\nconst WALL_COLORS = new Map([\n    [\"CAKRMW\", new THREE.Color(\"rgb(107, 69, 49)\")],\n    [\"CAFNCW\", new THREE.Color(16777215)],\n    [\"CAFNCB\", new THREE.Color(0)],\n    [\"GASAND\", new THREE.Color(\"rgb(82, 77, 57)\")],\n]);\nconst DEFAULT_WALL_COLOR = new THREE.Color(\"rgb(90, 89, 82)\");\nconst RUBBLE_COLOR = new THREE.Color(0);\nconst TIBERIUM_COLOR = new THREE.Color(\"rgb(173, 170, 132)\");\nconst OVERLAY_COLOR = new THREE.Color(0);\nexport class MinimapModel {\n    private tiles: any;\n    private tileOccupation: any;\n    private shroud: MapShroud | undefined;\n    private localPlayer: any;\n    private alliances: any;\n    private paradropRules: any;\n    private stride: number;\n    private tileColors: Uint32Array;\n    private aboveShroudTiles: Uint8Array;\n    private tileWithTechnos: Uint8Array;\n    constructor(tiles: any, tileOccupation: any, shroud: MapShroud | undefined, localPlayer: any, alliances: any, paradropRules: any) {\n        this.tiles = tiles;\n        this.tileOccupation = tileOccupation;\n        this.shroud = shroud;\n        this.localPlayer = localPlayer;\n        this.alliances = alliances;\n        this.paradropRules = paradropRules;\n        const mapSize = this.tiles.getMapSize();\n        this.stride = mapSize.width;\n        this.tileColors = new Uint32Array(mapSize.width * mapSize.height);\n        this.aboveShroudTiles = new Uint8Array(mapSize.width * mapSize.height);\n        this.tileWithTechnos = new Uint8Array(mapSize.width * mapSize.height);\n    }\n    computeAllColors(): void {\n        this.updateColors(this.tiles.getAll());\n    }\n    updateColors(tiles: any[]): void {\n        for (const tile of tiles) {\n            let priority = -1;\n            let topObject: any;\n            for (const obj of this.tileOccupation.getObjectsOnTile(tile)) {\n                const shouldConsider = ((obj.isTechno() || obj.isOverlay() || obj.isTerrain()) &&\n                    !obj.radarInvisible) ||\n                    (obj.isOverlay() && obj.isBridge()) ||\n                    (obj.isBuilding() &&\n                        !obj.rules.invisibleInGame &&\n                        (!obj.radarInvisible ||\n                            (obj.rules.canBeOccupied && obj.owner.isCombatant())));\n                if (shouldConsider) {\n                    const objPriority = 4 * Number(obj.isTechno()) +\n                        2 * Number(obj.isAircraft()) +\n                        Number(obj.name !== this.paradropRules.paradropPlane);\n                    if (objPriority > priority) {\n                        priority = objPriority;\n                        topObject = obj;\n                    }\n                }\n            }\n            let color: THREE.Color | undefined;\n            if (topObject) {\n                if ((topObject.isTechno() || topObject.isOverlay()) && topObject.rules.wall) {\n                    color = WALL_COLORS.get(topObject.name) ?? DEFAULT_WALL_COLOR;\n                }\n                else if (topObject.isBuilding() &&\n                    topObject.isDestroyed &&\n                    topObject.rules.leaveRubble) {\n                    color = RUBBLE_COLOR;\n                }\n                else if (topObject.isTechno()) {\n                    if (topObject.cloakableTrait?.isCloaked() &&\n                        this.localPlayer &&\n                        !this.alliances.haveSharedIntel(this.localPlayer, topObject.owner)) {\n                        color = undefined;\n                    }\n                    else {\n                        const disguise = (topObject.isInfantry() || topObject.isVehicle()) &&\n                            topObject.disguiseTrait?.getDisguise();\n                        color = this.localPlayer &&\n                            disguise &&\n                            !this.alliances.haveSharedIntel(this.localPlayer, topObject.owner) &&\n                            !this.localPlayer.sharedDetectDisguiseTrait?.has(topObject)\n                            ? disguise.owner\n                                ? new THREE.Color(disguise.owner.color.asHex())\n                                : DEFAULT_COLOR\n                            : new THREE.Color(topObject.owner.color.asHex());\n                    }\n                }\n                else if (topObject.isTerrain()) {\n                    color = DEFAULT_COLOR;\n                }\n                else if (topObject.isOverlay()) {\n                    color = topObject.isTiberium() ? TIBERIUM_COLOR : OVERLAY_COLOR;\n                }\n            }\n            color = color || this.tiles.getTileRadarColor(tile);\n            const index = tile.rx + tile.ry * this.stride;\n            this.tileColors[index] = color.getHex();\n            this.aboveShroudTiles[index] =\n                topObject?.name === this.paradropRules.paradropPlane ? 1 : 0;\n            this.tileWithTechnos[index] = topObject?.isTechno() ? 1 : 0;\n        }\n    }\n    getTileColor(tile: any): string {\n        const index = tile.rx + tile.ry * this.stride;\n        if (this.shroud?.getShroudType(tile) === ShroudType.Unexplored &&\n            !this.aboveShroudTiles[index]) {\n            return \"#000000\";\n        }\n        const color = new THREE.Color(this.tileColors[index]);\n        if (this.shroud?.isFlagged(tile, ShroudFlag.Darken) &&\n            !this.tileWithTechnos[index]) {\n            color.multiplyScalar(0.35);\n        }\n        return \"#\" + color.getHexString();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/map/MinimapRenderer.ts",
    "content": "import * as Coords from \"@/game/Coords\";\ninterface DxySize {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface CanvasSize {\n    width: number;\n    height: number;\n}\ninterface Point {\n    x: number;\n    y: number;\n}\nexport class MinimapRenderer {\n    private map: any;\n    private minimapModel: any;\n    private borderColor: string;\n    private canvasRenderScale: number;\n    private dxySize: DxySize;\n    private canvasSize: CanvasSize;\n    private canvas?: HTMLCanvasElement;\n    private ctx?: CanvasRenderingContext2D;\n    constructor(map: any, minimapModel: any, size: CanvasSize, borderColor: string, canvasRenderScale: number) {\n        this.map = map;\n        this.minimapModel = minimapModel;\n        this.borderColor = borderColor;\n        this.canvasRenderScale = canvasRenderScale;\n        const rawSize = this.map.mapBounds.getRawLocalSize();\n        this.dxySize = {\n            x: 2 * rawSize.x,\n            y: 2 * rawSize.y + 4,\n            width: 2 * rawSize.width,\n            height: 2 * rawSize.height + 8,\n        };\n        const aspectRatio = this.dxySize.height / this.dxySize.width;\n        this.canvasSize = this.computeCanvasSize(size, aspectRatio);\n    }\n    private computeCanvasSize(size: CanvasSize, aspectRatio: number): CanvasSize {\n        const { width, height } = size;\n        let result: CanvasSize;\n        if (height / width <= aspectRatio) {\n            result = {\n                width: Math.floor(height / aspectRatio),\n                height: height\n            };\n        }\n        else {\n            result = {\n                width: width,\n                height: Math.floor(width * aspectRatio)\n            };\n        }\n        return result;\n    }\n    public renderFull(): HTMLCanvasElement {\n        if (this.canvas) {\n            this.ctx!.fillStyle = \"black\";\n            this.ctx!.fillRect(0, 0, this.canvasRenderScale * this.canvasSize.width, this.canvasRenderScale * this.canvasSize.height);\n        }\n        else {\n            this.canvas = document.createElement(\"canvas\");\n            this.canvas.width = this.canvasRenderScale * this.canvasSize.width;\n            this.canvas.height = this.canvasRenderScale * this.canvasSize.height;\n            const ctx = this.canvas.getContext(\"2d\", { alpha: false });\n            if (!ctx)\n                throw new Error(\"Failed to get 2D context\");\n            this.ctx = ctx;\n            this.ctx.translate(0.5, 0.5);\n        }\n        this.renderTiles(this.map.tiles.getAll(), true);\n        return this.canvas;\n    }\n    public renderIncremental(tiles: any[]): void {\n        const tileSet = new Set(tiles);\n        for (const tile of tiles) {\n            const neighbors = this.map.tiles.getAllNeighbourTiles(tile);\n            neighbors.forEach(neighbor => tileSet.add(neighbor));\n        }\n        this.renderTiles(tileSet);\n    }\n    private renderTiles(tiles: Set<any> | any[], isFullRender: boolean = false): void {\n        const scale = this.canvasSize.width / this.dxySize.width / Coords.Coords.COS_ISO_CAMERA_BETA;\n        const ctx = this.ctx;\n        if (!ctx) {\n            throw new Error(\"Must do a full render before re-rendering any individual tiles.\");\n        }\n        ctx.imageSmoothingEnabled = false;\n        ctx.save();\n        ctx.rotate(Coords.Coords.ISO_CAMERA_BETA);\n        ctx.scale(scale, scale);\n        for (const tile of tiles) {\n            const color = this.minimapModel.getTileColor(tile);\n            if (!color || (isFullRender && color === \"#000000\"))\n                continue;\n            ctx.fillStyle = color;\n            const { x, y } = this.tileToLocalRxyOrigin(tile);\n            ctx.fillRect(this.canvasRenderScale * x, this.canvasRenderScale * y, this.canvasRenderScale + 0.5, this.canvasRenderScale + 0.5);\n        }\n        ctx.restore();\n        ctx.strokeStyle = this.borderColor;\n        ctx.lineWidth = this.canvasRenderScale;\n        ctx.strokeRect(0, 0, ctx.canvas.width - this.canvasRenderScale, ctx.canvas.height - this.canvasRenderScale);\n    }\n    private tileToLocalRxyOrigin(tile: any): Point {\n        const origin = this.dxyToLocalRxy(this.dxySize.x, this.dxySize.y);\n        return {\n            x: tile.rx - origin.x,\n            y: tile.ry - this.map.mapBounds.getFullSize().width / 2 - origin.y\n        };\n    }\n    private dxyToLocalRxy(x: number, y: number): Point {\n        return {\n            x: (x + y) / 2,\n            y: (y - x) / 2\n        };\n    }\n    public dxyToCanvas(x: number, y: number): Point {\n        const scale = this.canvasSize.width / this.dxySize.width;\n        return {\n            x: (x - this.dxySize.x) * scale,\n            y: (y - this.dxySize.y) * scale\n        };\n    }\n    public canvasToDxy(x: number, y: number): Point {\n        const scale = this.canvasSize.width / this.dxySize.width;\n        return {\n            x: x / scale + this.dxySize.x,\n            y: y / scale + this.dxySize.y\n        };\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/plugin/ChronoSparkleFxPlugin.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nexport class ChronoSparkleFxPlugin {\n    private gameObject: any;\n    private sparkleAnimName: string;\n    private objMoveTrait?: any;\n    private renderableManager?: any;\n    private chronoSparkleAnim?: any;\n    private lastTeleport?: number;\n    private lastWarpedOut?: boolean;\n    constructor(gameObject: any, sparkleAnimName: string) {\n        this.gameObject = gameObject;\n        this.sparkleAnimName = sparkleAnimName;\n        this.objMoveTrait = gameObject.isUnit() ? gameObject.moveTrait : undefined;\n    }\n    onCreate(renderableManager: any): void {\n        this.renderableManager = renderableManager;\n    }\n    update(): void {\n        if (!this.gameObject.isDestroyed &&\n            !this.gameObject.isCrashing &&\n            this.renderableManager) {\n            const lastTeleportTick = this.objMoveTrait?.lastTeleportTick;\n            const isTeleportChanged = lastTeleportTick !== this.lastTeleport;\n            const isWarpedOut = this.gameObject.warpedOutTrait.isActive();\n            if (isWarpedOut !== this.lastWarpedOut || isTeleportChanged) {\n                this.lastTeleport = lastTeleportTick;\n                this.lastWarpedOut = isWarpedOut;\n                if (isWarpedOut || isTeleportChanged) {\n                    this.chronoSparkleAnim?.endAnimationLoop();\n                    this.chronoSparkleAnim = this.renderableManager.createTransientAnim(this.sparkleAnimName, (anim: any) => {\n                        anim.extraOffset = {\n                            x: 0,\n                            y: Coords.ISO_TILE_SIZE / 2,\n                        };\n                        anim.setPosition(this.gameObject.position.worldPosition.clone());\n                        anim.create3DObject();\n                        anim.getAnimProps().loopCount = isWarpedOut ? -1 : 1;\n                    });\n                }\n                else if (!isWarpedOut) {\n                    this.chronoSparkleAnim?.endAnimationLoop();\n                }\n            }\n        }\n    }\n    onRemove(): void {\n        this.renderableManager = undefined;\n        this.chronoSparkleAnim?.endAnimationLoop();\n    }\n    dispose(): void { }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/plugin/DamageSmokePlugin.ts",
    "content": "import { DamageSmokeFx } from \"@/engine/renderable/fx/DamageSmokeFx\";\nexport class DamageSmokePlugin {\n    private gameObject: any;\n    private art: any;\n    private theater: any;\n    private imageFinder: any;\n    private gameSpeed: any;\n    private renderableManager?: any;\n    private smokeFx?: DamageSmokeFx;\n    private lastDamaged?: boolean;\n    private smokeStartTime?: number;\n    constructor(gameObject: any, art: any, theater: any, imageFinder: any, gameSpeed: any) {\n        this.gameObject = gameObject;\n        this.art = art;\n        this.theater = theater;\n        this.imageFinder = imageFinder;\n        this.gameSpeed = gameSpeed;\n    }\n    onCreate(renderableManager: any): void {\n        this.renderableManager = renderableManager;\n    }\n    update(time: number): void {\n        if (!this.renderableManager)\n            return;\n        const isDamaged = this.gameObject.healthTrait.health < 50;\n        const isDamagedChanged = isDamaged !== this.lastDamaged;\n        const isDestroyed = this.gameObject.isDestroyed;\n        if (isDamagedChanged || isDestroyed) {\n            this.lastDamaged = isDamaged;\n            if (isDamaged) {\n                if (!this.smokeFx) {\n                    this.smokeStartTime = time;\n                    const anim = this.art.getAnimation(\"SGRYSMK1\");\n                    if (anim) {\n                        const image = this.imageFinder.findByObjectArt(anim);\n                        const palette = this.theater.getPalette(anim.paletteType);\n                        this.smokeFx = new DamageSmokeFx(this.gameObject, anim, image, palette, this.gameSpeed);\n                        this.renderableManager.addEffect(this.smokeFx);\n                    }\n                }\n            }\n            else {\n                this.disposeSmokeFx();\n            }\n        }\n        if (this.smokeFx &&\n            this.smokeStartTime &&\n            time - this.smokeStartTime >= 80000 / this.gameSpeed.value) {\n            this.disposeSmokeFx();\n        }\n    }\n    private disposeSmokeFx(): void {\n        if (this.smokeFx) {\n            this.smokeFx.finishAndRemove();\n            this.smokeFx = undefined;\n        }\n    }\n    onRemove(): void {\n        this.renderableManager = undefined;\n        this.disposeSmokeFx();\n    }\n    dispose(): void {\n        this.disposeSmokeFx();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/plugin/HarvesterPlugin.ts",
    "content": "import { HarvesterStatus } from \"@/game/gameobject/trait/HarvesterTrait\";\nimport { Coords } from \"@/game/Coords\";\nexport class HarvesterPlugin {\n    private gameObject: any;\n    private harvesterTrait: any;\n    private renderableManager?: any;\n    private harvestAnim?: any;\n    private lastHarvesterStatus?: HarvesterStatus;\n    constructor(gameObject: any, harvesterTrait: any) {\n        this.gameObject = gameObject;\n        this.harvesterTrait = harvesterTrait;\n    }\n    onCreate(renderableManager: any): void {\n        this.renderableManager = renderableManager;\n    }\n    update(time: number): void {\n        if (this.gameObject.warpedOutTrait.isActive()) {\n            this.disposeHarvAnim();\n            this.lastHarvesterStatus = undefined;\n            return;\n        }\n        if (!this.renderableManager)\n            return;\n        const status = this.harvesterTrait.status;\n        if (status !== this.lastHarvesterStatus) {\n            this.lastHarvesterStatus = status;\n            this.disposeHarvAnim();\n            if (status === HarvesterStatus.Harvesting) {\n                this.harvestAnim = this.renderableManager.createTransientAnim(\"OREGATH\", (anim: any) => {\n                    const tile = this.gameObject.tile;\n                    anim.setPosition(Coords.tile3dToWorld(tile.rx + 0.5, tile.ry + 0.5, tile.z));\n                    anim.create3DObject();\n                    const animProps = anim.getAnimProps();\n                    const framesPerDirection = Math.floor(anim.getShpFile().numImages / 8);\n                    let direction = (this.gameObject.direction - 45 + 360) % 360;\n                    direction = (Math.round((direction / 360) * 8) % 8) * framesPerDirection;\n                    animProps.loopStart = animProps.start = direction;\n                    animProps.loopEnd = direction + framesPerDirection - 1;\n                    animProps.loopCount = -1;\n                });\n            }\n        }\n    }\n    private disposeHarvAnim(): void {\n        this.harvestAnim?.remove();\n        this.harvestAnim?.dispose();\n        this.harvestAnim = undefined;\n    }\n    onRemove(): void {\n        this.disposeHarvAnim();\n    }\n    dispose(): void {\n        this.disposeHarvAnim();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/plugin/InfantryDisguisePlugin.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nexport class InfantryDisguisePlugin {\n    private gameObject: any;\n    private disguiseTrait: any;\n    private localPlayer: any;\n    private alliances: any;\n    private renderable: any;\n    private art: any;\n    private gameSpeed: any;\n    private canSeeThroughDisguise: boolean = false;\n    private lastDisguise?: any;\n    private disguisedAt?: number;\n    private lastRenderDisguise?: any;\n    constructor(gameObject: any, disguiseTrait: any, localPlayer: any, alliances: any, renderable: any, art: any, gameSpeed: any) {\n        this.gameObject = gameObject;\n        this.disguiseTrait = disguiseTrait;\n        this.localPlayer = localPlayer;\n        this.alliances = alliances;\n        this.renderable = renderable;\n        this.art = art;\n        this.gameSpeed = gameSpeed;\n    }\n    onCreate(): void { }\n    update(time: number): void {\n        if (!this.gameObject.isDestroyed &&\n            !this.gameObject.warpedOutTrait.isActive()) {\n            let disguise = this.disguiseTrait.getDisguise();\n            let objectArt: any;\n            if (disguise !== this.lastDisguise) {\n                this.lastDisguise = disguise;\n                this.disguisedAt = disguise ? time : undefined;\n            }\n            const player = this.localPlayer.value;\n            if (disguise) {\n                this.canSeeThroughDisguise =\n                    !player ||\n                        this.alliances.haveSharedIntel(player, this.gameObject.owner) ||\n                        !!player.sharedDetectDisguiseTrait?.has(this.gameObject);\n                if (disguise && this.canSeeThroughDisguise) {\n                    disguise = player?.sharedDetectDisguiseTrait?.has(this.gameObject)\n                        ? undefined\n                        : Math.floor((time - this.disguisedAt!) * this.gameSpeed.value) / 1000 % 16 <= 3\n                            ? disguise\n                            : undefined;\n                }\n            }\n            if (this.lastRenderDisguise !== disguise) {\n                this.lastRenderDisguise = disguise;\n                if (disguise) {\n                    objectArt = this.art.getObject(disguise.rules.name, ObjectType.Infantry);\n                    this.renderable.setDisguise({\n                        objectArt,\n                        owner: disguise.owner,\n                    });\n                }\n                else {\n                    this.renderable.setDisguise(undefined);\n                }\n            }\n        }\n    }\n    onRemove(): void { }\n    getUiNameOverride(): string | undefined {\n        const disguise = this.gameObject.disguiseTrait?.getDisguise();\n        if (disguise && !this.canSeeThroughDisguise) {\n            return disguise.rules.uiName;\n        }\n        return undefined;\n    }\n    dispose(): void { }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/plugin/MindControlLinkPlugin.ts",
    "content": "import { MindControlLinkFx } from \"@/engine/renderable/fx/MindControlLinkFx\";\nimport * as THREE from \"three\";\nexport class MindControlLinkPlugin {\n    private source: any;\n    private selectionModel: any;\n    private alliances: any;\n    private viewer: any;\n    private links: Map<any, MindControlLinkFx>;\n    private renderableManager?: any;\n    constructor(source: any, selectionModel: any, alliances: any, viewer: any) {\n        this.source = source;\n        this.selectionModel = selectionModel;\n        this.alliances = alliances;\n        this.viewer = viewer;\n        this.links = new Map();\n    }\n    onCreate(renderableManager: any): void {\n        this.renderableManager = renderableManager;\n    }\n    update(): void {\n        if (!this.source.isDestroyed &&\n            !this.source.isCrashing &&\n            this.source.mindControllerTrait) {\n            if (!this.selectionModel.isSelected() ||\n                (this.viewer.value &&\n                    !this.alliances.haveSharedIntel(this.source.owner, this.viewer.value))) {\n                this.disposeLinks();\n            }\n            else {\n                const targets = this.source.mindControllerTrait.getTargets();\n                for (const [target, link] of this.links.entries()) {\n                    if (!targets.includes(target)) {\n                        link.removeAndDispose();\n                        this.links.delete(target);\n                    }\n                }\n                const color = new THREE.Color(this.source.owner.color.asHex());\n                const sourcePos = this.source.position.worldPosition.clone();\n                for (const target of targets) {\n                    const targetPos = target.position.worldPosition.clone();\n                    let link = this.links.get(target);\n                    if (!link) {\n                        link = new MindControlLinkFx(sourcePos, targetPos, color, 2);\n                        this.links.set(target, link);\n                        this.renderableManager?.addEffect(link);\n                    }\n                    link.updateEndpoints(sourcePos, targetPos);\n                }\n            }\n        }\n    }\n    onRemove(): void {\n        this.renderableManager = undefined;\n        this.disposeLinks();\n    }\n    dispose(): void {\n        this.disposeLinks();\n    }\n    private disposeLinks(): void {\n        this.links.forEach((link) => link.removeAndDispose());\n        this.links.clear();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/plugin/MoveSoundFxPlugin.ts",
    "content": "import { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nexport class MoveSoundFxPlugin {\n    private gameObject: any;\n    private moveSound: any;\n    private worldSound: any;\n    private lastMovingOrRotating: boolean = false;\n    private soundHandle?: any;\n    constructor(gameObject: any, moveSound: any, worldSound: any) {\n        this.gameObject = gameObject;\n        this.moveSound = moveSound;\n        this.worldSound = worldSound;\n    }\n    onCreate(): void { }\n    update(): void {\n        if (this.gameObject.isDestroyed || this.gameObject.isCrashing) {\n            return;\n        }\n        const isMovingOrRotating = !this.gameObject.warpedOutTrait.isActive() &&\n            !!((!this.gameObject.rules.balloonHover &&\n                this.gameObject.rules.hoverAttack &&\n                this.gameObject.zone === ZoneType.Air) ||\n                this.gameObject.spinVelocity ||\n                (!this.gameObject.moveTrait.isIdle() &&\n                    !this.gameObject.moveTrait.isWaiting()));\n        if (isMovingOrRotating !== this.lastMovingOrRotating) {\n            this.lastMovingOrRotating = isMovingOrRotating;\n            if (isMovingOrRotating) {\n                if (!this.soundHandle?.isPlaying()) {\n                    this.soundHandle = this.worldSound.playEffect(this.moveSound, this.gameObject, this.gameObject.owner, 0.35);\n                }\n            }\n            else if (this.soundHandle?.isLoop) {\n                this.soundHandle.stop();\n                this.soundHandle = undefined;\n            }\n        }\n    }\n    onRemove(): void {\n        this.soundHandle?.stop();\n    }\n    dispose(): void {\n        this.soundHandle?.stop();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/plugin/ObjectCloakPlugin.ts",
    "content": "export class ObjectCloakPlugin {\n    private gameObject: any;\n    private localPlayer: any;\n    private alliances: any;\n    private renderable: any;\n    private lastCanSeeThroughCloak: boolean = false;\n    private lastCloaked?: boolean;\n    constructor(gameObject: any, localPlayer: any, alliances: any, renderable: any) {\n        this.gameObject = gameObject;\n        this.localPlayer = localPlayer;\n        this.alliances = alliances;\n        this.renderable = renderable;\n    }\n    onCreate(): void { }\n    update(time: number): void {\n        const isCloaked = !!this.gameObject.cloakableTrait?.isCloaked() && !this.gameObject.isDestroyed;\n        const cloakChanged = isCloaked !== this.lastCloaked;\n        const canSeeThroughCloak = isCloaked && (!this.localPlayer.value ||\n            this.alliances.haveSharedIntel(this.localPlayer.value, this.gameObject.owner));\n        const visibilityChanged = canSeeThroughCloak !== this.lastCanSeeThroughCloak;\n        if (cloakChanged || visibilityChanged) {\n            this.lastCloaked = isCloaked;\n            this.lastCanSeeThroughCloak = canSeeThroughCloak;\n            this.renderable.get3DObject().visible = !isCloaked || canSeeThroughCloak;\n        }\n    }\n    onRemove(): void { }\n    dispose(): void { }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/plugin/ShipWakeTrailPlugin.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { TrailerSmokeFx } from \"@/engine/renderable/fx/TrailerSmokeFx\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { LandType } from \"@/game/type/LandType\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\nimport * as THREE from \"three\";\nexport class ShipWakeTrailPlugin {\n    private gameObject: any;\n    private rules: any;\n    private art: any;\n    private theater: any;\n    private imageFinder: any;\n    private gameSpeed: any;\n    private trailPos: THREE.Vector3;\n    private renderableManager?: any;\n    private trailerFx?: TrailerSmokeFx;\n    private lastMoving?: boolean;\n    private lastSubmerged?: boolean;\n    private lastInWater?: boolean;\n    constructor(gameObject: any, rules: any, art: any, theater: any, imageFinder: any, gameSpeed: any) {\n        this.gameObject = gameObject;\n        this.rules = rules;\n        this.art = art;\n        this.theater = theater;\n        this.imageFinder = imageFinder;\n        this.gameSpeed = gameSpeed;\n        this.trailPos = new THREE.Vector3();\n    }\n    onCreate(renderableManager: any): void {\n        this.renderableManager = renderableManager;\n    }\n    update(time: number): void {\n        if (!this.renderableManager)\n            return;\n        this.trailPos.copy(this.gameObject.position.worldPosition);\n        this.trailPos.y = Coords.tileHeightToWorld(this.gameObject.tile.z);\n        if (this.gameObject.rules.locomotor === LocomotorType.Hover) {\n            const hoverHeight = this.rules.general.hover.height;\n            this.trailPos.x -= hoverHeight;\n            this.trailPos.z -= hoverHeight;\n        }\n        const isMoving = this.gameObject.moveTrait.isMoving();\n        const isSubmerged = this.gameObject.submergibleTrait?.isSubmerged();\n        const isInWater = this.gameObject.zone === ZoneType.Water &&\n            this.gameObject.tile.landType === LandType.Water;\n        const movingChanged = isMoving !== this.lastMoving;\n        const submergedChanged = isSubmerged !== this.lastSubmerged;\n        const waterChanged = isInWater !== this.lastInWater;\n        if (movingChanged || submergedChanged || waterChanged) {\n            this.lastMoving = isMoving;\n            this.lastSubmerged = isSubmerged;\n            this.lastInWater = isInWater;\n            if (isMoving && !isSubmerged && isInWater) {\n                if (this.trailerFx) {\n                    this.trailerFx.enable();\n                }\n                else {\n                    const wakeAnim = this.art.getAnimation(this.rules.audioVisual.wake);\n                    if (wakeAnim) {\n                        const images = this.imageFinder.findByObjectArt(wakeAnim);\n                        const palette = this.theater.getPalette(wakeAnim.paletteType);\n                        const spawnDelay = this.gameObject.art.spawnDelay;\n                        this.trailerFx = new TrailerSmokeFx(this.trailPos, spawnDelay, wakeAnim, images, palette, this.gameSpeed);\n                        this.renderableManager.addEffect(this.trailerFx);\n                    }\n                }\n            }\n            else {\n                this.trailerFx?.disable();\n            }\n        }\n    }\n    onRemove(): void {\n        this.renderableManager = undefined;\n        this.trailerFx?.finishAndRemove();\n    }\n    dispose(): void {\n        this.trailerFx?.finishAndRemove();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/plugin/TntFxPlugin.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { SoundKey } from \"@/engine/sound/SoundKey\";\nexport class TntFxPlugin {\n    private gameObject: any;\n    private tntChargeTrait: any;\n    private frameDurationTicks: number;\n    private renderable: any;\n    private imageFinder: any;\n    private art: any;\n    private alliances: any;\n    private viewer: any;\n    private worldSound: any;\n    private animFactory: any;\n    private lastHasCharge: boolean = false;\n    private animStepCount?: number;\n    private bombAnim?: any;\n    private lastStartFrame?: number;\n    private soundHandle?: any;\n    constructor(gameObject: any, tntChargeTrait: any, frameDurationTicks: number, renderable: any, imageFinder: any, art: any, alliances: any, viewer: any, worldSound: any, animFactory: any) {\n        this.gameObject = gameObject;\n        this.tntChargeTrait = tntChargeTrait;\n        this.frameDurationTicks = frameDurationTicks;\n        this.renderable = renderable;\n        this.imageFinder = imageFinder;\n        this.art = art;\n        this.alliances = alliances;\n        this.viewer = viewer;\n        this.worldSound = worldSound;\n        this.animFactory = animFactory;\n    }\n    onCreate(): void {\n        this.animStepCount = Math.floor(this.imageFinder.findByObjectArt(this.art.getObject(\"BOMBCURS\", ObjectType.Animation)).numImages / 2);\n    }\n    update(time: number): void {\n        if (this.gameObject.isDestroyed || this.gameObject.isCrashing) {\n            if (this.gameObject.rules.leaveRubble) {\n                this.disposeBombAnim();\n                this.soundHandle?.stop();\n            }\n            return;\n        }\n        const hasCharge = this.tntChargeTrait.hasCharge();\n        const chargeChanged = hasCharge !== this.lastHasCharge;\n        let startFrame: number;\n        if (hasCharge) {\n            const progress = 1 - this.tntChargeTrait.getTicksLeft() / this.tntChargeTrait.getInitialTicks();\n            startFrame = Math.floor(2 * progress * (this.animStepCount! - 1));\n        }\n        else {\n            startFrame = 0;\n        }\n        const frameChanged = startFrame !== this.lastStartFrame;\n        this.bombAnim?.update(time);\n        if (chargeChanged || frameChanged) {\n            this.lastHasCharge = hasCharge;\n            this.lastStartFrame = startFrame;\n            if (hasCharge) {\n                if (chargeChanged) {\n                    this.soundHandle?.stop();\n                    this.soundHandle = this.worldSound?.playEffect(SoundKey.BombTickingSound, this.gameObject);\n                }\n                this.disposeBombAnim();\n                const chargeOwner = this.gameObject.tntChargeTrait.getChargeOwner();\n                if (!this.viewer.value || this.alliances.haveSharedIntel(chargeOwner, this.viewer.value)) {\n                    const anim = this.bombAnim = this.animFactory(\"BOMBCURS\");\n                    anim.setRenderOrder(999995);\n                    anim.create3DObject();\n                    const props = anim.getAnimProps();\n                    props.loopCount = -1;\n                    props.start = props.loopStart = startFrame;\n                    props.end = startFrame + 2 - 1;\n                    props.loopEnd = props.end;\n                    props.rate /= this.frameDurationTicks;\n                    this.renderable.get3DObject()?.add(anim.get3DObject());\n                }\n            }\n            else {\n                this.disposeBombAnim();\n                this.soundHandle?.stop();\n            }\n        }\n    }\n    private disposeBombAnim(): void {\n        if (this.bombAnim?.get3DObject()) {\n            this.renderable.get3DObject()?.remove(this.bombAnim.get3DObject());\n        }\n        this.bombAnim?.dispose();\n    }\n    onRemove(): void {\n        this.disposeBombAnim();\n        this.soundHandle?.stop();\n    }\n    dispose(): void {\n        this.disposeBombAnim();\n        this.soundHandle?.stop();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/plugin/TrailerSmokePlugin.ts",
    "content": "import { TrailerSmokeFx } from \"@/engine/renderable/fx/TrailerSmokeFx\";\nimport * as THREE from \"three\";\nexport class TrailerSmokePlugin {\n    private gameObject: any;\n    private art: any;\n    private theater: any;\n    private imageFinder: any;\n    private gameSpeed: any;\n    private initialPosition: THREE.Vector3;\n    private renderableManager?: any;\n    private trailerFx?: TrailerSmokeFx;\n    constructor(gameObject: any, art: any, theater: any, imageFinder: any, gameSpeed: any) {\n        this.gameObject = gameObject;\n        this.art = art;\n        this.theater = theater;\n        this.imageFinder = imageFinder;\n        this.gameSpeed = gameSpeed;\n    }\n    onCreate(renderableManager: any): void {\n        this.initialPosition = this.gameObject.position.worldPosition.clone();\n        this.renderableManager = renderableManager;\n    }\n    update(time: number): void {\n        if (this.renderableManager &&\n            !this.trailerFx &&\n            !this.gameObject.position.worldPosition.equals(this.initialPosition)) {\n            if (this.gameObject.isAircraft()) {\n                let anim;\n                if (this.gameObject.rules.missileSpawn) {\n                    anim = this.art.getAnimation(\"V3TRAIL\");\n                }\n                else if (this.gameObject.isCrashing) {\n                    anim = this.art.getAnimation(\"SGRYSMK1\");\n                }\n                if (anim) {\n                    const images = this.imageFinder.findByObjectArt(anim);\n                    const palette = this.theater.getPalette(anim.paletteType);\n                    const spawnDelay = this.gameObject.art.spawnDelay;\n                    this.trailerFx = new TrailerSmokeFx(this.gameObject.position.worldPosition, spawnDelay, anim, images, palette, this.gameSpeed);\n                    this.renderableManager.addEffect(this.trailerFx);\n                }\n            }\n            if (this.gameObject.isProjectile() || this.gameObject.isDebris()) {\n                const trailerAnim = this.gameObject.isProjectile()\n                    ? this.gameObject.art.trailer\n                    : this.gameObject.rules.trailerAnim;\n                if (trailerAnim) {\n                    const anim = this.art.getAnimation(trailerAnim);\n                    const images = this.imageFinder.findByObjectArt(anim);\n                    const palette = this.theater.getPalette(anim.paletteType);\n                    const spawnDelay = this.gameObject.isProjectile()\n                        ? this.gameObject.art.spawnDelay\n                        : this.gameObject.rules.trailerSeparation;\n                    this.trailerFx = new TrailerSmokeFx(this.gameObject.position.worldPosition, spawnDelay, anim, images, palette, this.gameSpeed);\n                    this.renderableManager.addEffect(this.trailerFx);\n                }\n            }\n        }\n    }\n    onRemove(): void {\n        this.renderableManager = undefined;\n        this.trailerFx?.finishAndRemove();\n    }\n    dispose(): void {\n        this.trailerFx?.finishAndRemove();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/plugin/VehicleDisguisePlugin.ts",
    "content": "import { MapSpriteTranslation } from \"@/engine/renderable/MapSpriteTranslation\";\nimport { ShpRenderable } from \"@/engine/renderable/ShpRenderable\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport * as THREE from \"three\";\n\n// Fade out/in duration in milliseconds\nconst FADE_OUT_MS = 200;\nconst FADE_IN_MS = 200;\n// Allied blink: show tree then show tank, repeating\nconst BLINK_TREE_MS = 3000;\nconst BLINK_TANK_MS = 1500;\n\nexport class VehicleDisguisePlugin {\n    private gameObject: any;\n    private disguiseTrait: any;\n    private localPlayer: any;\n    private alliances: any;\n    private renderable: any;\n    private art: any;\n    private imageFinder: any;\n    private theater: any;\n    private camera: any;\n    private lighting: any;\n    private gameSpeed: any;\n    private useSpriteBatching: boolean;\n\n    private canSeeThroughDisguise: boolean = false;\n    private lastDisguised?: boolean;\n    private disguisedAt?: number;\n    private disguiseObj?: THREE.Object3D;\n    private disguiseRenderable?: ShpRenderable;\n\n    // Sequential fade state:\n    // showingTree = which form is currently rendered\n    // wantTree    = which form we want to show\n    // opacity     = current opacity of the displayed form (0..1)\n    // When showingTree !== wantTree, we fade out; at 0 we swap; then fade in.\n    private showingTree: boolean = false;\n    private wantTree: boolean = false;\n    private opacity: number = 1;\n    private lastTime: number = 0;\n\n    constructor(gameObject: any, disguiseTrait: any, localPlayer: any, alliances: any, renderable: any, art: any, imageFinder: any, theater: any, camera: any, lighting: any, gameSpeed: any, useSpriteBatching: boolean) {\n        this.gameObject = gameObject;\n        this.disguiseTrait = disguiseTrait;\n        this.localPlayer = localPlayer;\n        this.alliances = alliances;\n        this.renderable = renderable;\n        this.art = art;\n        this.imageFinder = imageFinder;\n        this.theater = theater;\n        this.camera = camera;\n        this.lighting = lighting;\n        this.gameSpeed = gameSpeed;\n        this.useSpriteBatching = useSpriteBatching;\n    }\n\n    onCreate(): void { }\n\n    update(time: number): void {\n        if (this.gameObject.isDestroyed ||\n            this.gameObject.warpedOutTrait.isActive()) {\n            return;\n        }\n\n        const dt = this.lastTime > 0 ? time - this.lastTime : 16;\n        this.lastTime = time;\n\n        const isTraitDisguised = this.disguiseTrait.isDisguised();\n\n        // Track trait state transitions\n        if (isTraitDisguised !== this.lastDisguised) {\n            this.lastDisguised = isTraitDisguised;\n            this.disguisedAt = isTraitDisguised ? time : undefined;\n        }\n\n        const localPlayer = this.localPlayer.value;\n\n        // Update detection status\n        if (isTraitDisguised) {\n            this.canSeeThroughDisguise =\n                !localPlayer ||\n                this.alliances.haveSharedIntel(localPlayer, this.gameObject.owner) ||\n                !!localPlayer.sharedDetectDisguiseTrait?.has(this.gameObject);\n        }\n\n        // --- Decide desired form ---\n        if (!isTraitDisguised) {\n            // Moving / firing / cooldown → tank\n            this.wantTree = false;\n        } else if (!this.canSeeThroughDisguise) {\n            // Enemy view → always tree\n            this.wantTree = true;\n        } else if (localPlayer?.sharedDetectDisguiseTrait?.has(this.gameObject)) {\n            // Detected by detector → always tank\n            this.wantTree = false;\n        } else {\n            // Allied / own view → blink\n            const elapsed = time - (this.disguisedAt ?? time);\n            const phase = elapsed % (BLINK_TREE_MS + BLINK_TANK_MS);\n            this.wantTree = phase < BLINK_TREE_MS;\n        }\n\n        // --- Animate sequential fade ---\n        if (this.showingTree !== this.wantTree) {\n            if (this.showingTree) {\n                // Tree → Tank: fade out tree, then instantly show tank\n                this.opacity -= dt / FADE_OUT_MS;\n                if (this.opacity <= 0) {\n                    this.opacity = 1; // tank appears instantly\n                    this.showingTree = false;\n                }\n            } else {\n                // Tank → Tree: fade out tank, then fade in tree\n                this.opacity -= dt / FADE_OUT_MS;\n                if (this.opacity <= 0) {\n                    this.opacity = 0;\n                    this.showingTree = true;\n                }\n            }\n        } else if (this.opacity < 1) {\n            // Fading in (only for tree appearing)\n            this.opacity += dt / FADE_IN_MS;\n            if (this.opacity > 1) this.opacity = 1;\n        }\n\n        // --- Apply visuals ---\n        // Ensure disguise 3D object exists when needed\n        if (this.showingTree && isTraitDisguised) {\n            this.ensureDisguiseObj();\n        }\n\n        if (this.showingTree) {\n            // Tree form active\n            if (this.renderable.mainObj) {\n                this.renderable.mainObj.visible = false;\n            }\n            this.renderable.posObj.visible = this.canSeeThroughDisguise;\n            if (this.disguiseObj) {\n                this.disguiseObj.visible = true;\n                this.disguiseRenderable?.setOpacity(this.opacity);\n            }\n            // Restore vehicle opacity in case it was faded\n            this.setMainVehicleOpacity(1);\n        } else {\n            // Tank form active\n            if (this.renderable.mainObj) {\n                this.renderable.mainObj.visible = true;\n            }\n            this.renderable.posObj.visible = true;\n            if (this.disguiseObj) {\n                this.disguiseObj.visible = false;\n            }\n            this.setMainVehicleOpacity(this.opacity);\n        }\n\n        // Update disguise lighting\n        if (this.disguiseObj?.visible && this.disguiseRenderable && isTraitDisguised) {\n            const disguise = this.disguiseTrait.getDisguise();\n            if (disguise?.rules.type === ObjectType.Terrain) {\n                const terrainArt = this.art.getObject(disguise.rules.name, ObjectType.Terrain);\n                const extraLight = this.lighting\n                    .compute(terrainArt.lightingType, this.gameObject.tile, this.gameObject.tileElevation)\n                    .addScalar(-1);\n                this.disguiseRenderable.setExtraLight(extraLight);\n            }\n        }\n    }\n\n    private ensureDisguiseObj(): void {\n        if (this.disguiseObj) return;\n        const disguise = this.disguiseTrait.getDisguise();\n        if (!disguise || disguise.rules.type !== ObjectType.Terrain) return;\n        const terrainArt = this.art.getObject(disguise.rules.name, ObjectType.Terrain);\n        this.disguiseObj = this.createDisguiseObj(terrainArt);\n        this.renderable.get3DObject().add(this.disguiseObj);\n    }\n\n    private setMainVehicleOpacity(opacity: number): void {\n        if (this.renderable.vxlBuilders) {\n            for (const builder of this.renderable.vxlBuilders) {\n                builder.setOpacity(opacity);\n            }\n        }\n        if (this.renderable.shpRenderable) {\n            this.renderable.shpRenderable.setOpacity(opacity);\n        }\n        if (this.renderable.placeholder) {\n            this.renderable.placeholder.setOpacity(opacity);\n        }\n    }\n\n    private createDisguiseObj(disguise: any): THREE.Object3D {\n        const obj = new THREE.Object3D();\n        obj.matrixAutoUpdate = false;\n        const width = 1;\n        const height = 1;\n        const translation = new MapSpriteTranslation(width, height);\n        const { spriteOffset, anchorPointWorld } = translation.compute();\n        obj.position.x = anchorPointWorld.x;\n        obj.position.z = anchorPointWorld.y;\n        obj.updateMatrix();\n        const images = this.imageFinder.findByObjectArt(disguise);\n        const palette = this.theater.getPalette(disguise.paletteType, disguise.customPaletteName);\n        const renderable = ShpRenderable.factory(images, palette, this.camera, spriteOffset, disguise.hasShadow);\n        renderable.setBatched(this.useSpriteBatching);\n        if (this.useSpriteBatching) {\n            renderable.setBatchPalettes([palette]);\n        }\n        renderable.setFrame(0);\n        renderable.create3DObject();\n        obj.add(renderable.get3DObject());\n        this.disguiseRenderable = renderable;\n        return obj;\n    }\n\n    updateLighting(): void {\n        if (this.disguiseObj?.visible && this.disguiseRenderable) {\n            const disguise = this.disguiseTrait.getDisguise();\n            if (disguise) {\n                if (disguise.rules.type !== ObjectType.Terrain) {\n                    throw new Error(\"Unsupported disguise type \" + ObjectType[disguise.rules.type]);\n                }\n                const terrainObj = this.art.getObject(disguise.rules.name, ObjectType.Terrain);\n                this.disguiseRenderable.setExtraLight(this.lighting\n                    .compute(terrainObj.lightingType, this.gameObject.tile, this.gameObject.tileElevation)\n                    .addScalar(-1));\n            }\n        }\n    }\n\n    onRemove(): void {\n        this.setMainVehicleOpacity(1);\n        if (this.disguiseObj) {\n            this.renderable.get3DObject().remove(this.disguiseObj);\n            this.disguiseObj = undefined;\n        }\n    }\n\n    getUiNameOverride(): string | undefined {\n        if (this.gameObject.disguiseTrait?.hasTerrainDisguise() &&\n            !this.canSeeThroughDisguise) {\n            return \"\";\n        }\n    }\n\n    shouldDisableHighlight(): boolean {\n        return (!!this.gameObject.disguiseTrait?.hasTerrainDisguise() &&\n            !this.canSeeThroughDisguise);\n    }\n\n    dispose(): void {\n        this.disguiseRenderable?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/unit/BlobShadow.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { BatchedMesh } from \"@/engine/gfx/batch/BatchedMesh\";\nimport * as THREE from \"three\";\nexport class BlobShadow {\n    private static geometries = new Map<number, THREE.BufferGeometry>();\n    private static mat = new THREE.MeshBasicMaterial({\n        color: 0,\n        transparent: true,\n        opacity: 0.5,\n        alphaTest: 0,\n    });\n    private obj?: THREE.Mesh | BatchedMesh;\n    private lastTileZ?: number;\n    private lastTileElevation?: number;\n    private lastBridgeBelow?: boolean;\n    constructor(private gameObject: any, private radius: number, private useMeshInstancing: boolean) { }\n    get3DObject(): THREE.Mesh | BatchedMesh | undefined {\n        return this.obj;\n    }\n    create3DObject(): void {\n        if (!this.obj) {\n            let geometry = BlobShadow.geometries.get(this.radius);\n            if (!geometry) {\n                geometry = new THREE.CircleGeometry(this.radius * Coords.ISO_WORLD_SCALE);\n                BlobShadow.geometries.set(this.radius, geometry);\n            }\n            this.obj = new (this.useMeshInstancing ? BatchedMesh : THREE.Mesh)(geometry, BlobShadow.mat);\n            this.obj.rotation.x = -Math.PI / 2;\n            this.obj.matrixAutoUpdate = false;\n        }\n    }\n    update(_: any, __: any): void {\n        const obj = this.obj;\n        if (!obj)\n            return;\n        let isVisible = this.gameObject.zone === ZoneType.Air ||\n            (this.gameObject.isInfantry() &&\n                this.gameObject.stance === StanceType.Paradrop);\n        obj.visible = isVisible;\n        if (isVisible) {\n            const tileZ = this.gameObject.tile.z;\n            const tileElevation = this.gameObject.tileElevation;\n            const isOnBridge = !!this.gameObject.tile.onBridgeLandType;\n            if (tileZ !== this.lastTileZ ||\n                tileElevation !== this.lastTileElevation ||\n                isOnBridge !== this.lastBridgeBelow) {\n                this.lastTileZ = tileZ;\n                this.lastTileElevation = tileElevation;\n                this.lastBridgeBelow = isOnBridge;\n                const bridgeBelow = this.gameObject.position.getBridgeBelow();\n                obj.position.y =\n                    Coords.tileHeightToWorld(-tileElevation) +\n                        (bridgeBelow\n                            ? Coords.tileHeightToWorld(bridgeBelow.tileElevation) +\n                                0.01 * Coords.ISO_WORLD_SCALE\n                            : 0);\n                obj.updateMatrix();\n            }\n        }\n    }\n    dispose(): void { }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/unit/DebugLabel.ts",
    "content": "import { SpriteUtils } from \"@/engine/gfx/SpriteUtils\";\nimport { CanvasUtils } from \"@/engine/gfx/CanvasUtils\";\nimport { Coords } from \"@/game/Coords\";\nimport * as THREE from \"three\";\nexport class DebugLabel {\n    private mesh?: THREE.Mesh;\n    private texture?: THREE.Texture;\n    constructor(private text: string, private color: string, private camera: THREE.Camera) { }\n    get3DObject(): THREE.Mesh | undefined {\n        return this.mesh;\n    }\n    create3DObject(): void {\n        if (!this.mesh) {\n            const color = new THREE.Color(this.color);\n            const outlineColor = 0.5 < 0.299 * color.r + 0.587 * color.g + 0.114 * color.b\n                ? \"black\"\n                : \"white\";\n            this.texture = this.createTexture(this.text, \"#\" + color.getHexString(), outlineColor);\n            this.mesh = this.createMesh(this.texture);\n        }\n    }\n    private createMesh(texture: THREE.Texture): THREE.Mesh {\n        const geometry = SpriteUtils.createSpriteGeometry({\n            texture,\n            camera: this.camera,\n            align: { x: 0, y: -1 },\n            offset: { x: 0, y: Coords.ISO_TILE_SIZE / 4 },\n            scale: Coords.ISO_WORLD_SCALE,\n        });\n        const material = new THREE.MeshBasicMaterial({\n            map: texture,\n            side: THREE.DoubleSide,\n            transparent: true,\n            depthTest: false,\n        });\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.matrixAutoUpdate = false;\n        return mesh;\n    }\n    private createTexture(text: string, color: string, outlineColor: string): THREE.Texture {\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = canvas.height = 0;\n        const ctx = canvas.getContext(\"2d\");\n        if (!ctx)\n            throw new Error(\"Failed to get canvas context\");\n        let y = 0;\n        for (const line of text.split(\"\\n\")) {\n            const metrics = CanvasUtils.drawText(ctx, line, 0, y, {\n                color,\n                outlineColor,\n                outlineWidth: 2,\n                fontFamily: \"'Fira Sans Condensed', Arial, sans-serif\",\n                fontSize: 10,\n                fontWeight: \"400\",\n                paddingTop: 3,\n                paddingBottom: 3,\n                paddingLeft: 3,\n                paddingRight: 3,\n                autoEnlargeCanvas: true,\n            });\n            y += metrics.height;\n        }\n        const width = canvas.width;\n        const height = canvas.height;\n        const imageData = ctx.getImageData(0, 0, width, height);\n        canvas.width += 1;\n        canvas.height += 1;\n        ctx.putImageData(imageData, 1, 1);\n        const texture = new THREE.Texture(canvas);\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        texture.needsUpdate = true;\n        texture.flipY = false;\n        return texture;\n    }\n    update(): void { }\n    dispose(): void {\n        this.texture?.dispose();\n        if (Array.isArray(this.mesh?.material)) {\n            this.mesh.material.forEach((material) => material.dispose());\n        }\n        else {\n            this.mesh?.material?.dispose();\n        }\n        this.mesh?.geometry.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/unit/ExtraLightHelper.ts",
    "content": "import * as THREE from \"three\";\nexport class ExtraLightHelper {\n    static multiplyShp(target: THREE.Color, source: THREE.Color, intensity: number): void {\n        target.copy(source).add(source.clone().addScalar(1).multiplyScalar(intensity));\n    }\n    static multiplyVxl(target: THREE.Color, source: THREE.Color, intensity: number, radius: number): void {\n        target.copy(source).multiplyScalar(Math.max(0, 1 + radius));\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/unit/FlyerHelperMode.ts",
    "content": "export enum FlyerHelperMode {\n    Always = 0,\n    Selected = 1,\n    Never = 2\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/unit/ModelQuality.ts",
    "content": "export enum ModelQuality {\n    Low = 0,\n    High = 1\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/unit/RotorHelper.ts",
    "content": "import { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { clamp } from \"@/util/math\";\nimport * as THREE from \"three\";\nexport class RotorHelper {\n    static computeRotationStep(entity: {\n        zone: ZoneType;\n        rules: {\n            idleRate?: number;\n        };\n    }, currentRotation: number, rotor: {\n        speed?: number;\n        idleSpeed?: number;\n    }): number {\n        const isAirborne = entity.zone === ZoneType.Air;\n        const idleRate = entity.rules.idleRate;\n        const isIdle = isAirborne || !!rotor.idleSpeed || !!idleRate;\n        let speed = rotor.speed ?? 67;\n        if (!isAirborne) {\n            if (rotor.idleSpeed) {\n                speed = rotor.idleSpeed;\n            }\n            else if (idleRate) {\n                speed /= idleRate;\n            }\n        }\n        const direction = Math.sign(speed);\n        const maxRotation = Math.abs(THREE.MathUtils.degToRad(speed));\n        const currentRotationAbs = Math.abs(currentRotation);\n        return direction * clamp(currentRotationAbs + 0.1 * (isIdle ? 1 : (currentRotationAbs / maxRotation) * -0.5), 0, maxRotation);\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/entity/unit/ShadowQuality.ts",
    "content": "export enum ShadowQuality {\n    Off = 0,\n    Low = 1,\n    Medium = 2,\n    High = 3\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/DamageSmokeFx.ts",
    "content": "import { AnimProps } from '@/engine/AnimProps';\nimport { ImageUtils } from '@/engine/gfx/ImageUtils';\nimport * as THREE from 'three';\nimport SPE from './speRuntime';\nimport { patchSpeGroup } from './speCompat';\nconst PARTICLE_COUNT = 1000;\nexport class DamageSmokeFx {\n    private static textureCache = new Map<any, THREE.Texture>();\n    private gameObject: any;\n    private smokeArt: any;\n    private shpFile: any;\n    private palette: any;\n    private gameSpeed: any;\n    private lifetimeSeconds: number;\n    private finishRequested: boolean;\n    private container?: any;\n    private particleGroup?: SPE.Group;\n    private particleEmitter?: SPE.Emitter;\n    private particleMaxAge?: number;\n    private lastUpdateMillis?: number;\n    private firstUpdateMillis?: number;\n    private timeLeft?: number;\n    static clearTextureCache() {\n        this.textureCache.forEach(texture => texture.dispose());\n        this.textureCache.clear();\n    }\n    constructor(gameObject: any, smokeArt: any, shpFile: any, palette: any, gameSpeed: any) {\n        this.gameObject = gameObject;\n        this.smokeArt = smokeArt;\n        this.shpFile = shpFile;\n        this.palette = palette;\n        this.gameSpeed = gameSpeed;\n        this.lifetimeSeconds = Number.POSITIVE_INFINITY;\n        this.finishRequested = false;\n    }\n    setContainer(container: any) {\n        this.container = container;\n    }\n    create3DObject() {\n        if (!this.particleGroup) {\n            let texture = DamageSmokeFx.textureCache.get(this.shpFile);\n            if (!texture) {\n                const canvas = ImageUtils.convertShpToCanvas(this.shpFile, this.palette, true);\n                texture = new THREE.Texture(canvas);\n                texture.minFilter = THREE.NearestFilter;\n                texture.magFilter = THREE.NearestFilter;\n                texture.needsUpdate = true;\n                texture.flipY = false;\n                DamageSmokeFx.textureCache.set(this.shpFile, texture);\n            }\n            this.particleGroup = new SPE.Group({\n                texture: {\n                    value: texture,\n                    frames: new THREE.Vector2(this.shpFile.numImages, 1),\n                    frameCount: this.shpFile.numImages,\n                    loop: 1\n                },\n                maxParticleCount: PARTICLE_COUNT,\n                hasPerspective: false,\n                transparent: true,\n                alphaTest: 0,\n                blending: THREE.NormalBlending\n            });\n            patchSpeGroup(this.particleGroup);\n            this.particleGroup.mesh.name = \"fx_damage_smoke\";\n            this.particleGroup.mesh.frustumCulled = false;\n            const animProps = new AnimProps(this.smokeArt.art, this.shpFile);\n            const rate = (this.smokeArt.art.getBool(\"Normalized\") ? 2 : 1) * animProps.rate;\n            const activeMultiplier = rate / 10;\n            this.particleMaxAge = (2 * this.shpFile.numImages) / animProps.rate;\n            const velocity = 9 * rate;\n            const acceleration = 0.05 * rate;\n            this.particleEmitter = new SPE.Emitter({\n                particleCount: PARTICLE_COUNT,\n                maxAge: { value: this.particleMaxAge },\n                activeMultiplier: activeMultiplier / (PARTICLE_COUNT / this.particleMaxAge),\n                position: { value: this.computeEmitterPosition() },\n                acceleration: {\n                    value: new THREE.Vector3(0, -acceleration, 0),\n                    spread: new THREE.Vector3(2, 0, 2)\n                },\n                velocity: {\n                    value: new THREE.Vector3(0, velocity, 0),\n                    spread: new THREE.Vector3(0.1 * velocity, 0, 0.1 * velocity)\n                },\n                opacity: { value: 0.5 },\n                size: {\n                    value: Math.max(this.shpFile.height, this.shpFile.width)\n                }\n            });\n            this.particleGroup.addEmitter(this.particleEmitter);\n        }\n    }\n    computeEmitterPosition() {\n        return this.gameObject.position.worldPosition\n            .clone()\n            .add(this.gameObject.rules.damageSmokeOffset);\n    }\n    get3DObject() {\n        return this.particleGroup?.mesh;\n    }\n    update(timeMillis: number) {\n        if (this.particleEmitter) {\n            this.particleEmitter.position.value = this.computeEmitterPosition();\n        }\n        if (this.lastUpdateMillis) {\n            const deltaTime = timeMillis - this.lastUpdateMillis;\n            this.particleGroup?.tick((deltaTime / 1000) * this.gameSpeed.value);\n        }\n        else {\n            this.firstUpdateMillis = timeMillis;\n            this.particleGroup?.tick(0);\n        }\n        this.lastUpdateMillis = timeMillis;\n        if (this.finishRequested) {\n            this.finishRequested = false;\n            if (this.particleEmitter?.alive) {\n                const elapsedTime = ((timeMillis - (this.firstUpdateMillis || 0)) / 1000) * this.gameSpeed.value;\n                this.lifetimeSeconds = elapsedTime + (this.particleMaxAge || 0);\n                this.particleEmitter.disable();\n            }\n        }\n        this.timeLeft = Math.max(0, 1 - (timeMillis - (this.firstUpdateMillis || 0)) / ((1000 * this.lifetimeSeconds) / this.gameSpeed.value));\n        if (!this.timeLeft) {\n            this.container?.remove(this);\n            this.dispose();\n        }\n    }\n    finishAndRemove() {\n        this.finishRequested = true;\n    }\n    dispose() {\n        this.particleGroup?.mesh.geometry.dispose();\n        this.particleGroup?.mesh.material.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/DetectionLineFx.ts",
    "content": "import * as THREE from 'three';\nimport { MeshLine, MeshLineMaterial } from 'three.meshline';\nimport { Coords } from '@/game/Coords';\nimport { getMeshLineResolution } from '@/engine/renderable/fx/MeshLineResolution';\ninterface Camera {\n    top: number;\n    right: number;\n    rotation: THREE.Euler;\n}\ninterface Container {\n    remove(item: DetectionLineFx): void;\n}\nconst whiteColor = new THREE.Color(0xffffff);\nexport class DetectionLineFx {\n    private camera: Camera;\n    public sourcePos: THREE.Vector3;\n    public targetPos: THREE.Vector3;\n    public color: THREE.Color;\n    private renderOrder: number;\n    public needsUpdate: boolean;\n    private cameraHash: string;\n    private computedColor: THREE.Color;\n    private lineHeadMaterial: THREE.MeshBasicMaterial;\n    private container?: Container;\n    private wrapper?: THREE.Object3D;\n    private lineMesh?: THREE.Mesh;\n    private srcLineHead?: THREE.Mesh;\n    private destLineHead?: THREE.Mesh;\n    private lastUpdateMillis?: number;\n    static lineHeadGeometry = new THREE.PlaneGeometry(3 * Coords.ISO_WORLD_SCALE, 3 * Coords.ISO_WORLD_SCALE);\n    constructor(camera: Camera, sourcePos: THREE.Vector3, targetPos: THREE.Vector3, color: THREE.Color, renderOrder: number) {\n        this.camera = camera;\n        this.sourcePos = sourcePos;\n        this.targetPos = targetPos;\n        this.color = color;\n        this.renderOrder = renderOrder;\n        this.needsUpdate = false;\n        this.cameraHash = this.camera.top + \"_\" + this.camera.right;\n        this.computedColor = color.clone();\n        this.lineHeadMaterial = new THREE.MeshBasicMaterial({\n            color: 0xffffff,\n            transparent: true,\n            depthTest: false,\n            depthWrite: false,\n        });\n    }\n    setContainer(container: Container): void {\n        this.container = container;\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.wrapper;\n    }\n    create3DObject(): void {\n        if (!this.wrapper) {\n            this.wrapper = new THREE.Object3D();\n            this.wrapper.name = \"fx_detectionline\";\n            this.lineMesh = this.createLineMesh();\n            this.srcLineHead = this.createLineHead();\n            this.destLineHead = this.createLineHead();\n            this.wrapper.add(this.srcLineHead);\n            this.wrapper.add(this.destLineHead);\n            this.wrapper.add(this.lineMesh);\n            this.needsUpdate = true;\n        }\n    }\n    update(timeMillis: number): void {\n        if (!this.lastUpdateMillis) {\n            this.lastUpdateMillis = timeMillis;\n        }\n        const deltaTime = (timeMillis - this.lastUpdateMillis) / (1000 / 120);\n        this.lastUpdateMillis = timeMillis;\n        const currentCameraHash = this.camera.top + \"_\" + this.camera.right;\n        if (currentCameraHash !== this.cameraHash) {\n            this.cameraHash = currentCameraHash;\n            (this.lineMesh!.material as MeshLineMaterial).uniforms.resolution.value.copy(this.computeResolution(this.camera));\n        }\n        const material = this.lineMesh!.material as MeshLineMaterial;\n        if (this.needsUpdate) {\n            this.needsUpdate = false;\n            this.lineMesh!.geometry.dispose();\n            this.lineMesh!.geometry = this.createLineGeometry(this.sourcePos, this.targetPos);\n            const distance = this.sourcePos.distanceTo(this.targetPos);\n            material.uniforms.dashArray.value = this.computeDashArray(distance);\n            this.srcLineHead!.position.copy(this.sourcePos);\n            this.destLineHead!.position.copy(this.targetPos);\n        }\n        material.uniforms.dashOffset.value -= (material.uniforms.dashArray.value / 50) * deltaTime;\n        const pulseValue = Math.sin(((timeMillis % 1000) / 1000) * Math.PI);\n        const currentColor = this.computedColor.copy(this.color).lerp(whiteColor, pulseValue);\n        material.uniforms.color.value = currentColor.clone();\n        this.lineHeadMaterial.color.set(currentColor);\n    }\n    private createLineMesh(): THREE.Mesh {\n        const sourcePos = this.sourcePos.clone();\n        const targetPos = this.targetPos.clone();\n        const mesh = new THREE.Mesh(this.createLineGeometry(sourcePos, targetPos), this.createLineMaterial(this.color.clone(), sourcePos.distanceTo(targetPos)));\n        mesh.renderOrder = this.renderOrder;\n        return mesh;\n    }\n    private createLineGeometry(sourcePos: THREE.Vector3, targetPos: THREE.Vector3): THREE.BufferGeometry {\n        const points = [\n            sourcePos.x, sourcePos.y, sourcePos.z,\n            targetPos.x, targetPos.y, targetPos.z,\n        ];\n        const meshLine = new MeshLine();\n        meshLine.setPoints(points);\n        return meshLine.geometry;\n    }\n    private createLineMaterial(color: THREE.Color, distance: number): MeshLineMaterial {\n        return new MeshLineMaterial({\n            color: color,\n            lineWidth: 1,\n            resolution: this.computeResolution(this.camera),\n            transparent: true,\n            sizeAttenuation: 0,\n            dashArray: this.computeDashArray(distance),\n            depthTest: false,\n        });\n    }\n    private createLineHead(): THREE.Mesh {\n        const mesh = new THREE.Mesh(DetectionLineFx.lineHeadGeometry, this.lineHeadMaterial);\n        const quaternion = new THREE.Quaternion().setFromEuler(this.camera.rotation);\n        mesh.setRotationFromQuaternion(quaternion);\n        mesh.renderOrder = this.renderOrder;\n        return mesh;\n    }\n    private computeDashArray(distance: number): number {\n        return Math.min(1, 5 / distance) * Coords.ISO_WORLD_SCALE;\n    }\n    private computeResolution(camera: Camera): THREE.Vector2 {\n        return getMeshLineResolution(camera as unknown as THREE.Camera);\n    }\n    remove(): void {\n        this.container?.remove(this);\n    }\n    dispose(): void {\n        if (this.wrapper) {\n            this.lineMesh!.geometry.dispose();\n            (this.lineMesh!.material as MeshLineMaterial).dispose();\n            this.lineHeadMaterial.dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/Effect.ts",
    "content": "export class Effect {\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/LaserFx.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport * as THREE from 'three';\nimport { MeshLine, MeshLineMaterial } from 'three.meshline';\nimport { getMeshLineResolution } from '@/engine/renderable/fx/MeshLineResolution';\ninterface Container {\n    remove(item: LaserFx): void;\n}\nexport class LaserFx {\n    private camera: THREE.Camera;\n    private sourcePos: THREE.Vector3;\n    private targetPos: THREE.Vector3;\n    private color: THREE.Color;\n    private durationSeconds: number;\n    private width: number;\n    private container?: Container;\n    private lineMesh?: THREE.Mesh;\n    private firstUpdateMillis?: number;\n    private timeLeft: number = 1;\n    constructor(camera: THREE.Camera, sourcePos: THREE.Vector3, targetPos: THREE.Vector3, color: THREE.Color, durationSeconds: number, width: number) {\n        this.camera = camera;\n        this.sourcePos = sourcePos;\n        this.targetPos = targetPos;\n        this.color = color;\n        this.durationSeconds = durationSeconds;\n        this.width = width;\n    }\n    setContainer(container: Container): void {\n        this.container = container;\n    }\n    get3DObject(): THREE.Mesh | undefined {\n        return this.lineMesh;\n    }\n    create3DObject(): void {\n        if (!this.lineMesh) {\n            this.lineMesh = this.createObject();\n            this.lineMesh.name = \"fx_laser\";\n        }\n    }\n    update(timeMillis: number): void {\n        if (!this.firstUpdateMillis) {\n            this.firstUpdateMillis = timeMillis;\n        }\n        this.timeLeft = Math.max(0, 1 - (timeMillis - this.firstUpdateMillis) / (1000 * this.durationSeconds));\n        const material = this.lineMesh!.material as MeshLineMaterial;\n        material.uniforms.opacity.value = +this.timeLeft;\n        if (this.isFinished()) {\n            this.container!.remove(this);\n            this.dispose();\n        }\n    }\n    private createObject(): THREE.Mesh {\n        const sourcePos = this.sourcePos.clone();\n        const targetPos = this.targetPos.clone();\n        const points = [\n            sourcePos.x, sourcePos.y, sourcePos.z,\n            targetPos.x, targetPos.y, targetPos.z,\n        ];\n        const meshLine = new MeshLine();\n        meshLine.setPoints(points);\n        const material = new MeshLineMaterial({\n            color: this.color.clone(),\n            lineWidth: this.width,\n            resolution: getMeshLineResolution(this.camera),\n            transparent: true,\n            sizeAttenuation: 0,\n            blending: THREE.AdditiveBlending\n        });\n        return new THREE.Mesh(meshLine.geometry, material);\n    }\n    private isFinished(): boolean {\n        return this.timeLeft === 0;\n    }\n    dispose(): void {\n        if (this.lineMesh) {\n            this.lineMesh.geometry.dispose();\n            const material = this.lineMesh.material;\n            if (Array.isArray(material)) {\n                material.forEach((entry) => entry.dispose());\n            }\n            else {\n                material.dispose();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/LineTrailFx.ts",
    "content": "import { ObjectArt } from '@/game/art/ObjectArt';\nimport { Coords } from '@/game/Coords';\nimport * as THREE from 'three';\nimport { MeshLine, MeshLineMaterial } from 'three.meshline';\nimport { getMeshLineResolution } from '@/engine/renderable/fx/MeshLineResolution';\ninterface GameSpeed {\n    value?: number;\n}\ninterface Container {\n    remove(item: LineTrailFx): void;\n}\nexport class LineTrailFx {\n    private lazyTarget: () => THREE.Object3D | undefined;\n    private trailColor: THREE.Color;\n    private trailDecrement: number;\n    private gameSpeed: GameSpeed;\n    private camera: THREE.Camera;\n    private trailInitialized: boolean = false;\n    private container?: Container;\n    private wrapper?: THREE.Object3D;\n    private trailMesh?: THREE.Mesh;\n    private trailMaterial?: MeshLineMaterial;\n    private timeLeft?: number;\n    private finishDurationSeconds?: number;\n    private prevUpdateMillis?: number;\n    private lastTargetPosition?: THREE.Vector3;\n    private frozenTargetPosition?: THREE.Vector3;\n    private trailPoints: THREE.Vector3[] = [];\n    private maxPoints: number = 2;\n    private cameraHash?: string;\n    constructor(lazyTarget: () => THREE.Object3D | undefined, trailColor: THREE.Color, trailDecrement: number, gameSpeed: GameSpeed, camera: THREE.Camera) {\n        this.lazyTarget = lazyTarget;\n        this.trailColor = trailColor;\n        this.trailDecrement = trailDecrement;\n        this.gameSpeed = gameSpeed;\n        this.camera = camera;\n    }\n    setContainer(container: Container): void {\n        this.container = container;\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.wrapper;\n    }\n    create3DObject(): void {\n        if (!this.wrapper) {\n            this.wrapper = new THREE.Object3D();\n            this.wrapper.name = \"fx_linetrail\";\n        }\n    }\n    update(timeMillis: number): void {\n        if (this.timeLeft !== undefined) {\n            const prevTime = this.prevUpdateMillis;\n            this.prevUpdateMillis = timeMillis;\n            if (prevTime) {\n                this.timeLeft = Math.max(0, this.timeLeft - (timeMillis - prevTime) / 1000);\n            }\n        }\n        if (!this.trailInitialized) {\n            this.trailInitialized = true;\n            const trailMesh = this.createTrail(this.trailColor, this.trailDecrement);\n            if (trailMesh) {\n                this.trailMesh = trailMesh;\n                this.wrapper?.add(trailMesh);\n            }\n            else {\n                this.timeLeft = 0;\n            }\n        }\n        if (this.trailMesh && this.trailMaterial) {\n            const currentCameraHash = this.computeCameraHash();\n            if (currentCameraHash !== this.cameraHash) {\n                this.cameraHash = currentCameraHash;\n                this.trailMaterial.resolution = this.computeResolution();\n            }\n            const currentTargetPosition = this.resolveTargetPosition();\n            if (currentTargetPosition) {\n                this.lastTargetPosition = currentTargetPosition.clone();\n                this.updateTrailGeometry(currentTargetPosition);\n            }\n            const opacity = this.timeLeft === undefined || this.finishDurationSeconds === undefined\n                ? 1\n                : Math.max(0, this.timeLeft / this.finishDurationSeconds);\n            this.trailMaterial.opacity = opacity;\n        }\n        if (this.isFinished()) {\n            this.container?.remove(this);\n            this.dispose();\n        }\n    }\n    private createTrail(color: THREE.Color, decrement: number): THREE.Mesh | undefined {\n        const targetPosition = this.resolveTargetPosition();\n        if (!targetPosition)\n            return undefined;\n        this.maxPoints = Math.max(2, Math.floor(((3 / this.getGameSpeedValue()) * 50) /\n            (decrement / ObjectArt.DEFAULT_LINE_TRAIL_DEC)));\n        this.trailPoints = [targetPosition.clone(), targetPosition.clone()];\n        this.lastTargetPosition = targetPosition.clone();\n        this.cameraHash = this.computeCameraHash();\n        const meshLine = new MeshLine();\n        meshLine.setPoints(this.flattenPoints(this.trailPoints));\n        const material = new MeshLineMaterial({\n            color: color.clone(),\n            lineWidth: 0.8,\n            resolution: this.computeResolution(),\n            transparent: true,\n            sizeAttenuation: 0,\n            depthTest: false,\n            depthWrite: false,\n            blending: THREE.AdditiveBlending,\n        });\n        material.opacity = 1;\n        this.trailMaterial = material;\n        const mesh = new THREE.Mesh(meshLine.geometry, material);\n        mesh.frustumCulled = false;\n        mesh.renderOrder = 1000000;\n        return mesh;\n    }\n    isFinished(): boolean {\n        return this.timeLeft === 0;\n    }\n    requestFinishAndDispose(): void {\n        this.finishDurationSeconds = 0.8 / this.getGameSpeedValue();\n        this.timeLeft = this.finishDurationSeconds;\n    }\n    stopTracking(): void {\n        if (!this.frozenTargetPosition) {\n            this.frozenTargetPosition =\n                this.lastTargetPosition?.clone() ?? this.resolveTargetPosition()?.clone();\n        }\n    }\n    dispose(): void {\n        if (this.trailMesh) {\n            this.trailMesh.geometry.dispose();\n            this.trailMaterial?.dispose();\n            this.wrapper?.remove(this.trailMesh);\n            this.trailMesh = undefined;\n        }\n    }\n    private resolveTargetPosition(): THREE.Vector3 | undefined {\n        if (this.frozenTargetPosition) {\n            return this.frozenTargetPosition.clone();\n        }\n        const target = this.lazyTarget();\n        if (!target) {\n            return undefined;\n        }\n        const position = new THREE.Vector3();\n        target.getWorldPosition(position);\n        return position;\n    }\n    private updateTrailGeometry(currentTargetPosition: THREE.Vector3): void {\n        if (!this.trailMesh) {\n            return;\n        }\n        const lastPoint = this.trailPoints[this.trailPoints.length - 1];\n        if (!lastPoint) {\n            this.trailPoints.push(currentTargetPosition.clone());\n        }\n        else if (lastPoint.distanceToSquared(currentTargetPosition) > 1) {\n            this.trailPoints.push(currentTargetPosition.clone());\n        }\n        else {\n            lastPoint.copy(currentTargetPosition);\n        }\n        while (this.trailPoints.length > this.maxPoints) {\n            this.trailPoints.shift();\n        }\n        if (this.trailPoints.length === 1) {\n            this.trailPoints.push(this.trailPoints[0].clone());\n        }\n        const meshLine = new MeshLine();\n        meshLine.setPoints(this.flattenPoints(this.trailPoints));\n        this.trailMesh.geometry.dispose();\n        this.trailMesh.geometry = meshLine.geometry;\n    }\n    private flattenPoints(points: THREE.Vector3[]): number[] {\n        return points.flatMap((point) => [point.x, point.y, point.z]);\n    }\n    private computeCameraHash(): string {\n        const camera = this.camera as THREE.OrthographicCamera;\n        return `${camera.top}_${camera.right}_${camera.rotation.x}_${camera.rotation.y}`;\n    }\n    private computeResolution(): THREE.Vector2 {\n        return getMeshLineResolution(this.camera);\n    }\n    private getGameSpeedValue(): number {\n        if (typeof this.gameSpeed?.value !== 'number') {\n            throw new Error(`[LineTrailFx] invalid gameSpeed dependency. Expected BoxedVar<number>, got \"${this.gameSpeed?.constructor?.name ?? typeof this.gameSpeed}\"`);\n        }\n        return this.gameSpeed.value;\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/MeshLineResolution.ts",
    "content": "import * as THREE from 'three';\nimport { Coords } from '@/game/Coords';\ninterface MeshLineCamera extends THREE.Camera {\n    top?: number;\n    right?: number;\n    rotation: THREE.Euler;\n    userData: THREE.Object3D['userData'] & {\n        meshLineResolution?: {\n            width: number;\n            height: number;\n        };\n    };\n}\nexport function setMeshLineViewportResolution(camera: MeshLineCamera, width: number, height: number): void {\n    camera.userData.meshLineResolution = { width, height };\n}\nexport function getMeshLineResolution(camera: MeshLineCamera): THREE.Vector2 {\n    const viewportResolution = camera.userData.meshLineResolution;\n    if (viewportResolution?.width && viewportResolution?.height) {\n        return new THREE.Vector2(viewportResolution.width, viewportResolution.height);\n    }\n    const top = camera.top ?? 1;\n    const right = camera.right ?? top;\n    const aspectRatio = right / top;\n    const height = (2 * top) / Math.cos(camera.rotation.y);\n    return new THREE.Vector2(height * aspectRatio, height)\n        .multiplyScalar((top * Math.cos(camera.rotation.x)) / Coords.ISO_WORLD_SCALE);\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/MindControlLinkFx.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport * as THREE from 'three';\nexport class MindControlLinkFx {\n    private sourcePos: THREE.Vector3;\n    private targetPos: THREE.Vector3;\n    private color: THREE.Color;\n    private heightTiles: number;\n    private colorAnimProgress: number = 0;\n    private container?: any;\n    private lineMesh?: THREE.Line;\n    private lastUpdate?: number;\n    constructor(sourcePos: THREE.Vector3, targetPos: THREE.Vector3, color: THREE.Color, heightTiles: number) {\n        this.sourcePos = sourcePos;\n        this.targetPos = targetPos;\n        this.color = color;\n        this.heightTiles = heightTiles;\n    }\n    setContainer(container: any): void {\n        this.container = container;\n    }\n    get3DObject(): THREE.Line | undefined {\n        return this.lineMesh;\n    }\n    create3DObject(): void {\n        if (!this.lineMesh) {\n            this.lineMesh = this.createObject();\n            this.lineMesh.name = \"fx_mclink\";\n        }\n    }\n    updateEndpoints(sourcePos: THREE.Vector3, targetPos: THREE.Vector3): void {\n        const hasChanged = !sourcePos.equals(this.sourcePos) || !targetPos.equals(this.targetPos);\n        this.sourcePos = sourcePos;\n        this.targetPos = targetPos;\n        if (hasChanged && this.lineMesh) {\n            this.lineMesh.geometry.dispose();\n            this.lineMesh.geometry = this.createLineGeometry(this.sourcePos, this.targetPos, this.heightTiles, this.color, this.colorAnimProgress);\n        }\n    }\n    update(timeMillis: number): void {\n        if (!this.lastUpdate) {\n            this.lastUpdate = timeMillis;\n        }\n        this.colorAnimProgress += (timeMillis - this.lastUpdate) / 1000;\n        this.colorAnimProgress -= Math.floor(this.colorAnimProgress);\n        this.lastUpdate = timeMillis;\n        this.lineMesh!.geometry.dispose();\n        this.lineMesh!.geometry = this.createLineGeometry(this.sourcePos, this.targetPos, this.heightTiles, this.color, this.colorAnimProgress);\n    }\n    private createObject(): THREE.Line {\n        const geometry = this.createLineGeometry(this.sourcePos, this.targetPos, this.heightTiles, this.color, this.colorAnimProgress);\n        const material = new THREE.LineBasicMaterial({\n            vertexColors: true,\n            transparent: true\n        });\n        const line = new THREE.Line(geometry, material);\n        line.renderOrder = 1000000;\n        return line;\n    }\n    private createLineGeometry(source: THREE.Vector3, target: THREE.Vector3, heightTiles: number, color: THREE.Color, animProgress: number): THREE.BufferGeometry {\n        const white = new THREE.Color(0xFFFFFF);\n        const colorAnimPos = 1.5 * animProgress;\n        const distanceTiles = target.clone().sub(source).length() / Coords.LEPTONS_PER_TILE;\n        const numPoints = Math.max(2, Math.floor(15 * distanceTiles) + 1);\n        const positions = new Float32Array(3 * numPoints);\n        const colors = new Float32Array(3 * numPoints);\n        const tempVec = new THREE.Vector3();\n        for (let i = 0; i < numPoints; i++) {\n            const t = numPoints === 1 ? 0 : i / (numPoints - 1);\n            tempVec.lerpVectors(source, target, t);\n            tempVec.y += Coords.LEPTONS_PER_TILE / 4 +\n                heightTiles * Coords.LEPTONS_PER_TILE * Math.sin(t * Math.PI);\n            positions[3 * i] = tempVec.x;\n            positions[3 * i + 1] = tempVec.y;\n            positions[3 * i + 2] = tempVec.z;\n            let pointColor = color;\n            if (t < colorAnimPos && colorAnimPos - 0.5 <= t) {\n                pointColor = color.clone().lerp(white, (t - (colorAnimPos - 0.5)) / 0.5);\n            }\n            colors[3 * i] = pointColor.r;\n            colors[3 * i + 1] = pointColor.g;\n            colors[3 * i + 2] = pointColor.b;\n        }\n        const geometry = new THREE.BufferGeometry();\n        geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n        geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n        return geometry;\n    }\n    removeAndDispose(): void {\n        this.container?.remove(this);\n        this.dispose();\n    }\n    dispose(): void {\n        if (this.lineMesh) {\n            this.lineMesh.geometry.dispose();\n            (this.lineMesh.material as any).dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/RadBeamFx.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport * as THREE from 'three';\nimport { MeshLine, MeshLineMaterial } from 'three.meshline';\nimport { truncToDecimals } from '@/util/math';\nimport { getMeshLineResolution } from '@/engine/renderable/fx/MeshLineResolution';\nexport class RadBeamFx {\n    private camera: THREE.Camera;\n    private sourcePos: THREE.Vector3;\n    private targetPos: THREE.Vector3;\n    private color: THREE.Color;\n    private durationSeconds: number;\n    private width: number;\n    private amplitude: number = 0;\n    private container?: any;\n    private lineMesh?: THREE.Mesh;\n    private firstUpdateMillis?: number;\n    private timeLeft: number = 1;\n    constructor(camera: THREE.Camera, sourcePos: THREE.Vector3, targetPos: THREE.Vector3, color: THREE.Color, durationSeconds: number, width: number) {\n        this.camera = camera;\n        this.sourcePos = sourcePos;\n        this.targetPos = targetPos;\n        this.color = color;\n        this.durationSeconds = durationSeconds;\n        this.width = width;\n    }\n    setContainer(container: any): void {\n        this.container = container;\n    }\n    get3DObject(): THREE.Mesh | undefined {\n        return this.lineMesh;\n    }\n    create3DObject(): void {\n        if (!this.lineMesh) {\n            this.lineMesh = this.createObject();\n            this.lineMesh.name = \"fx_radbeam\";\n        }\n    }\n    update(timeMillis: number): void {\n        if (!this.firstUpdateMillis) {\n            this.firstUpdateMillis = timeMillis;\n        }\n        this.timeLeft = Math.max(0, 1 - (timeMillis - this.firstUpdateMillis) / (1000 * this.durationSeconds));\n        const newAmplitude = truncToDecimals((Coords.LEPTONS_PER_TILE / 6) * (1 - this.timeLeft), 1);\n        if (newAmplitude !== this.amplitude) {\n            this.amplitude = newAmplitude;\n            this.lineMesh!.geometry.dispose();\n            this.lineMesh!.geometry = this.createLineGeometry(this.sourcePos, this.targetPos, this.amplitude);\n        }\n        if (this.isFinished()) {\n            this.container.remove(this);\n            this.dispose();\n        }\n    }\n    private createObject(): THREE.Mesh {\n        const sourcePos = this.sourcePos.clone();\n        const targetPos = this.targetPos.clone();\n        const geometry = this.createLineGeometry(sourcePos, targetPos, this.amplitude);\n        const material = new MeshLineMaterial({\n            color: this.color.clone(),\n            lineWidth: this.width,\n            resolution: getMeshLineResolution(this.camera),\n            transparent: true,\n            sizeAttenuation: 0,\n        });\n        return new THREE.Mesh(geometry, material);\n    }\n    private createLineGeometry(sourcePos: THREE.Vector3, targetPos: THREE.Vector3, amplitude: number): THREE.BufferGeometry {\n        const points: number[] = [];\n        const distance = targetPos.clone().sub(sourcePos).length() / Coords.LEPTONS_PER_TILE;\n        const segments = 15 * distance;\n        const tempVec = new THREE.Vector3();\n        for (let i = 0; i <= segments; i++) {\n            const t = i / segments;\n            tempVec.lerpVectors(sourcePos, targetPos, t);\n            tempVec.y += amplitude * Math.sin(t * distance * (Coords.LEPTONS_PER_TILE / Math.PI));\n            points.push(tempVec.x, tempVec.y, tempVec.z);\n        }\n        const meshLine = new MeshLine();\n        meshLine.setPoints(points);\n        return meshLine.geometry;\n    }\n    private isFinished(): boolean {\n        return this.timeLeft === 0;\n    }\n    dispose(): void {\n        if (this.lineMesh) {\n            this.lineMesh.geometry.dispose();\n            (this.lineMesh.material as any).dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/RallyPointFx.ts",
    "content": "import * as THREE from 'three';\nimport { MeshLine, MeshLineMaterial } from 'three.meshline';\nimport { Coords } from '@/game/Coords';\nimport { getMeshLineResolution } from '@/engine/renderable/fx/MeshLineResolution';\ninterface Camera extends THREE.Camera {\n    top: number;\n    right: number;\n    rotation: THREE.Euler;\n}\ninterface Container {\n    remove(item: RallyPointFx): void;\n}\nexport class RallyPointFx {\n    private camera: Camera;\n    public sourcePos: THREE.Vector3;\n    public targetPos: THREE.Vector3;\n    public color: THREE.Color;\n    private renderOrder?: number;\n    public needsUpdate: boolean = false;\n    public visible: boolean = true;\n    private cameraHash: string;\n    private container?: Container;\n    private wrapper?: THREE.Object3D;\n    private lineMesh?: THREE.Mesh;\n    private shadowLineMesh?: THREE.Mesh;\n    private lastUpdateMillis?: number;\n    constructor(camera: Camera, sourcePos: THREE.Vector3, targetPos: THREE.Vector3, color: THREE.Color, renderOrder?: number) {\n        this.camera = camera;\n        this.sourcePos = sourcePos;\n        this.targetPos = targetPos;\n        this.color = color;\n        this.renderOrder = renderOrder;\n        this.cameraHash = this.camera.top + \"_\" + this.camera.right;\n    }\n    setContainer(container: Container): void {\n        this.container = container;\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.wrapper;\n    }\n    create3DObject(): void {\n        if (!this.wrapper) {\n            this.wrapper = new THREE.Object3D();\n            this.wrapper.matrixAutoUpdate = false;\n            this.lineMesh = this.createLineMesh();\n            this.lineMesh.name = \"fx_rallypoint\";\n            this.lineMesh.matrixAutoUpdate = false;\n            this.shadowLineMesh = this.createLineShadowMesh();\n            this.shadowLineMesh.name = \"fx_rallypoint_shadow\";\n            this.shadowLineMesh.matrixAutoUpdate = false;\n            this.wrapper.add(this.lineMesh);\n            this.wrapper.add(this.shadowLineMesh);\n        }\n    }\n    update(currentTime: number): void {\n        if (!this.lastUpdateMillis) {\n            this.lastUpdateMillis = currentTime;\n        }\n        const deltaTime = (currentTime - this.lastUpdateMillis) / (1000 / 120);\n        this.lastUpdateMillis = currentTime;\n        if (this.wrapper) {\n            this.wrapper.visible = this.visible;\n        }\n        const currentCameraHash = this.camera.top + \"_\" + this.camera.right;\n        if (currentCameraHash !== this.cameraHash) {\n            this.cameraHash = currentCameraHash;\n            [this.lineMesh, this.shadowLineMesh].forEach((mesh) => {\n                const material = mesh?.material as MeshLineMaterial | undefined;\n                if (material && (material as any).isMeshLineMaterial) {\n                    material.uniforms.resolution.value.copy(this.computeResolution(this.camera));\n                }\n            });\n        }\n        if (this.needsUpdate) {\n            this.needsUpdate = false;\n            if (this.lineMesh) {\n                this.lineMesh.geometry = this.createLineGeometry(this.sourcePos, this.targetPos);\n            }\n            if (this.shadowLineMesh) {\n                this.shadowLineMesh.geometry = this.createShadowLineGeometry(this.sourcePos, this.targetPos);\n            }\n            const lineMat = this.lineMesh?.material as MeshLineMaterial | undefined;\n            if (lineMat && (lineMat as any).isMeshLineMaterial) {\n                lineMat.uniforms.color.value = this.color.clone();\n            }\n            const distance = this.sourcePos.distanceTo(this.targetPos);\n            [this.lineMesh, this.shadowLineMesh].forEach((mesh) => {\n                const material = mesh?.material as MeshLineMaterial | undefined;\n                if (material && (material as any).isMeshLineMaterial) {\n                    material.uniforms.dashArray.value = this.computeDashArray(distance);\n                    (material as any).depthTest = this.renderOrder === undefined;\n                }\n            });\n            if (this.lineMesh) {\n                this.lineMesh.renderOrder = this.renderOrder ?? 0;\n            }\n            if (this.shadowLineMesh) {\n                this.shadowLineMesh.renderOrder = this.renderOrder !== undefined ? this.renderOrder - 1 : 0;\n            }\n        }\n        [this.lineMesh, this.shadowLineMesh].forEach((mesh) => {\n            const material = mesh?.material as MeshLineMaterial | undefined;\n            if (material && (material as any).isMeshLineMaterial) {\n                material.uniforms.dashOffset.value -=\n                    (material.uniforms.dashArray.value / 50) * deltaTime;\n            }\n        });\n    }\n    private createLineMesh(): THREE.Mesh {\n        const sourcePos = this.sourcePos.clone();\n        const targetPos = this.targetPos.clone();\n        const mesh = new THREE.Mesh(this.createLineGeometry(sourcePos, targetPos), this.createLineMaterial(this.color.clone(), sourcePos.distanceTo(targetPos)));\n        if (this.renderOrder) {\n            mesh.renderOrder = this.renderOrder;\n        }\n        return mesh;\n    }\n    private createLineShadowMesh(): THREE.Mesh {\n        const mesh = new THREE.Mesh(this.createShadowLineGeometry(this.sourcePos, this.targetPos), this.createLineMaterial(new THREE.Color(0x000000), this.sourcePos.distanceTo(this.targetPos)));\n        if (this.renderOrder) {\n            mesh.renderOrder = this.renderOrder - 1;\n        }\n        return mesh;\n    }\n    private createShadowLineGeometry(sourcePos: THREE.Vector3, targetPos: THREE.Vector3): THREE.BufferGeometry {\n        const offset = new THREE.Vector3(+Coords.ISO_WORLD_SCALE, 0, +Coords.ISO_WORLD_SCALE);\n        return this.createLineGeometry(sourcePos.clone().add(offset), targetPos.clone().add(offset));\n    }\n    private createLineGeometry(sourcePos: THREE.Vector3, targetPos: THREE.Vector3): THREE.BufferGeometry {\n        const points = [\n            sourcePos.x, sourcePos.y, sourcePos.z,\n            targetPos.x, targetPos.y, targetPos.z,\n        ];\n        const meshLine = new MeshLine();\n        meshLine.setPoints(points);\n        return meshLine.geometry;\n    }\n    private createLineMaterial(color: THREE.Color, distance: number): MeshLineMaterial {\n        return new MeshLineMaterial({\n            color: color,\n            lineWidth: 2,\n            resolution: this.computeResolution(this.camera),\n            transparent: true,\n            sizeAttenuation: 0,\n            dashArray: this.computeDashArray(distance),\n            depthTest: this.renderOrder === undefined,\n        });\n    }\n    private computeDashArray(distance: number): number {\n        return Math.min(1, 5 / distance) * Coords.ISO_WORLD_SCALE;\n    }\n    private computeResolution(camera: Camera): THREE.Vector2 {\n        return getMeshLineResolution(camera as unknown as THREE.Camera);\n    }\n    remove(): void {\n        if (this.container) {\n            this.container.remove(this);\n        }\n    }\n    dispose(): void {\n        if (this.wrapper) {\n            [this.lineMesh, this.shadowLineMesh].forEach((mesh) => {\n                if (mesh) {\n                    if (mesh.geometry) {\n                        mesh.geometry.dispose();\n                    }\n                    if (mesh.material instanceof THREE.Material) {\n                        mesh.material.dispose();\n                    }\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/SparkFx.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport * as THREE from 'three';\nimport SPE from './speRuntime';\nimport { patchSpeGroup } from './speCompat';\nexport class SparkFx {\n    private static readonly PARTICLE_LIFETIME = 1;\n    private static readonly MAX_PARTICLE_COUNT = 100;\n    private static sparkTex?: THREE.DataTexture;\n    private pos: THREE.Vector3;\n    private color: THREE.Color;\n    private spawnDurationSeconds: number;\n    private gameSpeed: {\n        value: number;\n    };\n    private totalDurationSeconds: number;\n    private container?: any;\n    private particleGroup?: SPE.Group;\n    private particleEmitter?: SPE.Emitter;\n    private firstUpdateMillis?: number;\n    private lastUpdateMillis?: number;\n    private timeLeft: number = 1;\n    constructor(pos: THREE.Vector3, color: THREE.Color, spawnDurationSeconds: number, gameSpeed: {\n        value: number;\n    }) {\n        this.pos = pos;\n        this.color = color;\n        this.spawnDurationSeconds = spawnDurationSeconds;\n        this.gameSpeed = gameSpeed;\n        this.totalDurationSeconds = spawnDurationSeconds + SparkFx.PARTICLE_LIFETIME;\n    }\n    setContainer(container: any): void {\n        this.container = container;\n    }\n    create3DObject(): void {\n        if (!this.particleGroup) {\n            if (!SparkFx.sparkTex) {\n                SparkFx.sparkTex = new THREE.DataTexture(new Uint8Array(4).fill(255), 1, 1, THREE.RGBAFormat);\n                SparkFx.sparkTex.needsUpdate = true;\n            }\n            this.particleGroup = new SPE.Group({\n                texture: { value: SparkFx.sparkTex },\n                maxParticleCount: SparkFx.MAX_PARTICLE_COUNT,\n            });\n            patchSpeGroup(this.particleGroup);\n            this.particleGroup.mesh.name = \"fx_spark\";\n            this.particleGroup.mesh.frustumCulled = false;\n            this.particleEmitter = new SPE.Emitter({\n                maxAge: { value: SparkFx.PARTICLE_LIFETIME },\n                position: {\n                    value: this.pos,\n                    spread: new THREE.Vector3(10, 0, 10).multiplyScalar(Coords.ISO_WORLD_SCALE),\n                },\n                acceleration: {\n                    value: new THREE.Vector3(0, -50, 0).multiplyScalar(Coords.ISO_WORLD_SCALE),\n                    spread: new THREE.Vector3(0, 0, 0),\n                },\n                velocity: {\n                    value: new THREE.Vector3(0, 30, 0).multiplyScalar(Coords.ISO_WORLD_SCALE),\n                    spread: new THREE.Vector3(40, 5, 40).multiplyScalar(Coords.ISO_WORLD_SCALE),\n                },\n                color: { value: [this.color] },\n                opacity: { value: [1, 0.5] },\n                size: { value: 1 },\n                particleCount: SparkFx.MAX_PARTICLE_COUNT,\n            });\n            this.particleGroup.addEmitter(this.particleEmitter);\n        }\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.particleGroup?.mesh;\n    }\n    update(timeMillis: number): void {\n        if (this.lastUpdateMillis) {\n            const deltaTime = timeMillis - this.lastUpdateMillis;\n            this.particleGroup?.tick((deltaTime / 1000) * this.gameSpeed.value);\n        }\n        else {\n            this.firstUpdateMillis = timeMillis;\n            this.particleGroup?.tick(0);\n        }\n        this.lastUpdateMillis = timeMillis;\n        if (this.particleEmitter?.alive &&\n            timeMillis - this.firstUpdateMillis! >=\n                (1000 * this.spawnDurationSeconds) / this.gameSpeed.value) {\n            this.particleEmitter.disable();\n        }\n        this.timeLeft = Math.max(0, 1 -\n            (timeMillis - this.firstUpdateMillis!) /\n                ((1000 * this.totalDurationSeconds) / this.gameSpeed.value));\n        if (!this.timeLeft) {\n            this.container?.remove(this);\n            this.dispose();\n        }\n    }\n    dispose(): void {\n        this.particleGroup?.mesh.geometry.dispose();\n        this.particleGroup?.mesh.material.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/TeslaFx.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport * as THREE from 'three';\ntype TeslaBoltRuntime = {\n    line: THREE.Line;\n    material: THREE.LineBasicMaterial;\n    seed: number;\n    update: (elapsedSeconds: number) => void;\n    dispose: () => void;\n};\nexport class TeslaFx {\n    private sourcePos: THREE.Vector3;\n    private targetPos: THREE.Vector3;\n    private primaryColor: THREE.Color;\n    private secondaryColor: THREE.Color;\n    private durationSeconds: number;\n    private bolts: TeslaBoltRuntime[];\n    private boltMeshes: THREE.Object3D[];\n    private container?: any;\n    private target?: THREE.Object3D;\n    private firstUpdateMillis?: number;\n    private timeLeft: number = 1;\n    constructor(sourcePos: THREE.Vector3, targetPos: THREE.Vector3, primaryColor: THREE.Color, secondaryColor: THREE.Color, durationSeconds: number) {\n        this.sourcePos = sourcePos;\n        this.targetPos = targetPos;\n        this.primaryColor = primaryColor;\n        this.secondaryColor = secondaryColor;\n        this.durationSeconds = durationSeconds;\n        this.bolts = [];\n        this.boltMeshes = [];\n    }\n    setContainer(container: any): void {\n        this.container = container;\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    create3DObject(): void {\n        if (!this.target) {\n            this.target = new THREE.Object3D();\n            this.target.name = \"fx_tesla\";\n            const primaryHex = this.primaryColor.getHex();\n            const colors = [primaryHex, primaryHex, this.secondaryColor.getHex()];\n            colors.forEach((color) => {\n                try {\n                    const { mesh, bolt } = this.createBolt(color);\n                    this.boltMeshes.push(mesh);\n                    this.bolts.push(bolt);\n                    this.target?.add(mesh);\n                }\n                catch (e) {\n                    console.warn(\"Couldn't create lightning FX\", [e]);\n                }\n            });\n        }\n    }\n    update(timeMillis: number): void {\n        if (!this.firstUpdateMillis) {\n            this.firstUpdateMillis = timeMillis;\n        }\n        const elapsedSeconds = (timeMillis - this.firstUpdateMillis) / 1000;\n        this.timeLeft = Math.max(0, 1 - elapsedSeconds / this.durationSeconds);\n        try {\n            this.bolts.forEach(bolt => bolt.update(elapsedSeconds));\n        }\n        catch (e) {\n            console.warn(\"Couldn't update lightning FX\", [e]);\n        }\n        if (this.isFinished()) {\n            this.container?.remove(this);\n            this.dispose();\n        }\n    }\n    private createBolt(color: number): {\n        mesh: THREE.Line;\n        bolt: TeslaBoltRuntime;\n    } {\n        const sourceOffset = this.sourcePos.clone();\n        const destOffset = this.targetPos.clone();\n        const material = new THREE.LineBasicMaterial({\n            color,\n            transparent: true,\n            opacity: 0.9,\n        });\n        const line = new THREE.Line(new THREE.BufferGeometry(), material);\n        const seed = Math.random() * Math.PI * 2;\n        const pointCount = 10;\n        const rebuildGeometry = (elapsedSeconds: number) => {\n            const direction = destOffset.clone().sub(sourceOffset);\n            const distance = Math.max(direction.length(), 1);\n            const forward = direction.normalize();\n            const reference = Math.abs(forward.y) > 0.9 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0);\n            const right = new THREE.Vector3().crossVectors(forward, reference).normalize();\n            const up = new THREE.Vector3().crossVectors(forward, right).normalize();\n            const amplitude = Math.max(0.18 * Coords.ISO_WORLD_SCALE, distance * 0.02);\n            const points: THREE.Vector3[] = [];\n            for (let i = 0; i < pointCount; i++) {\n                const t = (pointCount as number) === 1 ? 0 : i / (pointCount - 1);\n                const point = sourceOffset.clone().lerp(destOffset, t);\n                if (i !== 0 && i !== pointCount - 1) {\n                    const envelope = Math.sin(t * Math.PI);\n                    const phase = elapsedSeconds * 18 + seed + t * Math.PI * 4;\n                    point.addScaledVector(right, Math.sin(phase) * amplitude * envelope);\n                    point.addScaledVector(up, Math.cos(phase * 1.31) * amplitude * envelope * 0.6);\n                }\n                points.push(point);\n            }\n            line.geometry.dispose();\n            line.geometry = new THREE.BufferGeometry().setFromPoints(points);\n        };\n        const bolt: TeslaBoltRuntime = {\n            line,\n            material,\n            seed,\n            update: rebuildGeometry,\n            dispose: () => {\n                line.geometry.dispose();\n                material.dispose();\n            },\n        };\n        bolt.update(0);\n        return { mesh: line, bolt };\n    }\n    isFinished(): boolean {\n        return this.timeLeft === 0;\n    }\n    dispose(): void {\n        this.bolts.forEach((bolt) => bolt.dispose());\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/TrailerSmokeFx.ts",
    "content": "import { AnimProps } from \"@/engine/AnimProps\";\nimport { ImageUtils } from \"@/engine/gfx/ImageUtils\";\nimport * as THREE from \"three\";\nimport SPELib from \"./speRuntime\";\nimport { patchSpeGroup } from \"./speCompat\";\ninterface SmokeArt {\n    art: {\n        getBool(key: string): boolean;\n    };\n    translucent: boolean;\n    translucency: number;\n}\ninterface ShpFile {\n    numImages: number;\n    height: number;\n    width: number;\n}\ninterface GameSpeed {\n    value: number;\n}\ninterface Container {\n    remove(item: TrailerSmokeFx): void;\n}\ndeclare namespace SPE {\n    interface GroupConfig {\n        texture: {\n            value: THREE.Texture;\n            frames: THREE.Vector2;\n            frameCount: number;\n            loop: number;\n        };\n        maxParticleCount: number;\n        hasPerspective: boolean;\n        transparent: boolean;\n        alphaTest: number;\n        blending: THREE.Blending;\n    }\n    interface EmitterConfig {\n        particleCount: number;\n        maxAge: {\n            value: number;\n        };\n        activeMultiplier: number;\n        position: {\n            value: THREE.Vector3;\n        };\n        acceleration: {\n            value: THREE.Vector3;\n        };\n        velocity: {\n            value: THREE.Vector3;\n        };\n        opacity: {\n            value: number | number[];\n        };\n        size: {\n            value: number;\n        };\n    }\n    class Group {\n        mesh: THREE.Mesh;\n        constructor(config: GroupConfig);\n        addEmitter(emitter: Emitter): void;\n        tick(deltaTime: number): void;\n    }\n    class Emitter {\n        position: {\n            value: THREE.Vector3;\n        };\n        alive: boolean;\n        constructor(config: EmitterConfig);\n        disable(): void;\n        enable(): void;\n    }\n}\nconst MAX_PARTICLES = 1000;\nconst PARTICLE_COUNT = 1000;\nexport class TrailerSmokeFx {\n    private static textureCache: Map<ShpFile, THREE.Texture> = new Map();\n    private pos: THREE.Vector3;\n    private spawnDelayFrames: number;\n    private smokeArt: SmokeArt;\n    private shpFile: ShpFile;\n    private palette: any;\n    private gameSpeed: GameSpeed;\n    private lifetimeSeconds: number;\n    private finishRequested: boolean;\n    private finishProcessed: boolean;\n    private container?: Container;\n    private particleGroup?: SPE.Group;\n    private particleEmitter?: SPE.Emitter;\n    private particleMaxAge?: number;\n    private lastUpdateMillis?: number;\n    private firstUpdateMillis?: number;\n    private timeLeft?: number;\n    static clearTextureCache(): void {\n        this.textureCache.forEach((texture) => texture.dispose());\n        this.textureCache.clear();\n    }\n    constructor(pos: THREE.Vector3, spawnDelayFrames: number, smokeArt: SmokeArt, shpFile: ShpFile, palette: any, gameSpeed: GameSpeed) {\n        this.pos = pos;\n        this.spawnDelayFrames = spawnDelayFrames;\n        this.smokeArt = smokeArt;\n        this.shpFile = shpFile;\n        this.palette = palette;\n        this.gameSpeed = gameSpeed;\n        this.lifetimeSeconds = Number.POSITIVE_INFINITY;\n        this.finishRequested = false;\n        this.finishProcessed = false;\n    }\n    setContainer(container: Container): void {\n        this.container = container;\n    }\n    create3DObject(): void {\n        if (!this.particleGroup) {\n            let texture = TrailerSmokeFx.textureCache.get(this.shpFile);\n            if (!texture) {\n                const canvas = ImageUtils.convertShpToCanvas(this.shpFile as any, this.palette, true);\n                texture = new THREE.Texture(canvas);\n                texture.minFilter = THREE.NearestFilter;\n                texture.magFilter = THREE.NearestFilter;\n                texture.needsUpdate = true;\n                texture.flipY = true;\n                TrailerSmokeFx.textureCache.set(this.shpFile, texture);\n            }\n            this.particleGroup = new SPELib.Group({\n                texture: {\n                    value: texture,\n                    frames: new THREE.Vector2(this.shpFile.numImages, 1),\n                    frameCount: this.shpFile.numImages,\n                    loop: 1,\n                },\n                maxParticleCount: MAX_PARTICLES,\n                hasPerspective: false,\n                transparent: true,\n                alphaTest: 0,\n                blending: THREE.NormalBlending,\n            });\n            patchSpeGroup(this.particleGroup);\n            this.particleGroup.mesh.name = \"fx_trailer_smoke\";\n            this.particleGroup.mesh.frustumCulled = false;\n            const animProps = new AnimProps(this.smokeArt.art as any, this.shpFile as any);\n            const activeMultiplier = ((this.smokeArt.art.getBool(\"Normalized\") ? 2 : 1) * animProps.rate) /\n                this.spawnDelayFrames;\n            this.particleMaxAge = this.shpFile.numImages / animProps.rate;\n            this.particleEmitter = new SPELib.Emitter({\n                particleCount: PARTICLE_COUNT,\n                maxAge: { value: this.particleMaxAge },\n                activeMultiplier: activeMultiplier / (PARTICLE_COUNT / this.particleMaxAge),\n                position: { value: this.pos },\n                acceleration: { value: new THREE.Vector3() },\n                velocity: { value: new THREE.Vector3() },\n                opacity: {\n                    value: this.smokeArt.translucent\n                        ? [1, 0]\n                        : 1 - this.smokeArt.translucency,\n                },\n                size: {\n                    value: Math.max(this.shpFile.height, this.shpFile.width),\n                },\n            });\n            this.particleGroup.addEmitter(this.particleEmitter);\n        }\n    }\n    get3DObject(): THREE.Mesh | undefined {\n        return this.particleGroup?.mesh;\n    }\n    update(currentTime: number): void {\n        if (!this.particleEmitter || !this.particleGroup)\n            return;\n        this.particleEmitter.position.value = this.pos;\n        if (this.lastUpdateMillis) {\n            const deltaTime = currentTime - this.lastUpdateMillis;\n            this.particleGroup.tick((deltaTime / 1000) * this.gameSpeed.value);\n        }\n        else {\n            this.firstUpdateMillis = currentTime;\n            this.particleGroup.tick(0);\n        }\n        this.lastUpdateMillis = currentTime;\n        if (this.finishRequested) {\n            this.finishRequested = false;\n            if (!this.finishProcessed) {\n                this.finishProcessed = true;\n                const elapsedSeconds = ((currentTime - (this.firstUpdateMillis || 0)) / 1000) *\n                    this.gameSpeed.value;\n                this.lifetimeSeconds = elapsedSeconds + (this.particleMaxAge || 0);\n            }\n            if (this.particleEmitter.alive) {\n                this.particleEmitter.disable();\n            }\n        }\n        this.timeLeft = Math.max(0, 1 -\n            (currentTime - (this.firstUpdateMillis || 0)) /\n                ((1000 * this.lifetimeSeconds) / this.gameSpeed.value));\n        if (!this.timeLeft) {\n            this.container?.remove(this);\n            this.dispose();\n        }\n    }\n    finishAndRemove(): void {\n        this.finishRequested = true;\n    }\n    disable(): void {\n        this.particleEmitter?.disable();\n    }\n    enable(): void {\n        this.particleEmitter?.enable();\n    }\n    dispose(): void {\n        this.particleGroup?.mesh.geometry.dispose();\n        if (this.particleGroup?.mesh.material instanceof THREE.Material) {\n            this.particleGroup.mesh.material.dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/handler/BeaconFxHandler.ts",
    "content": "import { EventType } from '@/game/event/EventType';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { Coords } from '@/game/Coords';\nimport { SoundKey } from '@/engine/sound/SoundKey';\ninterface Game {\n    events: {\n        subscribe: (event: EventType, handler: (event: any) => void) => {\n            dispose: () => void;\n        };\n    };\n    alliances: {\n        areAllied: (player1: Player, player2: Player) => boolean;\n    };\n    map: {\n        tileOccupation: {\n            getBridgeOnTile: (tile: Tile) => Bridge | undefined;\n        };\n    };\n}\ninterface Player {\n    isObserver: boolean;\n    color: any;\n}\ninterface Tile {\n    rx: number;\n    ry: number;\n    z: number;\n    onBridgeLandType: boolean;\n}\ninterface Bridge {\n    tileElevation: number;\n}\ninterface RenderableManager {\n    createTransientAnim: (name: string, callback: (anim: any) => void) => any;\n}\ninterface Renderer {\n    onFrame: {\n        subscribe: (handler: (time: number) => void) => {\n            unsubscribe: (handler: (time: number) => void) => void;\n        };\n    };\n}\ninterface WorldSound {\n    playEffect: (sound: SoundKey, position: any, player: Player) => void;\n}\ninterface Beacon {\n    tile: Tile;\n    anim: any;\n    startTime?: number;\n}\nexport class BeaconFxHandler {\n    private game: Game;\n    private localPlayer: {\n        value: Player;\n    };\n    private renderableManager: RenderableManager;\n    private renderer: Renderer;\n    private worldSound: WorldSound;\n    private disposables: CompositeDisposable;\n    private beacons: Map<Player, Beacon[]>;\n    private now?: number;\n    constructor(game: Game, localPlayer: {\n        value: Player;\n    }, renderableManager: RenderableManager, renderer: Renderer, worldSound: WorldSound) {\n        this.game = game;\n        this.localPlayer = localPlayer;\n        this.renderableManager = renderableManager;\n        this.renderer = renderer;\n        this.worldSound = worldSound;\n        this.disposables = new CompositeDisposable();\n        this.beacons = new Map();\n    }\n    private handlePingEvent = (event: {\n        player: Player;\n        tile: Tile;\n    }) => {\n        const localPlayer = this.localPlayer.value;\n        if ((!localPlayer ||\n            localPlayer.isObserver ||\n            event.player === localPlayer ||\n            this.game.alliances.areAllied(event.player, localPlayer)) &&\n            this.canPingLocation(event.player, event.tile)) {\n            let beacons = this.beacons.get(event.player);\n            if (!beacons) {\n                beacons = [];\n                this.beacons.set(event.player, beacons);\n            }\n            let existingBeacon = beacons.find((b) => b.tile === event.tile);\n            const bridge = event.tile.onBridgeLandType\n                ? this.game.map.tileOccupation.getBridgeOnTile(event.tile)\n                : undefined;\n            const position = Coords.tile3dToWorld(event.tile.rx + 0.5, event.tile.ry + 0.5, event.tile.z + (bridge?.tileElevation ?? 0));\n            this.worldSound.playEffect(SoundKey.PlaceBeaconSound, position, event.player);\n            if (existingBeacon) {\n                existingBeacon.startTime = this.now;\n            }\n            else {\n                const anim = this.renderableManager.createTransientAnim(\"PBEACON\", (anim: any) => {\n                    anim.setPosition(position);\n                    anim.setRenderOrder(1000000);\n                    anim.remapColor(event.player.color);\n                    anim.create3DObject();\n                });\n                beacons.push({\n                    tile: event.tile,\n                    anim,\n                    startTime: this.now,\n                });\n            }\n        }\n    };\n    private handleFrame = (time: number) => {\n        this.now = time;\n        for (const beacons of this.beacons.values()) {\n            for (const beacon of beacons.slice()) {\n                if (beacon.startTime === undefined) {\n                    beacon.startTime = time;\n                }\n                else if (time > beacon.startTime + 7000) {\n                    beacon.anim.endAnimationLoop();\n                    const index = beacons.indexOf(beacon);\n                    if (index === -1) {\n                        throw new Error(\"Beacon not found in array\");\n                    }\n                    beacons.splice(index, 1);\n                }\n            }\n        }\n    };\n    init(): void {\n        this.disposables.add(this.game.events.subscribe(EventType.PingLocation, this.handlePingEvent));\n        this.renderer.onFrame.subscribe(this.handleFrame);\n        this.disposables.add(() => (this.renderer.onFrame as any).unsubscribe(this.handleFrame));\n    }\n    canPingLocation(player: Player, tile: Tile): boolean {\n        const beacons = this.beacons.get(player) ?? [];\n        const lastPingTime = beacons.reduce((max, beacon) => Math.max(max, beacon.startTime ?? 0), 0);\n        return ((beacons.length < 3 || beacons.some((b) => b.tile === tile)) &&\n            (!this.now || this.now - lastPingTime >= 1000 / 3));\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/handler/ChronoFxHandler.ts",
    "content": "import { EventType } from '@/game/event/EventType';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { Coords } from '@/game/Coords';\nimport { DeathType } from '@/game/gameobject/common/DeathType';\nimport * as THREE from 'three';\ninterface Game {\n    events: {\n        subscribe: (event: EventType, handler: (event: any) => void) => {\n            dispose: () => void;\n        };\n    };\n    rules: {\n        audioVisual: {\n            warpOut: string;\n            warpAway: string;\n        };\n    };\n}\ninterface RenderableManager {\n    createTransientAnim: (name: string, callback: (anim: any) => void) => any;\n}\ninterface GameObject {\n    position: {\n        getTileOffset: () => THREE.Vector2;\n    };\n    tile: {\n        rx: number;\n        ry: number;\n        z: number;\n    };\n    centerTile?: {\n        rx: number;\n        ry: number;\n        z: number;\n    };\n    isBuilding: () => boolean;\n    deathType: DeathType;\n}\ninterface TeleportEvent {\n    isChronoshift: boolean;\n    target: GameObject;\n    prevTile: {\n        rx: number;\n        ry: number;\n        z: number;\n    };\n}\ninterface DestroyEvent {\n    target: GameObject;\n}\nexport class ChronoFxHandler {\n    private game: Game;\n    private renderableManager: RenderableManager;\n    private disposables: CompositeDisposable;\n    private handleObjectTeleport: (event: TeleportEvent) => void;\n    private handleObjectDestroy: (event: DestroyEvent) => void;\n    constructor(game: Game, renderableManager: RenderableManager) {\n        this.game = game;\n        this.renderableManager = renderableManager;\n        this.disposables = new CompositeDisposable();\n        this.handleObjectTeleport = (event: TeleportEvent) => {\n            if (event.isChronoshift) {\n                const offset = event.target.position\n                    .getTileOffset()\n                    .multiplyScalar(1 / Coords.LEPTONS_PER_TILE);\n                this.renderableManager.createTransientAnim(this.game.rules.audioVisual.warpOut, (anim) => {\n                    anim.setPosition(Coords.tile3dToWorld(event.prevTile.rx + offset.x, event.prevTile.ry + offset.y, event.prevTile.z));\n                });\n                this.renderableManager.createTransientAnim(this.game.rules.audioVisual.warpOut, (anim) => {\n                    const tile = event.target.tile;\n                    anim.setPosition(Coords.tile3dToWorld(tile.rx + offset.x, tile.ry + offset.y, tile.z));\n                });\n            }\n        };\n        this.handleObjectDestroy = (event: DestroyEvent) => {\n            if (event.target.deathType === DeathType.Temporal) {\n                const tile = event.target.isBuilding()\n                    ? event.target.centerTile!\n                    : event.target.tile;\n                const offset = event.target.isBuilding()\n                    ? new THREE.Vector2(0.5, 0.5)\n                    : event.target.position\n                        .getTileOffset()\n                        .multiplyScalar(1 / Coords.LEPTONS_PER_TILE);\n                this.renderableManager.createTransientAnim(this.game.rules.audioVisual.warpAway, (anim) => {\n                    anim.setPosition(Coords.tile3dToWorld(tile.rx + offset.x, tile.ry + offset.y, tile.z));\n                });\n            }\n        };\n    }\n    init(): void {\n        this.disposables.add(this.game.events.subscribe(EventType.ObjectTeleport, this.handleObjectTeleport), this.game.events.subscribe(EventType.ObjectDestroy, this.handleObjectDestroy));\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/handler/CrateFxHandler.ts",
    "content": "import { EventType } from '@/game/event/EventType';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { Coords } from '@/game/Coords';\ninterface Game {\n    events: {\n        subscribe: (event: EventType, handler: (event: any) => void) => {\n            dispose: () => void;\n        };\n    };\n}\ninterface RenderableManager {\n    createTransientAnim: (name: string, callback: (anim: any) => void) => any;\n}\ninterface CratePickupEvent {\n    target: {\n        animName: string;\n    };\n    tile: {\n        rx: number;\n        ry: number;\n        z: number;\n    };\n}\nexport class CrateFxHandler {\n    private game: Game;\n    private renderableManager: RenderableManager;\n    private disposables: CompositeDisposable;\n    constructor(game: Game, renderableManager: RenderableManager) {\n        this.game = game;\n        this.renderableManager = renderableManager;\n        this.disposables = new CompositeDisposable();\n    }\n    init(): void {\n        this.disposables.add(this.game.events.subscribe(EventType.CratePickup, (event: CratePickupEvent) => {\n            const animName = event.target.animName;\n            if (animName) {\n                this.renderableManager.createTransientAnim(animName, (anim) => {\n                    anim.setPosition(Coords.tile3dToWorld(event.tile.rx, event.tile.ry, event.tile.z + 1));\n                    anim.setRenderOrder(1e6);\n                });\n            }\n        }));\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/handler/ParasiteSparkFxHandler.ts",
    "content": "import { EventType } from '@/game/event/EventType';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { Coords } from '@/game/Coords';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { SparkFx } from '@/engine/renderable/fx/SparkFx';\nimport * as THREE from 'three';\ninterface Game {\n    events: {\n        subscribe: (event: EventType, handler: (event: any) => void) => {\n            dispose: () => void;\n        };\n    };\n    speed: any;\n}\ninterface RenderableManager {\n    addEffect: (effect: any) => void;\n}\ninterface GameObject {\n    isVehicle: () => boolean;\n    isAircraft: () => boolean;\n    position: {\n        worldPosition: THREE.Vector3;\n    };\n    parasiteableTrait?: {\n        getParasite: () => GameObject;\n    };\n    healthTrait: {\n        health: number;\n    };\n}\ninterface Attacker {\n    obj?: GameObject;\n}\ninterface DamageEvent {\n    target: GameObject;\n    attacker?: Attacker;\n}\nexport class ParasiteSparkFxHandler {\n    private game: Game;\n    private renderableManager: RenderableManager;\n    private disposables: CompositeDisposable;\n    constructor(game: Game, renderableManager: RenderableManager) {\n        this.game = game;\n        this.renderableManager = renderableManager;\n        this.disposables = new CompositeDisposable();\n        this.handleObjectDamaged = (event: DamageEvent) => {\n            if ((event.target.isVehicle() || event.target.isAircraft()) &&\n                event.attacker?.obj &&\n                !event.attacker.obj.rules.organic &&\n                event.target.parasiteableTrait?.getParasite() === event.attacker.obj &&\n                event.target.healthTrait.health > 0) {\n                const position = event.target.position.worldPosition.clone();\n                position.y += Coords.tileHeightToWorld(0.5);\n                const duration = 20 / GameSpeed.BASE_TICKS_PER_SECOND;\n                const sparkFx = new SparkFx(position, new THREE.Color(1, 1, 1), duration, this.game.speed);\n                this.renderableManager.addEffect(sparkFx);\n            }\n        };\n    }\n    init(): void {\n        this.disposables.add(this.game.events.subscribe(EventType.InflictDamage, this.handleObjectDamaged));\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/handler/SuperWeaponFxHandler.ts",
    "content": "import { EventType } from '@/game/event/EventType';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { getRandomInt } from '@/util/math';\nimport { LightningStormFx } from '@/engine/gfx/lighting/LightningStormFx';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { SuperWeaponType } from '@/game/type/SuperWeaponType';\nimport { Coords } from '@/game/Coords';\ninterface Game {\n    events: {\n        subscribe: (event: EventType, handler: (event: any) => void) => {\n            dispose: () => void;\n        };\n    };\n    rules: {\n        audioVisual: {\n            weatherConClouds: string[];\n            ironCurtainInvokeAnim: string;\n            chronoBlast: string;\n            chronoBlastDest: string;\n            chronoPlacement: string;\n        };\n        general: {\n            lightningStorm: {\n                duration: number;\n            };\n        };\n    };\n    map: {\n        tileOccupation: {\n            getBridgeOnTile: (tile: Tile) => Bridge | undefined;\n        };\n        getIonLighting: () => any;\n    };\n}\ninterface Tile {\n    rx: number;\n    ry: number;\n    z: number;\n}\ninterface Bridge {\n    tileElevation: number;\n}\ninterface RenderableManager {\n    createTransientAnim: (name: string, callback: (anim: any) => void) => any;\n    createAnim: (name: string, callback: (anim: any) => void) => any;\n    getRenderableContainer: () => {\n        remove: (anim: any) => void;\n    } | undefined;\n}\ninterface LightingDirector {\n    addEffect: (effect: any) => void;\n}\ninterface LightningStormEvent {\n    position: any;\n}\ninterface SuperWeaponActivateEvent {\n    target: SuperWeaponType;\n    atTile: Tile;\n    atTile2?: Tile;\n}\nexport class SuperWeaponFxHandler {\n    private game: Game;\n    private renderableManager: RenderableManager;\n    private lightingDirector: LightingDirector;\n    private disposables: CompositeDisposable;\n    private lightingFx?: LightningStormFx;\n    private chronoSphereAnim?: any;\n    constructor(game: Game, renderableManager: RenderableManager, lightingDirector: LightingDirector) {\n        this.game = game;\n        this.renderableManager = renderableManager;\n        this.lightingDirector = lightingDirector;\n        this.disposables = new CompositeDisposable();\n    }\n    init(): void {\n        this.disposables.add(this.game.events.subscribe(EventType.LightningStormCloud, (event: LightningStormEvent) => {\n            const clouds = this.game.rules.audioVisual.weatherConClouds;\n            const cloudAnim = clouds[getRandomInt(0, clouds.length - 1)];\n            const anim = this.renderableManager.createTransientAnim(cloudAnim, (anim) => {\n                anim.setPosition(event.position);\n            });\n            this.lightingFx?.waitForCloudAnim(anim);\n        }), this.game.events.subscribe(EventType.LightningStormManifest, () => {\n            const fx = new LightningStormFx(this.game.rules.general.lightningStorm.duration / GameSpeed.BASE_TICKS_PER_SECOND, this.game.map.getIonLighting());\n            this.lightingFx = fx;\n            this.lightingDirector.addEffect(fx);\n        }), this.game.events.subscribe(EventType.SuperWeaponActivate, (event: SuperWeaponActivateEvent) => {\n            const weaponType = event.target;\n            if (weaponType === SuperWeaponType.IronCurtain) {\n                this.renderableManager.createTransientAnim(this.game.rules.audioVisual.ironCurtainInvokeAnim, (anim) => {\n                    const pos = Coords.tile3dToWorld(event.atTile.rx + 0.5, event.atTile.ry + 0.5, event.atTile.z);\n                    anim.setPosition(pos);\n                });\n            }\n            else if (weaponType === SuperWeaponType.ChronoSphere) {\n                this.disposeChronoSphereAnim();\n                const sourceElevation = this.game.map.tileOccupation.getBridgeOnTile(event.atTile)?.tileElevation ?? 0;\n                const sourcePos = Coords.tile3dToWorld(event.atTile.rx + 0.5, event.atTile.ry + 0.5, event.atTile.z + sourceElevation);\n                const destTile = event.atTile2;\n                const destElevation = this.game.map.tileOccupation.getBridgeOnTile(destTile)?.tileElevation ?? 0;\n                const destPos = Coords.tile3dToWorld(destTile.rx + 0.5, destTile.ry + 0.5, destTile.z + destElevation);\n                this.renderableManager.createTransientAnim(this.game.rules.audioVisual.chronoBlast, (anim) => {\n                    anim.setPosition(sourcePos);\n                });\n                this.renderableManager.createTransientAnim(this.game.rules.audioVisual.chronoBlastDest, (anim) => {\n                    anim.setPosition(destPos);\n                });\n            }\n        }));\n    }\n    createChronoSphereAnim(tile: Tile): void {\n        this.chronoSphereAnim = this.renderableManager.createAnim(this.game.rules.audioVisual.chronoPlacement, (anim) => {\n            const elevation = this.game.map.tileOccupation.getBridgeOnTile(tile)?.tileElevation ?? 0;\n            const pos = Coords.tile3dToWorld(tile.rx + 0.5, tile.ry + 0.5, tile.z + elevation);\n            anim.setPosition(pos);\n        });\n    }\n    disposeChronoSphereAnim(): void {\n        const anim = this.chronoSphereAnim;\n        if (anim) {\n            this.renderableManager.getRenderableContainer()?.remove(anim);\n            anim.dispose();\n        }\n    }\n    dispose(): void {\n        this.lightingFx = undefined;\n        this.disposeChronoSphereAnim();\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/handler/TriggerActionFxHandler.ts",
    "content": "import { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { Coords } from '@/game/Coords';\nimport { EventType } from '@/game/event/EventType';\ninterface Game {\n    events: {\n        subscribe: (event: EventType, handler: (event: any) => void) => {\n            dispose: () => void;\n        };\n    };\n}\ninterface RenderableManager {\n    createTransientAnim: (name: string, callback: (anim: any) => void) => any;\n}\ninterface TriggerAnimEvent {\n    type: EventType;\n    name: string;\n    tile: {\n        rx: number;\n        ry: number;\n        z: number;\n    };\n}\nexport class TriggerActionFxHandler {\n    private game: Game;\n    private renderableManager: RenderableManager;\n    private disposables: CompositeDisposable;\n    constructor(game: Game, renderableManager: RenderableManager) {\n        this.game = game;\n        this.renderableManager = renderableManager;\n        this.disposables = new CompositeDisposable();\n        this.handleEvent = (event: TriggerAnimEvent) => {\n            switch (event.type) {\n                case EventType.TriggerAnim: {\n                    const animName = event.name;\n                    this.renderableManager.createTransientAnim(animName, (anim) => {\n                        const position = Coords.tile3dToWorld(event.tile.rx + 0.5, event.tile.ry + 0.5, event.tile.z);\n                        anim.setPosition(position);\n                    });\n                    break;\n                }\n            }\n        };\n    }\n    init(): void {\n        this.disposables.add(this.game.events.subscribe(this.handleEvent));\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/handler/WarheadDetonateFxHandler.ts",
    "content": "import { EventType } from '@/game/event/EventType';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { Coords } from '@/game/Coords';\nimport { getRandomInt } from '@/util/math';\nimport * as THREE from 'three';\ninterface Game {\n    events: {\n        subscribe: (event: EventType, handler: (event: any) => void) => {\n            dispose: () => void;\n        };\n    };\n    rules: {\n        audioVisual: {\n            weatherConBolts: string[];\n        };\n    };\n}\ninterface RenderableManager {\n    createTransientAnim: (name: string, callback: (anim: any) => void) => any;\n}\ninterface WarheadDetonateEvent {\n    explodeAnim?: string;\n    position: THREE.Vector3;\n    target: {\n        rules: {\n            bullets?: boolean;\n        };\n    };\n    isLightningStrike?: boolean;\n}\nexport class WarheadDetonateFxHandler {\n    private game: Game;\n    private renderableManager: RenderableManager;\n    private disposables: CompositeDisposable;\n    private handleWarheadDetonation: (event: WarheadDetonateEvent) => void;\n    constructor(game: Game, renderableManager: RenderableManager) {\n        this.game = game;\n        this.renderableManager = renderableManager;\n        this.disposables = new CompositeDisposable();\n        this.handleWarheadDetonation = (event: WarheadDetonateEvent) => {\n            let explodeAnim = event.explodeAnim;\n            if (explodeAnim) {\n                this.renderableManager.createTransientAnim(explodeAnim, (anim) => {\n                    let position = event.position.clone();\n                    if (event.target.rules.bullets) {\n                        const offset = Coords.getWorldTileSize() / 8;\n                        position = new THREE.Vector3(getRandomInt(-offset, offset), 0, getRandomInt(-offset, offset)).add(position);\n                    }\n                    anim.setPosition(position);\n                });\n            }\n            if (event.isLightningStrike) {\n                const bolts = this.game.rules.audioVisual.weatherConBolts;\n                explodeAnim = bolts[getRandomInt(0, bolts.length - 1)];\n                this.renderableManager.createTransientAnim(explodeAnim, (anim) => {\n                    anim.setPosition(event.position);\n                });\n            }\n        };\n    }\n    init(): void {\n        this.disposables.add(this.game.events.subscribe(EventType.WarheadDetonate, this.handleWarheadDetonation));\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/speCompat.ts",
    "content": "import SPE from './speRuntime';\nimport type * as THREE from 'three';\nlet shaderPatched = false;\nfunction patchShaderSource(source: string): string {\n    return source\n        .replace(/uniform sampler2D texture;/g, 'uniform sampler2D particleTexture;')\n        .replace(/texture2D\\(\\s*texture\\s*,/g, 'texture2D( particleTexture,');\n}\nfunction patchShaders(): void {\n    if (shaderPatched) {\n        return;\n    }\n    shaderPatched = true;\n    const speAny = SPE as any;\n    if (typeof speAny.shaderChunks?.uniforms === 'string') {\n        speAny.shaderChunks.uniforms = patchShaderSource(speAny.shaderChunks.uniforms);\n    }\n    if (typeof speAny.shaders?.vertex === 'string') {\n        speAny.shaders.vertex = patchShaderSource(speAny.shaders.vertex);\n    }\n    if (typeof speAny.shaders?.fragment === 'string') {\n        speAny.shaders.fragment = patchShaderSource(speAny.shaders.fragment);\n    }\n}\nexport function patchSpeGroup(group: any): any {\n    patchShaders();\n    const material = group?.material ?? group?.mesh?.material;\n    if (material) {\n        if (typeof material.vertexShader === 'string') {\n            material.vertexShader = patchShaderSource(material.vertexShader);\n        }\n        if (typeof material.fragmentShader === 'string') {\n            material.fragmentShader = patchShaderSource(material.fragmentShader);\n        }\n        if (material.uniforms?.texture && !material.uniforms.particleTexture) {\n            material.uniforms.particleTexture = material.uniforms.texture;\n            delete material.uniforms.texture;\n        }\n        material.needsUpdate = true;\n    }\n    const attributes = group?.attributes;\n    if (attributes) {\n        for (const attribute of Object.values(attributes) as Array<{\n            bufferAttribute?: THREE.BufferAttribute;\n        }>) {\n            if (attribute.bufferAttribute && !(attribute.bufferAttribute as any).updateRange) {\n                (attribute.bufferAttribute as any).updateRange = { offset: 0, count: -1 };\n            }\n        }\n    }\n    return group;\n}\n"
  },
  {
    "path": "src/engine/renderable/fx/speRuntime.ts",
    "content": "import '@/setupThreeGlobal';\nimport SPE from 'shader-particle-engine';\n\nexport default SPE;\n"
  },
  {
    "path": "src/engine/resourceConfigs.ts",
    "content": "import { TheaterType } from \"./TheaterType\";\nexport enum ResourceType {\n    IsoSnow = 0,\n    IsoTemp = 1,\n    IsoUrb = 2,\n    BuildGen = 3,\n    TheaterSnow = 4,\n    TheaterTemp = 5,\n    TheaterUrb = 6,\n    TheaterSnow2 = 7,\n    TheaterTemp2 = 8,\n    TheaterUrb2 = 9,\n    Ui = 10,\n    UiAlly = 11,\n    UiSov = 12,\n    Anims = 13,\n    Vxl = 14,\n    Cameo = 15,\n    Ini = 16,\n    Strings = 17,\n    EvaAlly = 18,\n    EvaSov = 19,\n    Sounds = 20,\n    HalloweenMix = 21,\n    XmasMix = 22\n}\nexport type ResourceId = string;\nexport interface ResourceConfig {\n    id: ResourceId;\n    src: string;\n    type: 'binary' | 'text' | 'json';\n    sizeHint?: number;\n}\nexport const resourceConfigs = new Map<ResourceType, ResourceConfig>()\n    .set(ResourceType.IsoSnow, {\n    id: \"isoSnow\",\n    src: \"isosnow.mix\",\n    type: \"binary\",\n    sizeHint: 28758698,\n})\n    .set(ResourceType.IsoTemp, {\n    id: \"isoTemp\",\n    src: \"isotemp.mix\",\n    type: \"binary\",\n    sizeHint: 29171410,\n})\n    .set(ResourceType.IsoUrb, {\n    id: \"isoUrb\",\n    src: \"isourb.mix\",\n    type: \"binary\",\n    sizeHint: 31811402,\n})\n    .set(ResourceType.BuildGen, {\n    id: \"buildGen\",\n    src: \"build-gen.mix\",\n    type: \"binary\",\n    sizeHint: 27801690,\n})\n    .set(ResourceType.TheaterSnow, {\n    id: \"theater.snow\",\n    src: \"snow.mix\",\n    type: \"binary\",\n    sizeHint: 18421274,\n})\n    .set(ResourceType.TheaterTemp, {\n    id: \"theater.temp\",\n    src: \"temperat.mix\",\n    type: \"binary\",\n    sizeHint: 2728266,\n})\n    .set(ResourceType.TheaterUrb, {\n    id: \"theater.urb\",\n    src: \"urban.mix\",\n    type: \"binary\",\n    sizeHint: 2726218,\n})\n    .set(ResourceType.TheaterSnow2, {\n    id: \"theater.snow2\",\n    src: \"sno.mix\",\n    type: \"binary\",\n    sizeHint: 10898,\n})\n    .set(ResourceType.TheaterTemp2, {\n    id: \"theater.temp2\",\n    src: \"tem.mix\",\n    type: \"binary\",\n    sizeHint: 10850,\n})\n    .set(ResourceType.TheaterUrb2, {\n    id: \"theater.urb2\",\n    src: \"urb.mix\",\n    type: \"binary\",\n    sizeHint: 10850,\n})\n    .set(ResourceType.UiAlly, {\n    id: \"uially\",\n    src: \"sidec01.mix\",\n    type: \"binary\",\n    sizeHint: 2099412,\n})\n    .set(ResourceType.UiSov, {\n    id: \"uisov\",\n    src: \"sidec02.mix\",\n    type: \"binary\",\n    sizeHint: 2102564,\n})\n    .set(ResourceType.Anims, {\n    id: \"anims\",\n    src: \"anims.mix\",\n    type: \"binary\",\n    sizeHint: 15867898,\n})\n    .set(ResourceType.Vxl, {\n    id: \"vxl\",\n    src: \"vxl.mix\",\n    type: \"binary\",\n    sizeHint: 5271701,\n})\n    .set(ResourceType.Cameo, {\n    id: \"cameo\",\n    src: \"cameo.mix\",\n    type: \"binary\",\n    sizeHint: 608120,\n})\n    .set(ResourceType.Ini, {\n    id: \"ini\",\n    src: \"ini.mix\",\n    type: \"binary\",\n    sizeHint: 1000842,\n})\n    .set(ResourceType.Ui, {\n    id: \"ui\",\n    src: \"ui.mix\",\n    type: \"binary\",\n    sizeHint: 4424093,\n})\n    .set(ResourceType.Strings, {\n    id: \"strings\",\n    src: \"strings.mix\",\n    type: \"binary\",\n    sizeHint: 485818,\n})\n    .set(ResourceType.EvaAlly, {\n    id: \"evaally\",\n    src: \"eva-ally.mix\",\n    type: \"binary\",\n    sizeHint: 1835436,\n})\n    .set(ResourceType.EvaSov, {\n    id: \"evasov\",\n    src: \"eva-sov.mix\",\n    type: \"binary\",\n    sizeHint: 2021760,\n})\n    .set(ResourceType.Sounds, {\n    id: \"sounds\",\n    src: \"sounds.mix\",\n    type: \"binary\",\n    sizeHint: 17684750,\n})\n    .set(ResourceType.HalloweenMix, {\n    id: \"halloweenmix\",\n    src: \"expandspawn09.mix\",\n    type: \"binary\",\n    sizeHint: 20312,\n})\n    .set(ResourceType.XmasMix, {\n    id: \"xmasmix\",\n    src: \"expandspawn10.mix\",\n    type: \"binary\",\n    sizeHint: 10318,\n});\nexport const resourcesForPrefetch: ResourceType[] = [\n    ResourceType.BuildGen,\n    ResourceType.Sounds,\n    ResourceType.Anims,\n    ResourceType.Vxl,\n    ResourceType.IsoUrb,\n    ResourceType.TheaterUrb,\n    ResourceType.TheaterUrb2,\n    ResourceType.IsoTemp,\n    ResourceType.TheaterTemp,\n    ResourceType.TheaterTemp2,\n    ResourceType.IsoSnow,\n    ResourceType.TheaterSnow,\n    ResourceType.TheaterSnow2,\n];\nexport const theaterSpecificResources = new Map<TheaterType, ResourceType[]>()\n    .set(TheaterType.Snow, [\n    ResourceType.TheaterSnow,\n    ResourceType.TheaterSnow2,\n    ResourceType.IsoSnow,\n])\n    .set(TheaterType.Temperate, [\n    ResourceType.TheaterTemp,\n    ResourceType.TheaterTemp2,\n    ResourceType.IsoTemp,\n])\n    .set(TheaterType.Urban, [\n    ResourceType.TheaterUrb,\n    ResourceType.TheaterUrb2,\n    ResourceType.IsoUrb,\n]);\n"
  },
  {
    "path": "src/engine/sound/AudioLoop.ts",
    "content": "import { getRandomInt } from \"../../util/math\";\ninterface AudioItem {\n    startTime: number;\n    duration: number;\n    handle: {\n        stop(): void;\n        setVolume(volume: number): void;\n        setPan(pan: number): void;\n    };\n}\ninterface PlayBufferResult {\n    handle: {\n        stop(): void;\n        setVolume(volume: number): void;\n        setPan(pan: number): void;\n    };\n    source: AudioBufferSourceNode;\n}\ninterface DelayRange {\n    min: number;\n    max: number;\n}\nexport class AudioLoop {\n    private audioContext: AudioContext;\n    private volume: number;\n    private pan: number;\n    private rate: number;\n    private delayMs?: DelayRange;\n    private attack: boolean;\n    private decay: boolean;\n    private playBuffer: (buffer: AudioBuffer, startTime: number, volume: number, pan: number, rate: number) => PlayBufferResult;\n    private isLoop: boolean = true;\n    private items: AudioItem[] = [];\n    private playing: boolean = false;\n    private remainingLoops: number;\n    private buffers?: AudioBuffer[];\n    private timePointer!: number;\n    private bufferPointer?: number;\n    constructor(audioContext: AudioContext, volume: number, pan: number, rate: number, delayMs: DelayRange | undefined, attack: boolean, decay: boolean, loops: number, playBuffer: (buffer: AudioBuffer, startTime: number, volume: number, pan: number, rate: number) => PlayBufferResult) {\n        this.audioContext = audioContext;\n        this.volume = volume;\n        this.pan = pan;\n        this.rate = rate;\n        this.delayMs = delayMs;\n        this.attack = attack;\n        this.decay = decay;\n        this.playBuffer = playBuffer;\n        this.remainingLoops = loops;\n    }\n    private handleSoundEnded = (): void => {\n        if (this.playing) {\n            this.removeCompleted();\n            this.fill(this.buffers!);\n            if (!this.remainingLoops && !this.items.length) {\n                this.stop();\n            }\n        }\n    };\n    setBuffers(buffers: AudioBuffer[]): void {\n        this.buffers = buffers;\n        if (this.playing) {\n            this.timePointer = Math.max(this.timePointer, this.audioContext.currentTime);\n            this.fill(this.buffers);\n        }\n    }\n    start(startTime: number): void {\n        if (this.playing) {\n            throw new Error(\"Already playing\");\n        }\n        this.timePointer = startTime;\n        this.playing = true;\n        if (this.buffers) {\n            this.fill(this.buffers);\n        }\n    }\n    isPlaying(): boolean {\n        return this.playing;\n    }\n    stop(): void {\n        if (this.playing) {\n            this.playing = false;\n            if (this.decay && this.buffers) {\n                this.removeCompleted();\n                if (this.items.length) {\n                    const nextStartTime = this.items[0].startTime + this.items[0].duration;\n                    this.items.splice(1).forEach((item) => item.handle.stop());\n                    this.queueBuffer(this.buffers[this.buffers.length - 1], nextStartTime);\n                }\n            }\n            else {\n                this.items.forEach((item) => item.handle.stop());\n                this.items.length = 0;\n            }\n        }\n    }\n    setVolume(volume: number): void {\n        this.volume = volume;\n        this.items.forEach((item) => item.handle.setVolume(volume));\n    }\n    setPan(pan: number): void {\n        this.pan = pan;\n        this.items.forEach((item) => item.handle.setPan(pan));\n    }\n    private add(item: AudioItem): void {\n        this.items.push(item);\n    }\n    private removeCompleted(): void {\n        this.items = this.items.filter((item) => item.startTime + item.duration >= this.audioContext.currentTime);\n    }\n    private fill(buffers: AudioBuffer[]): void {\n        let timeAhead = this.items.length\n            ? this.timePointer - this.items[0].startTime\n            : 0;\n        while (timeAhead < 0.1 || this.items.length < 3) {\n            if (!this.attack || this.bufferPointer !== undefined) {\n                if (this.remainingLoops <= 0)\n                    break;\n                this.remainingLoops--;\n            }\n            if (this.attack) {\n                this.bufferPointer =\n                    this.bufferPointer === undefined\n                        ? 0\n                        : getRandomInt(1, buffers.length - 1 - (this.decay ? 1 : 0));\n            }\n            else {\n                this.bufferPointer = getRandomInt(0, buffers.length - 1);\n            }\n            const buffer = buffers[this.bufferPointer];\n            const duration = this.queueBuffer(buffer, this.timePointer);\n            this.timePointer += duration;\n            timeAhead += duration;\n        }\n    }\n    private queueBuffer(buffer: AudioBuffer, startTime: number): number {\n        const delay = this.delayMs\n            ? getRandomInt(this.delayMs.min, this.delayMs.max) / 1000\n            : 0;\n        const actualStartTime = startTime + delay;\n        const duration = buffer.duration / this.rate;\n        const { handle, source } = this.playBuffer(buffer, actualStartTime, this.volume, this.pan, this.rate);\n        source.addEventListener(\"ended\", this.handleSoundEnded);\n        this.add({ startTime: actualStartTime, duration, handle });\n        return duration + delay;\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/AudioSequence.ts",
    "content": "interface AudioItem {\n    startTime: number;\n    duration: number;\n    handle: {\n        stop(): void;\n        setVolume(volume: number): void;\n        setPan(pan: number): void;\n    };\n}\ninterface PlayBufferResult {\n    handle: {\n        stop(): void;\n        setVolume(volume: number): void;\n        setPan(pan: number): void;\n    };\n    source: AudioBufferSourceNode;\n}\nexport class AudioSequence {\n    private audioContext: AudioContext;\n    private volume: number;\n    private pan: number;\n    private rate: number;\n    private delayMs: number;\n    private playBuffer: (buffer: AudioBuffer, startTime: number, volume: number, pan: number, rate: number) => PlayBufferResult;\n    private isLoop: boolean = false;\n    private items: AudioItem[] = [];\n    private playing: boolean = false;\n    private buffers?: AudioBuffer[];\n    private timePointer!: number;\n    constructor(audioContext: AudioContext, volume: number, pan: number, rate: number, delayMs: number, playBuffer: (buffer: AudioBuffer, startTime: number, volume: number, pan: number, rate: number) => PlayBufferResult) {\n        this.audioContext = audioContext;\n        this.volume = volume;\n        this.pan = pan;\n        this.rate = rate;\n        this.delayMs = delayMs;\n        this.playBuffer = playBuffer;\n    }\n    private handleSoundEnded = (): void => {\n        if (this.playing) {\n            this.removeCompleted();\n            if (!this.items.length) {\n                this.playing = false;\n            }\n        }\n    };\n    setBuffers(buffers: AudioBuffer[]): void {\n        this.buffers = buffers;\n        if (this.playing) {\n            this.timePointer = Math.max(this.timePointer, this.audioContext.currentTime);\n            this.fill(this.buffers);\n        }\n    }\n    start(startTime: number): void {\n        if (this.playing) {\n            throw new Error(\"Already playing\");\n        }\n        this.timePointer = startTime;\n        this.playing = true;\n        if (this.buffers) {\n            this.fill(this.buffers);\n        }\n    }\n    isPlaying(): boolean {\n        return this.playing;\n    }\n    stop(): void {\n        if (this.playing) {\n            this.playing = false;\n            this.items.forEach((item) => item.handle.stop());\n            this.items.length = 0;\n        }\n    }\n    setVolume(volume: number): void {\n        this.volume = volume;\n        this.items.forEach((item) => item.handle.setVolume(volume));\n    }\n    setPan(pan: number): void {\n        this.pan = pan;\n        this.items.forEach((item) => item.handle.setPan(pan));\n    }\n    private add(item: AudioItem): void {\n        this.items.push(item);\n    }\n    private removeCompleted(): void {\n        this.items = this.items.filter((item) => item.startTime + item.duration >= this.audioContext.currentTime);\n    }\n    private fill(buffers: AudioBuffer[]): void {\n        let delay = this.delayMs ? this.delayMs / 1000 : 0;\n        for (const buffer of buffers) {\n            const duration = this.queueBuffer(buffer, this.timePointer, delay);\n            this.timePointer += duration;\n            delay = 0;\n        }\n    }\n    private queueBuffer(buffer: AudioBuffer, startTime: number, delay: number): number {\n        const actualStartTime = startTime + delay;\n        const duration = buffer.duration / this.rate;\n        const { handle, source } = this.playBuffer(buffer, actualStartTime, this.volume, this.pan, this.rate);\n        source.addEventListener(\"ended\", this.handleSoundEnded);\n        this.add({ startTime: actualStartTime, duration, handle });\n        return duration + delay;\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/AudioSystem.ts",
    "content": "import { ChannelType } from \"./ChannelType\";\nimport { CompositeDisposable } from \"../../util/disposable/CompositeDisposable\";\nimport { InternalPlaybackHandle } from \"./InternalPlaybackHandle\";\nimport { AudioLoop } from \"./AudioLoop\";\nimport { AudioSequence } from \"./AudioSequence\";\nconst SILENT_MP3 = \"data:audio/mpeg;base64,/+MYxAAAAANIAUAAAASEEB/jwOFM/0MM/90b/+RhST//w4NFwOjf///PZu////9lns5GFDv//l9GlUIEEIAAAgIg8Ir/JGq3/+MYxDsLIj5QMYcoAP0dv9HIjUcH//yYSg+CIbkGP//8w0bLVjUP///3Z0x5QCAv/yLjwtGKTEFNRTMuOTeqqqqqqqqqqqqq/+MYxEkNmdJkUYc4AKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\";\ninterface Mixer {\n    onVolumeChange: {\n        subscribe(handler: (channel: ChannelType, mixer: Mixer) => void): void;\n        unsubscribe(handler: (channel: ChannelType, mixer: Mixer) => void): void;\n    };\n    getVolume(channel: ChannelType): number;\n    isMuted(channel: ChannelType): boolean;\n    setMuted(channel: ChannelType, muted: boolean): void;\n}\ninterface AudioFile {\n    getData(): ArrayBuffer;\n    asFile(): File;\n}\ninterface MusicState {\n    source: MediaElementAudioSourceNode;\n    playing: boolean;\n    onEnd?: () => void;\n}\nexport class AudioSystem {\n    private mixer: Mixer;\n    private audioContext?: AudioContext;\n    private channels = new Map<ChannelType, GainNode>();\n    private audioBufferCache = new Map<AudioFile, AudioBuffer>();\n    private disposables = new CompositeDisposable();\n    private soundsPlaying = new Set<AudioBufferSourceNode>();\n    private musicState?: MusicState;\n    constructor(mixer: Mixer) {\n        this.mixer = mixer;\n    }\n    private handleVolumeChange = (channel: ChannelType, mixer: Mixer): void => {\n        this.getChannel(channel).gain.value = mixer.isMuted(channel)\n            ? 0\n            : mixer.getVolume(channel);\n    };\n    isInitialized(): boolean {\n        return !!this.audioContext;\n    }\n    isSuspended(): boolean {\n        return this.audioContext?.state !== \"running\";\n    }\n    initialize(): void {\n        if (this.isInitialized())\n            return;\n        this.audioContext = new AudioContext();\n        this.mixer.onVolumeChange.subscribe(this.handleVolumeChange);\n        this.disposables.add(() => this.mixer.onVolumeChange.unsubscribe(this.handleVolumeChange));\n        this.createChannels(this.audioContext, this.mixer);\n    }\n    dispose(): void {\n        this.disposables.dispose();\n        if (this.audioContext) {\n            const ctx = this.audioContext;\n            if (ctx.state !== 'closed') {\n                try {\n                    void ctx.close().catch(() => { });\n                }\n                catch {\n                }\n            }\n            this.soundsPlaying.clear();\n            this.audioContext = undefined;\n        }\n    }\n    private createChannels(audioContext: AudioContext, mixer: Mixer): void {\n        const channelTypes = Object.keys(ChannelType)\n            .map(Number)\n            .filter((num) => !Number.isNaN(num));\n        channelTypes.forEach((channelType) => {\n            const gainNode = audioContext.createGain();\n            gainNode.gain.value = mixer.getVolume(channelType);\n            this.channels.set(channelType, gainNode);\n        });\n        const masterChannel = this.getChannel(ChannelType.Master);\n        channelTypes.forEach((channelType) => {\n            const channel = this.getChannel(channelType);\n            if (channelType === ChannelType.Master) {\n                channel.connect(audioContext.destination);\n            }\n            else if (channelType === ChannelType.Effect) {\n                const compressor = audioContext.createDynamicsCompressor();\n                channel.connect(compressor).connect(masterChannel);\n            }\n            else {\n                channel.connect(masterChannel);\n            }\n        });\n    }\n    private getChannel(channelType: ChannelType): GainNode {\n        if (!this.channels.has(channelType)) {\n            throw new Error(`Sound channel \"${channelType}\" doesn't exist`);\n        }\n        return this.channels.get(channelType)!;\n    }\n    setMuted(muted: boolean): void {\n        this.mixer.setMuted(ChannelType.Master, muted);\n    }\n    playWavFile(file: AudioFile, channel: ChannelType, volume: number = 1, pan: number = 0, delayMs: number = 0, rate: number = 1, loop: boolean = false): InternalPlaybackHandle {\n        if (!this.isInitialized()) {\n            throw new Error(\"Can't play audio file because audio system is not initialized\");\n        }\n        const startTime = this.audioContext!.currentTime + delayMs / 1000;\n        this.removeSuspendedSounds();\n        return this.playWavFileAtTime(file, channel, startTime, volume, pan, rate, loop);\n    }\n    private removeSuspendedSounds(): void {\n        if (this.isSuspended()) {\n            this.soundsPlaying.forEach((source) => {\n                try {\n                    source.stop();\n                }\n                catch (error) {\n                    console.error(error);\n                }\n            });\n        }\n    }\n    playWavLoop(files: AudioFile[], channel: ChannelType, volume: number = 1, pan: number = 0, delayMs?: {\n        min: number;\n        max: number;\n    }, rate: number = 1, attack: boolean = false, decay: boolean = false, loops: number = Number.POSITIVE_INFINITY): AudioLoop {\n        if (!this.isInitialized()) {\n            throw new Error(\"Can't play audio sequence because audio system is not initialized\");\n        }\n        const audioContext = this.audioContext!;\n        this.removeSuspendedSounds();\n        const audioLoop = new AudioLoop(audioContext, volume, pan, rate, delayMs, attack, decay, loops, (buffer, startTime, vol, p, r) => {\n            const handle = new InternalPlaybackHandle();\n            return {\n                handle,\n                source: this.playAudioBuffer(handle, buffer, channel, vol, p, startTime, r, false),\n            };\n        });\n        Promise.all(files.map((file) => this.decodeFile(file, audioContext)))\n            .then((buffers) => {\n            audioLoop.setBuffers(buffers);\n        })\n            .catch((error) => console.error(error));\n        audioLoop.start(audioContext.currentTime);\n        return audioLoop;\n    }\n    playWavSequence(files: AudioFile[], channel: ChannelType, volume: number = 1, pan: number = 0, delayMs: number = 0, rate: number = 1): AudioSequence {\n        if (!this.isInitialized()) {\n            throw new Error(\"Can't play audio sequence because audio system is not initialized\");\n        }\n        const audioContext = this.audioContext!;\n        this.removeSuspendedSounds();\n        const audioSequence = new AudioSequence(audioContext, volume, pan, rate, delayMs, (buffer, startTime, vol, p, r) => {\n            const handle = new InternalPlaybackHandle();\n            return {\n                handle,\n                source: this.playAudioBuffer(handle, buffer, channel, vol, p, startTime, r, false),\n            };\n        });\n        Promise.all(files.map((file) => this.decodeFile(file, audioContext)))\n            .then((buffers) => {\n            audioSequence.setBuffers(buffers);\n        })\n            .catch((error) => console.error(error));\n        audioSequence.start(audioContext.currentTime);\n        return audioSequence;\n    }\n    private async decodeFile(file: AudioFile, audioContext: AudioContext): Promise<AudioBuffer> {\n        let buffer = this.audioBufferCache.get(file);\n        if (!buffer) {\n            const arrayBuffer = new Uint8Array(file.getData()).buffer;\n            buffer = await audioContext.decodeAudioData(arrayBuffer);\n            if (this.audioBufferCache.size >= 100) {\n                this.audioBufferCache.delete(this.audioBufferCache.keys().next().value);\n            }\n            this.audioBufferCache.set(file, buffer);\n        }\n        return buffer;\n    }\n    private playWavFileAtTime(file: AudioFile, channel: ChannelType, startTime: number, volume: number = 1, pan: number = 0, rate: number = 1, loop: boolean = false): InternalPlaybackHandle {\n        if (!this.isInitialized()) {\n            throw new Error(\"Can't play audio file because audio system is not initialized\");\n        }\n        const audioContext = this.audioContext!;\n        const handle = new InternalPlaybackHandle();\n        const cachedBuffer = this.audioBufferCache.get(file);\n        if (cachedBuffer) {\n            this.playAudioBuffer(handle, cachedBuffer, channel, volume, pan, startTime, rate, loop);\n        }\n        else {\n            let arrayBuffer: ArrayBuffer;\n            try {\n                const data = file.getData();\n                arrayBuffer = new Uint8Array(data).buffer;\n            }\n            catch (error) {\n                console.error(\"Failed to decode wav file\", error);\n                return handle;\n            }\n            (async () => {\n                const buffer = await audioContext.decodeAudioData(arrayBuffer);\n                if (this.audioBufferCache.size >= 100) {\n                    this.audioBufferCache.delete(this.audioBufferCache.keys().next().value);\n                }\n                this.audioBufferCache.set(file, buffer);\n                if (!handle.stopRequested) {\n                    this.playAudioBuffer(handle, buffer, channel, volume, pan, startTime, rate, loop);\n                }\n            })().catch((error) => console.error(error));\n        }\n        return handle;\n    }\n    private playAudioBuffer(handle: InternalPlaybackHandle, buffer: AudioBuffer, channel: ChannelType, volume: number, pan: number, startTime: number, rate: number, loop: boolean): AudioBufferSourceNode {\n        const audioContext = this.audioContext!;\n        const gainNode = audioContext.createGain();\n        gainNode.gain.value = volume;\n        const panNode = audioContext.createStereoPanner();\n        panNode.pan.value = pan;\n        const sourceNode = audioContext.createBufferSource();\n        sourceNode.buffer = buffer;\n        sourceNode.playbackRate.value = rate;\n        sourceNode.loop = loop;\n        sourceNode.connect(panNode).connect(gainNode).connect(this.getChannel(channel));\n        handle.setNodes(sourceNode, gainNode, panNode);\n        sourceNode.addEventListener(\"ended\", () => {\n            this.soundsPlaying.delete(sourceNode);\n            (handle as any).playing = false;\n        });\n        this.soundsPlaying.add(sourceNode);\n        sourceNode.start(startTime);\n        return sourceNode;\n    }\n    async initMusicLoop(): Promise<void> {\n        if (!this.isInitialized()) {\n            throw new Error(\"Can't initialize music loop because audio system is not initialized\");\n        }\n        if (this.audioContext && this.audioContext.state === 'suspended') {\n            try {\n                await this.audioContext.resume();\n                console.log('[AudioSystem] AudioContext resumed successfully');\n            }\n            catch (error) {\n                console.error('[AudioSystem] Failed to resume AudioContext:', error);\n                throw error;\n            }\n        }\n        if (!this.musicState) {\n            this.initMusicNode();\n        }\n    }\n    async playMusicFile(file: AudioFile, repeat: boolean, onEnded?: () => void): Promise<void> {\n        if (!this.isInitialized()) {\n            throw new Error(\"Can't play audio file because audio system is not initialized\");\n        }\n        this.removeSuspendedSounds();\n        this.stopMusic();\n        const musicState = this.musicState ?? this.initMusicNode();\n        const audioElement = musicState.source.mediaElement;\n        audioElement.loop = repeat;\n        const objectUrl = URL.createObjectURL(file.asFile());\n        audioElement.src = objectUrl;\n        audioElement.onended = audioElement.onpause = () => {\n            URL.revokeObjectURL(objectUrl);\n        };\n        if (onEnded) {\n            musicState.onEnd = onEnded;\n            audioElement.addEventListener(\"ended\", musicState.onEnd, { once: true });\n        }\n        await this.playOrResumeMusic();\n    }\n    private initMusicNode(): MusicState {\n        const audioContext = this.audioContext!;\n        const gainNode = audioContext.createGain();\n        gainNode.gain.value = 1;\n        const panNode = audioContext.createStereoPanner();\n        panNode.pan.value = 0;\n        const audioElement = document.createElement(\"audio\");\n        audioElement.src = SILENT_MP3;\n        audioElement.loop = true;\n        const sourceNode = audioContext.createMediaElementSource(audioElement);\n        this.musicState = { source: sourceNode, playing: false };\n        sourceNode.addEventListener(\"ended\", () => {\n            this.musicState!.playing = false;\n        });\n        sourceNode\n            .connect(panNode)\n            .connect(gainNode)\n            .connect(this.getChannel(ChannelType.Music));\n        return this.musicState;\n    }\n    private async playOrResumeMusic(): Promise<void> {\n        if (this.musicState && !this.musicState.playing) {\n            this.musicState.playing = true;\n            try {\n                await this.musicState.source.mediaElement.play()?.catch((error) => console.error(error));\n            }\n            catch (error) {\n                console.error(error);\n                this.musicState.playing = false;\n            }\n        }\n    }\n    stopMusic(): void {\n        if (this.musicState?.playing) {\n            try {\n                if (this.musicState.onEnd) {\n                    this.musicState.source.mediaElement.removeEventListener(\"ended\", this.musicState.onEnd);\n                }\n                this.musicState.source.mediaElement.pause();\n                this.musicState.source.mediaElement.src = SILENT_MP3;\n                this.musicState.playing = false;\n                this.musicState.onEnd = undefined;\n            }\n            catch (error) {\n                console.error(error);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/ChannelType.ts",
    "content": "export enum ChannelType {\n    Master = 0,\n    Ui = 1,\n    Ambient = 2,\n    Effect = 3,\n    Voice = 4,\n    Music = 5,\n    CreditTicks = 6\n}\n"
  },
  {
    "path": "src/engine/sound/Eva.ts",
    "content": "import { ChannelType } from \"./ChannelType\";\ninterface EvaSpec {\n    sound: string;\n    priority: number;\n    queue?: boolean;\n}\ninterface EvaSpecs {\n    getSpec(name: string): EvaSpec | undefined;\n}\ninterface Sound {\n    getWavFile(name: string): any;\n    audioSystem: {\n        playWavFile(file: any, channel: ChannelType): any;\n    };\n}\ninterface Renderer {\n    onFrame: {\n        subscribe(handler: (time: number) => void): void;\n        unsubscribe(handler: (time: number) => void): void;\n    };\n}\nexport class Eva {\n    private evaSpecs: EvaSpecs;\n    private sound: Sound;\n    private renderer: Renderer;\n    private evaWaitingList: EvaSpec[] = [];\n    private lastEvaEventBySpec = new Map<EvaSpec, number>();\n    private currentEvaPlaying?: any;\n    constructor(evaSpecs: EvaSpecs, sound: Sound, renderer: Renderer) {\n        this.evaSpecs = evaSpecs;\n        this.sound = sound;\n        this.renderer = renderer;\n    }\n    private handleFrame = (time: number): void => {\n        if (this.currentEvaPlaying?.isPlaying()) {\n            this.evaWaitingList = this.evaWaitingList.filter((eva) => eva.queue);\n        }\n        else {\n            this.currentEvaPlaying = undefined;\n            this.evaWaitingList.sort((a, b) => b.priority - a.priority);\n            this.evaWaitingList = this.evaWaitingList.filter((eva) => time - (this.lastEvaEventBySpec.get(eva) || 0) >= 5000);\n            if (this.evaWaitingList.length) {\n                const nextEva = this.evaWaitingList.shift()!;\n                const wavFile = this.sound.getWavFile(nextEva.sound);\n                if (wavFile) {\n                    this.currentEvaPlaying = this.sound.audioSystem.playWavFile(wavFile, ChannelType.Voice);\n                    this.lastEvaEventBySpec.set(nextEva, time);\n                    this.evaWaitingList.splice(1);\n                }\n            }\n        }\n    };\n    init(): void {\n        this.renderer.onFrame.subscribe(this.handleFrame);\n    }\n    dispose(): void {\n        this.renderer.onFrame.unsubscribe(this.handleFrame);\n        this.currentEvaPlaying?.stop();\n    }\n    play(name: string, queue: boolean = false): void {\n        let spec = this.evaSpecs.getSpec(name);\n        if (spec) {\n            if (queue) {\n                spec = { ...spec, queue: true };\n            }\n            this.evaWaitingList.push(spec);\n        }\n        else {\n            console.warn(`No EVA with name ${name} was found. Skipping.`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/EvaSpecs.ts",
    "content": "import { SideType } from \"../../game/SideType\";\nexport enum EvaPriority {\n    Low = 0,\n    Normal = 1,\n    Important = 2,\n    Critical = 3\n}\ninterface EvaSpec {\n    text: string;\n    sound: string;\n    priority: EvaPriority;\n    queue: boolean;\n}\nexport class EvaSpecs {\n    private sideType: SideType;\n    private specs = new Map<string, EvaSpec>();\n    constructor(sideType: SideType) {\n        this.sideType = sideType;\n    }\n    readIni(ini: any): EvaSpecs {\n        let dialogListSection = ini.getSection(\"DialogList\");\n        if (!dialogListSection) {\n            throw new Error(\"Missing eva.ini [DialogList] section\");\n        }\n        const dialogNames = new Set(dialogListSection.entries.values());\n        const sidePrefix = this.sideType === SideType.GDI ? \"Allied\" : \"Russian\";\n        for (let dialogName of dialogNames) {\n            if (dialogName) {\n                let dialogSection = ini.getSection(dialogName);\n                if (dialogSection) {\n                    const spec: EvaSpec = {\n                        text: dialogSection.getString(\"Text\"),\n                        sound: dialogSection.getString(sidePrefix),\n                        priority: dialogSection.getEnum(\"Priority\", EvaPriority, EvaPriority.Normal, true),\n                        queue: dialogSection.getString(\"Type\").trim().toLowerCase() === \"queue\",\n                    };\n                    this.specs.set(dialogName as string, spec);\n                }\n                else {\n                    console.warn(`Missing eva section [${dialogName}]`);\n                }\n            }\n        }\n        return this;\n    }\n    getSpec(name: string): EvaSpec | undefined {\n        return this.specs.get(name);\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/InternalPlaybackHandle.ts",
    "content": "export class InternalPlaybackHandle {\n    private playing: boolean = true;\n    private isLoop: boolean = false;\n    public stopRequested: boolean = false;\n    private sourceNode?: AudioBufferSourceNode;\n    private gainNode?: GainNode;\n    private panNode?: StereoPannerNode;\n    private volumeRequested?: number;\n    private panRequested?: number;\n    setNodes(sourceNode: AudioBufferSourceNode, gainNode: GainNode, panNode: StereoPannerNode): void {\n        this.sourceNode = sourceNode;\n        this.gainNode = gainNode;\n        this.panNode = panNode;\n        if (this.stopRequested) {\n            this.stop();\n        }\n        else {\n            if (this.volumeRequested !== undefined) {\n                gainNode.gain.value = this.volumeRequested;\n            }\n            if (this.panRequested !== undefined) {\n                panNode.pan.value = this.panRequested;\n            }\n        }\n    }\n    isPlaying(): boolean {\n        return this.playing;\n    }\n    stop(): void {\n        try {\n            if (this.sourceNode) {\n                this.sourceNode.stop();\n            }\n            else {\n                this.stopRequested = true;\n            }\n            this.playing = false;\n        }\n        catch (error) {\n            console.error(error);\n        }\n    }\n    setVolume(volume: number): void {\n        if (this.gainNode) {\n            this.gainNode.gain.value = volume;\n        }\n        else {\n            this.volumeRequested = volume;\n        }\n    }\n    setPan(pan: number): void {\n        if (this.panNode) {\n            this.panNode.pan.value = pan;\n        }\n        else {\n            this.panRequested = pan;\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/Mixer.ts",
    "content": "import { EventDispatcher } from \"../../util/event\";\nexport class Mixer {\n    private volumes: Map<number, number> = new Map();\n    private mutes: Map<number, boolean> = new Map();\n    private _onVolumeChange = new EventDispatcher<[\n        Mixer,\n        number\n    ]>();\n    get onVolumeChange() {\n        return this._onVolumeChange.asEvent();\n    }\n    setVolume(channel: number, volume: number): void {\n        if (this.getVolume(channel) !== volume) {\n            this.volumes.set(channel, volume);\n            this._onVolumeChange.dispatch(this as any, channel);\n        }\n    }\n    getVolume(channel: number): number {\n        return this.volumes.get(channel) ?? 1;\n    }\n    setMuted(channel: number, muted: boolean): void {\n        this.mutes.set(channel, muted);\n        this._onVolumeChange.dispatch(this as any, channel);\n    }\n    isMuted(channel: number): boolean {\n        return !!this.mutes.get(channel);\n    }\n    serialize(): string {\n        return [...this.volumes.entries()]\n            .map(([channel, volume]) => channel + \",\" + volume)\n            .join(\";\");\n    }\n    unserialize(data: string): Mixer {\n        this.volumes = new Map(data.split(\";\").map((entry) => {\n            const [channel, volume] = entry.split(\",\").map(Number);\n            return [channel, volume];\n        }));\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/Music.ts",
    "content": "import { getRandomInt } from \"../../util/math\";\nexport enum MusicType {\n    Normal = 0,\n    NormalShuffle = 1,\n    Intro = \"INTRO\",\n    Score = \"SCORE\",\n    Loading = \"LOADING\",\n    Credits = \"CREDITS\",\n    Options = \"RA2Options\"\n}\ninterface MusicSpec {\n    name: string;\n    sound: string;\n    repeat: boolean;\n    normal: boolean;\n}\ninterface MusicSpecs {\n    getSpec(name: string): MusicSpec | undefined;\n    getAll(): MusicSpec[];\n}\ninterface AudioSystem {\n    playMusicFile(file: any, repeat: boolean, onEnded?: () => void): Promise<boolean>;\n    stopMusic(): void;\n}\ninterface AudioFiles {\n    get(filename: string): Promise<any>;\n}\nexport class Music {\n    private audioSystem: AudioSystem;\n    private audioFiles: AudioFiles;\n    private musicSpecs: MusicSpecs;\n    private playlist: MusicSpec[] = [];\n    private currentPlaylistIdx: number = -1;\n    private shuffle: boolean = false;\n    private repeat: boolean = false;\n    private currentMusicType?: MusicType;\n    private initialRepeatName?: string;\n    constructor(audioSystem: AudioSystem, audioFiles: AudioFiles, musicSpecs: MusicSpecs) {\n        this.audioSystem = audioSystem;\n        this.audioFiles = audioFiles;\n        this.musicSpecs = musicSpecs;\n    }\n    unserializeOptions(data: string): void {\n        const [shuffleStr, repeatStr, repeatName] = data.split(\",\");\n        this.shuffle = Boolean(Number(shuffleStr));\n        this.repeat = Boolean(Number(repeatStr));\n        this.initialRepeatName = repeatName;\n    }\n    serializeOptions(): string {\n        return [\n            Number(this.shuffle),\n            Number(this.repeat),\n            this.repeat && this.currentPlaylistIdx !== -1\n                ? this.playlist[this.currentPlaylistIdx].name\n                : undefined,\n        ].join(\",\");\n    }\n    getShuffleMode(): boolean {\n        return this.shuffle;\n    }\n    getRepeatMode(): boolean {\n        return this.repeat;\n    }\n    getPlaylist(): MusicSpec[] {\n        return this.buildPlaylist(false);\n    }\n    getCurrentPlaylistItem(): MusicSpec | undefined {\n        if (this.currentPlaylistIdx !== -1) {\n            return this.playlist[this.currentPlaylistIdx];\n        }\n    }\n    dispose(): void {\n        this.stopPlaying();\n    }\n    private getMusicSpec(name: string): MusicSpec | undefined {\n        console.log(`[Music] Looking for music spec: \"${name}\"`);\n        const spec = this.musicSpecs.getSpec(name);\n        if (spec) {\n            console.log(`[Music] Found music spec for \"${name}\":`, spec);\n            return spec;\n        }\n        console.warn(`[Music] Music \"${name}\" is not defined`);\n        const allSpecs = this.musicSpecs.getAll();\n        console.log(`[Music] Available music specs:`, allSpecs.map(s => ({ name: s.name, sound: s.sound })));\n    }\n    async play(type: MusicType): Promise<void> {\n        if (this.currentMusicType === type)\n            return;\n        if (type === MusicType.Normal || type === MusicType.NormalShuffle) {\n            const shouldShuffle = this.shuffle || type === MusicType.NormalShuffle;\n            this.playlist = this.buildPlaylist(shouldShuffle);\n            this.currentPlaylistIdx = 0;\n            if (this.initialRepeatName) {\n                const index = this.playlist.findIndex((spec) => spec.name === this.initialRepeatName);\n                if (index !== -1) {\n                    this.currentPlaylistIdx = index;\n                }\n            }\n            const success = await this.playSpec(this.playlist[this.currentPlaylistIdx], () => this.advancePlaylist());\n            if (success) {\n                this.currentMusicType = type;\n            }\n        }\n        else {\n            const spec = this.getMusicSpec(type as string);\n            if (spec) {\n                const success = await this.playSpec(spec);\n                if (success) {\n                    this.currentMusicType = type;\n                }\n            }\n            else {\n                console.warn(`No music spec found for type \"${type}\"`);\n            }\n        }\n    }\n    stopPlaying(): void {\n        this.audioSystem.stopMusic();\n        this.currentMusicType = undefined;\n    }\n    setShuffleMode(shuffle: boolean): void {\n        if (shuffle !== this.shuffle) {\n            this.shuffle = shuffle;\n            const currentItem = this.currentPlaylistIdx !== -1\n                ? this.playlist[this.currentPlaylistIdx]\n                : undefined;\n            this.playlist = this.buildPlaylist(this.shuffle);\n            this.currentPlaylistIdx = currentItem\n                ? this.playlist.findIndex((spec) => spec === currentItem)\n                : -1;\n        }\n    }\n    setRepeatMode(repeat: boolean): void {\n        this.repeat = repeat;\n    }\n    private async playSpec(spec: MusicSpec, onEnded?: () => void): Promise<boolean> {\n        const file = await this.getMp3File(spec.sound);\n        if (!file)\n            return false;\n        await this.audioSystem.playMusicFile(file, spec.repeat, onEnded);\n        return true;\n    }\n    private async getMp3File(name: string): Promise<any> {\n        const filename = name.toLowerCase() + \".mp3\";\n        console.log(`[Music] Looking for audio file: \"${filename}\"`);\n        let file;\n        try {\n            file = await this.audioFiles.get(filename);\n            console.log(`[Music] audioFiles.get(\"${filename}\") result:`, !!file);\n        }\n        catch (error) {\n            console.error(`[Music] Failed to fetch audio file \"${filename}\":`, error);\n            return;\n        }\n        if (file) {\n            console.log(`[Music] Successfully got audio file: ${filename}`);\n            return file;\n        }\n        console.warn(`[Music] Audio file \"${filename}\" not found.`);\n        console.log(`[Music] Debugging themes directory access...`);\n        try {\n            console.log(`[Music] audioFiles type:`, typeof this.audioFiles);\n            console.log(`[Music] audioFiles constructor:`, this.audioFiles.constructor.name);\n        }\n        catch (error) {\n            console.error(`[Music] Error debugging audioFiles:`, error);\n        }\n    }\n    private buildPlaylist(shuffle: boolean): MusicSpec[] {\n        let playlist = this.musicSpecs.getAll().filter((spec) => spec.normal);\n        if (shuffle) {\n            playlist = this.shufflePlaylist(playlist);\n        }\n        return playlist;\n    }\n    private shufflePlaylist(playlist: MusicSpec[]): MusicSpec[] {\n        const shuffled: MusicSpec[] = [];\n        const remaining = [...playlist];\n        while (remaining.length) {\n            shuffled.push(...remaining.splice(getRandomInt(0, remaining.length - 1), 1));\n        }\n        return shuffled;\n    }\n    private async advancePlaylist(): Promise<void> {\n        this.currentPlaylistIdx = this.repeat\n            ? this.currentPlaylistIdx\n            : (this.currentPlaylistIdx + 1) % this.playlist.length;\n        await this.playSpec(this.playlist[this.currentPlaylistIdx], () => this.advancePlaylist());\n    }\n    async selectPlaylistItem(item: MusicSpec): Promise<void> {\n        const index = this.playlist?.findIndex((spec) => spec === item);\n        if (index !== -1) {\n            this.currentPlaylistIdx = index;\n            this.stopPlaying();\n            await this.playSpec(this.playlist[this.currentPlaylistIdx], () => this.advancePlaylist());\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/MusicSpecs.ts",
    "content": "interface MusicSpec {\n    name: string;\n    sound: string;\n    normal: boolean;\n    repeat: boolean;\n}\nexport class MusicSpecs {\n    private ini: any;\n    private specs = new Map<string, MusicSpec>();\n    constructor(ini: any) {\n        this.ini = ini;\n        this.parse();\n    }\n    private parse(): void {\n        let themesSection = this.ini.getSection(\"Themes\");\n        if (themesSection) {\n            for (const themeName of themesSection.entries.values()) {\n                if (themeName) {\n                    let themeSection = this.ini.getSection(themeName);\n                    if (themeSection) {\n                        const spec: MusicSpec = {\n                            name: themeSection.getString(\"Name\"),\n                            sound: themeSection.getString(\"Sound\"),\n                            normal: themeSection.getBool(\"Normal\", true),\n                            repeat: themeSection.getBool(\"Repeat\"),\n                        };\n                        this.specs.set(themeName, spec);\n                    }\n                    else {\n                        console.warn(`Music section [${themeName}] not found. Skipping.`);\n                    }\n                }\n            }\n        }\n        else {\n            console.warn(\"[Themes] section missing. Music will not be played.\");\n        }\n    }\n    getSpec(name: string): MusicSpec | undefined {\n        return this.specs.get(name);\n    }\n    getAll(): MusicSpec[] {\n        return [...this.specs.values()];\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/Sound.ts",
    "content": "import { ChannelType } from \"./ChannelType\";\nimport { SoundKey } from \"./SoundKey\";\nimport { SoundSpecs, SoundControl } from \"./SoundSpecs\";\nimport { getRandomInt } from \"../../util/math\";\nimport { isNotNullOrUndefined } from \"../../util/typeGuard\";\ninterface AudioVisualRules {\n    ini: {\n        getString(key: string): string | undefined;\n    };\n}\ninterface AudioFiles {\n    get(filename: string): any;\n}\ninterface SoundSpec {\n    name: string;\n    control: Set<SoundControl>;\n    sounds: string[];\n    volume: number;\n    delay?: {\n        min: number;\n        max: number;\n    };\n    limit: number;\n    loop?: number;\n    fShift?: {\n        min: number;\n        max: number;\n    };\n    attack?: number;\n    decay?: number;\n}\ninterface AudioSystem {\n    initialize(): void;\n    dispose(): void;\n    playWavFile(file: any, channel: ChannelType, volume?: number, pan?: number, delay?: number, rate?: number, loop?: boolean): any;\n    playWavSequence(files: any[], channel: ChannelType, volume?: number, pan?: number, delay?: number, rate?: number): any;\n    playWavLoop(files: any[], channel: ChannelType, volume?: number, pan?: number, delayMs?: {\n        min: number;\n        max: number;\n    }, rate?: number, attack?: boolean, decay?: boolean, loops?: number): any;\n}\ninterface PlaybackHandle {\n    isPlaying(): boolean;\n    stop(): void;\n}\nexport class Sound {\n    private audioSystem: AudioSystem;\n    private audioFiles: AudioFiles;\n    private soundSpecs: SoundSpecs;\n    private audioVisualRules: AudioVisualRules;\n    private document: Document;\n    private playbackHandles = new Map<string, PlaybackHandle[]>();\n    constructor(audioSystem: AudioSystem, audioFiles: AudioFiles, soundSpecs: SoundSpecs, audioVisualRules: AudioVisualRules, document: Document) {\n        this.audioSystem = audioSystem;\n        this.audioFiles = audioFiles;\n        this.soundSpecs = soundSpecs;\n        this.audioVisualRules = audioVisualRules;\n        this.document = document;\n    }\n    private handleClick = (event: Event): void => {\n        const target = event.target as Element;\n        if (target.matches(\"button, .menu-button:not(.disabled)\")) {\n            this.play(SoundKey.GUIMainButtonSound, ChannelType.Ui);\n        }\n        else if (target.matches(\".list-item\")) {\n            this.play(SoundKey.GenericClick, ChannelType.Ui);\n        }\n        else if (target instanceof HTMLInputElement &&\n            [\"checkbox\", \"radio\", \"range\"].includes(target.type) &&\n            !target.disabled) {\n            this.play(SoundKey.GUICheckboxSound, ChannelType.Ui);\n        }\n        else if ((target instanceof HTMLSelectElement && !target.disabled) ||\n            target.matches(\".select:not(.disabled) *\")) {\n            this.play(SoundKey.GUIComboOpenSound, ChannelType.Ui);\n        }\n    };\n    initialize(): void {\n        this.audioSystem.initialize();\n        this.document.addEventListener(\"click\", this.handleClick);\n    }\n    dispose(): void {\n        this.audioSystem.dispose();\n        this.document.removeEventListener(\"click\", this.handleClick);\n    }\n    private getSoundKey(key: SoundKey | string): string | undefined {\n        let soundKey: string | undefined;\n        if (typeof SoundKey[key as keyof typeof SoundKey] === \"string\") {\n            soundKey = this.audioVisualRules.ini.getString(SoundKey[key as keyof typeof SoundKey] as any);\n            if (!soundKey)\n                return;\n        }\n        else {\n            soundKey = key as string;\n        }\n        return soundKey;\n    }\n    private getSoundSpec(key: SoundKey | string): SoundSpec | undefined {\n        const soundKey = this.getSoundKey(key);\n        if (soundKey) {\n            const spec = this.soundSpecs.getSpec(soundKey);\n            if (spec)\n                return spec;\n            console.warn(`Sound \"${soundKey}\" is not defined`);\n        }\n        else {\n            console.warn(`No sound is defined for key \"${SoundKey[key as keyof typeof SoundKey]}\"`);\n        }\n    }\n    play(key: SoundKey | string, channel: ChannelType): PlaybackHandle | undefined {\n        const spec = this.getSoundSpec(key);\n        if (spec) {\n            const loops = spec.control.has(SoundControl.Loop)\n                ? spec.loop || Number.POSITIVE_INFINITY\n                : 0;\n            return this.playWithOptions(spec, channel, spec.volume / 100, 0, spec.limit, loops);\n        }\n    }\n    private playWithOptions(spec: SoundSpec, channel: ChannelType, volume: number, pan: number, limit: number, loops: number): PlaybackHandle | undefined {\n        if (!spec.sounds.length)\n            return;\n        this.cleanOldHandles();\n        let handles = this.playbackHandles.get(spec.name);\n        if (!handles) {\n            handles = [];\n            this.playbackHandles.set(spec.name, handles);\n        }\n        if (limit && handles.length >= limit) {\n            if (!spec.control.has(SoundControl.Interrupt))\n                return;\n            handles.shift()!.stop();\n        }\n        const rate = 1 + (spec.fShift\n            ? getRandomInt(spec.fShift.min, spec.fShift.max) / 100\n            : 0);\n        let handle: PlaybackHandle | undefined;\n        const hasAttack = spec.control.has(SoundControl.Attack);\n        const hasDecay = spec.control.has(SoundControl.Decay);\n        if (loops && (spec.sounds.length > 1 || loops !== Number.POSITIVE_INFINITY || spec.delay)) {\n            const sequence = this.buildAttackDecaySequence(spec, hasAttack, hasDecay, true);\n            const wavFiles = sequence\n                .map((sound) => this.getWavFile(sound))\n                .filter(isNotNullOrUndefined);\n            handle = this.audioSystem.playWavLoop(wavFiles, channel, volume, pan, spec.delay, rate, hasAttack, hasDecay, loops);\n        }\n        else {\n            let delay = 0;\n            if (spec.delay) {\n                delay = getRandomInt(spec.delay.min, spec.delay.max);\n            }\n            if (hasAttack || hasDecay) {\n                const sequence = this.buildAttackDecaySequence(spec, hasAttack, hasDecay, false);\n                const wavFiles = sequence\n                    .map((sound) => this.getWavFile(sound))\n                    .filter(isNotNullOrUndefined);\n                handle = this.audioSystem.playWavSequence(wavFiles, channel, volume, pan, delay, rate);\n            }\n            else {\n                let soundName: string;\n                if (spec.control.has(SoundControl.Random)) {\n                    soundName = spec.sounds[getRandomInt(0, spec.sounds.length - 1)];\n                }\n                else {\n                    soundName = spec.sounds[0];\n                }\n                const wavFile = this.getWavFile(soundName);\n                if (!wavFile)\n                    return;\n                handle = this.audioSystem.playWavFile(wavFile, channel, volume, pan, delay, rate, loops !== 0);\n            }\n        }\n        if (handle) {\n            handles.push(handle);\n        }\n        return handle;\n    }\n    private buildAttackDecaySequence(spec: SoundSpec, hasAttack: boolean, hasDecay: boolean, isLoop: boolean): string[] {\n        const attackCount = hasAttack ? spec.attack || 1 : 0;\n        const decayCount = hasDecay ? spec.decay || 1 : 0;\n        const mainSounds = spec.sounds.slice(attackCount, spec.sounds.length - decayCount);\n        const sequence: string[] = [];\n        if (attackCount > 0) {\n            const attackSound = spec.sounds[getRandomInt(0, attackCount - 1)];\n            sequence.push(attackSound);\n        }\n        if (isLoop) {\n            sequence.push(...mainSounds);\n        }\n        else {\n            sequence.push(mainSounds[getRandomInt(0, mainSounds.length - 1)]);\n        }\n        if (decayCount > 0) {\n            const decaySound = spec.sounds[getRandomInt(spec.sounds.length - decayCount, spec.sounds.length - 1)];\n            sequence.push(decaySound);\n        }\n        return sequence;\n    }\n    private getWavFile(soundName: string): any {\n        const filename = soundName + \".wav\";\n        const file = this.audioFiles.get(filename);\n        if (file)\n            return file;\n        console.error(`Audio file \"${filename}\" not found.`);\n    }\n    private cleanOldHandles(): void {\n        for (const [name, handles] of this.playbackHandles) {\n            const activeHandles = handles.filter((handle) => handle.isPlaying());\n            this.playbackHandles.set(name, activeHandles);\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/SoundKey.ts",
    "content": "export enum SoundKey {\n    CreateInfantrySound = 0,\n    CreateUnitSound = 1,\n    CreateAircraftSound = 2,\n    SpySatActivationSound = 3,\n    SpySatDeactivationSound = 4,\n    UpgradeVeteranSound = 5,\n    UpgradeEliteSound = 6,\n    BaseUnderAttackSound = 7,\n    BuildingGarrisonedSound = 8,\n    BuildingRepairedSound = 9,\n    CheerSound = 10,\n    PlaceBeaconSound = 11,\n    StartPlanningModeSound = 12,\n    EndPlanningModeSound = 13,\n    AddPlanningModeCommandSound = 14,\n    ExecutePlanSound = 15,\n    CratePromoteSound = 16,\n    CrateMoneySound = 17,\n    CrateRevealSound = 18,\n    CrateFireSound = 19,\n    CrateArmourSound = 20,\n    CrateSpeedSound = 21,\n    CrateUnitSound = 22,\n    GUIMainButtonSound = 23,\n    GUIBuildSound = 24,\n    GUITabSound = 25,\n    GUIOpenSound = 26,\n    GUICloseSound = 27,\n    GUIMoveOutSound = 28,\n    GUIMoveInSound = 29,\n    GUIComboOpenSound = 30,\n    GUIComboCloseSound = 31,\n    GUICheckboxSound = 32,\n    ScoreAnimSound = 33,\n    SinkingSound = 34,\n    ImpactWaterSound = 35,\n    ImpactLandSound = 36,\n    BombTickingSound = 37,\n    ChronoInSound = 38,\n    ChronoOutSound = 39,\n    BombAttachSound = 40,\n    YuriMindControlSound = 41,\n    DigSound = 42,\n    CloakSound = 43,\n    SellSound = 44,\n    GameClosed = 45,\n    IncomingMessage = 46,\n    MessageCharTyped = 47,\n    SystemError = 48,\n    OptionsChanged = 49,\n    GameForming = 50,\n    PlayerLeft = 51,\n    PlayerJoined = 52,\n    Construction = 53,\n    BuildingDieSound = 54,\n    BuildingSlam = 55,\n    RadarOn = 56,\n    RadarOff = 57,\n    MovieOn = 58,\n    MovieOff = 59,\n    ScoldSound = 60,\n    TeslaCharge = 61,\n    TeslaZap = 62,\n    BuildingDamageSound = 63,\n    ChuteSound = 64,\n    GenericClick = 65,\n    GenericBeep = 66,\n    BuildingDrop = 67,\n    StopSound = 68,\n    GuardSound = 69,\n    ScatterSound = 70,\n    DeploySound = 71,\n    StormSound = 72,\n    LightningSounds = 73,\n    ShellButtonSlideSound = 74,\n    QuickMatchTimer = 75\n}\n"
  },
  {
    "path": "src/engine/sound/SoundSpec.ts",
    "content": "import { SoundControl, SoundPriority, SoundType } from \"./SoundSpecs\";\ninterface MinMaxPair {\n    min: number;\n    max: number;\n}\ninterface SoundDefaults {\n    minVolume: number;\n    range: number;\n    volume: number;\n    limit: number;\n    type: SoundType[];\n    priority: SoundPriority;\n}\nexport class SoundSpec {\n    name!: string;\n    control!: Set<SoundControl>;\n    sounds!: string[];\n    volume!: number;\n    delay?: MinMaxPair;\n    priority!: SoundPriority;\n    type!: SoundType[];\n    fShift?: MinMaxPair;\n    limit!: number;\n    loop?: number;\n    range!: number;\n    minVolume!: number;\n    vShift?: MinMaxPair;\n    attack?: number;\n    decay?: number;\n    read(section: any, defaults: SoundDefaults): SoundSpec {\n        let range = section.getNumber(\"Range\", defaults.range);\n        if (range === -2) {\n            range = Number.POSITIVE_INFINITY;\n        }\n        this.name = section.name;\n        this.control = new Set(section.getEnumArray(\"Control\", SoundControl, /\\s+/, [], true));\n        this.sounds = section\n            .getArray(\"Sounds\", /\\s+/)\n            .map((sound: string) => sound.replace(/^\\$/, \"\"));\n        this.volume = section.has(\"Volume\")\n            ? section.getNumber(\"Volume\", defaults.volume)\n            : section.getNumber(\"volume\", defaults.volume);\n        this.delay = this.createMinMaxPair(section.getNumberArray(\"Delay\", /\\s+/, []));\n        this.priority = section.getEnum(\"Priority\", SoundPriority, defaults.priority, true);\n        this.type = section.getEnumArray(\"Type\", SoundType, /\\s+/, defaults.type, true);\n        if (!this.type.some((type) => [SoundType.Screen, SoundType.Local, SoundType.Global].includes(type))) {\n            const fallbackType = defaults.type.find((type) => [SoundType.Screen, SoundType.Local, SoundType.Global].includes(type));\n            if (fallbackType) {\n                this.type.push(fallbackType);\n            }\n        }\n        this.fShift = this.createMinMaxPair(section.getNumberArray(\"FShift\", /\\s+/, []));\n        this.limit = section.getNumber(\"Limit\", defaults.limit);\n        this.loop = section.getNumber(\"Loop\");\n        this.range = range;\n        this.minVolume = section.getNumber(\"MinVolume\", defaults.minVolume);\n        this.vShift = this.createMinMaxPair(section.getNumberArray(section.has(\"Vshift\") ? \"Vshift\" : \"VShift\", /\\s+/, []));\n        this.attack = section.getNumber(\"Attack\");\n        this.decay = section.getNumber(\"Decay\");\n        return this;\n    }\n    private createMinMaxPair(values: number[]): MinMaxPair | undefined {\n        if (values.length) {\n            let [min, max] = values;\n            if (max === undefined) {\n                max = min;\n            }\n            return { min, max };\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/SoundSpecs.ts",
    "content": "import { SoundSpec } from \"./SoundSpec\";\nexport enum SoundType {\n    Global = 0,\n    Normal = 1,\n    Screen = 2,\n    Local = 3,\n    Player = 4,\n    Unshroud = 5,\n    Shroud = 6\n}\nexport enum SoundPriority {\n    Lowest = 0,\n    Low = 1,\n    Normal = 2,\n    High = 3,\n    Critical = 4\n}\nexport enum SoundControl {\n    All = 0,\n    Loop = 1,\n    Random = 2,\n    Predelay = 3,\n    Interrupt = 4,\n    Attack = 5,\n    Decay = 6,\n    Ambient = 7\n}\nexport class SoundSpecs {\n    private ini: any;\n    private specs: Map<string, SoundSpec>;\n    private defaults: any;\n    constructor(ini: any) {\n        this.ini = ini;\n        this.specs = new Map();\n        this.parse();\n    }\n    private parse(): void {\n        let defaultsSection = this.ini.getSection(\"Defaults\");\n        if (defaultsSection) {\n            this.defaults = {\n                minVolume: defaultsSection.getNumber(\"MinVolume\"),\n                range: defaultsSection.getNumber(\"Range\"),\n                volume: defaultsSection.getNumber(\"Volume\"),\n                limit: defaultsSection.getNumber(\"Limit\"),\n                type: defaultsSection.getEnumArray(\"Type\", SoundType, /\\s+/, [], true),\n                priority: defaultsSection.getEnum(\"Priority\", SoundPriority, SoundPriority.Normal, true),\n            };\n            let soundListSection = this.ini.getSection(\"SoundList\");\n            if (soundListSection) {\n                for (let soundName of new Set(soundListSection.entries.values())) {\n                    if (soundName) {\n                        let soundSection = this.ini.getSection(soundName);\n                        if (soundSection) {\n                            this.specs.set(soundName as string, new SoundSpec().read(soundSection, this.defaults));\n                        }\n                        else {\n                            console.warn(`Missing sound section [${soundName}]`);\n                        }\n                    }\n                }\n            }\n            else {\n                console.warn(\"Missing sound [SoundList] section. Sounds will not be played.\");\n            }\n        }\n        else {\n            console.warn(\"Missing sound [Defaults] section. Sounds will not be played.\");\n        }\n    }\n    getSpec(name: string): SoundSpec | undefined {\n        return this.specs.get(name);\n    }\n    getAll(): SoundSpec[] {\n        return [...this.specs.values()];\n    }\n}\n"
  },
  {
    "path": "src/engine/sound/WorldSound.ts",
    "content": "import { SoundKey } from \"./SoundKey\";\nimport { ChannelType } from \"./ChannelType\";\nimport { SoundSpecs, SoundType, SoundControl } from \"./SoundSpecs\";\nimport { clamp, getRandomInt } from \"../../util/math\";\nimport { rectEquals } from \"../../util/geometry\";\nimport { ShroudType } from \"../../game/map/MapShroud\";\nimport { Coords } from \"../../game/Coords\";\nimport { isNotNullOrUndefined } from \"../../util/typeGuard\";\nimport { isPerformanceFeatureEnabled, measurePerformanceFeature } from \"@/performance/PerformanceRuntime\";\ninterface WorldPosition {\n    x: number;\n    y: number;\n    z: number;\n}\ninterface GameObject {\n    position: {\n        worldPosition: WorldPosition;\n    };\n}\ninterface SoundSpec {\n    name: string;\n    volume: number;\n    minVolume: number;\n    type: SoundType[];\n    control: Set<SoundControl>;\n    limit: number;\n    loop?: number;\n    range: number;\n    vShift?: {\n        min: number;\n        max: number;\n    };\n}\ninterface PlaybackHandle {\n    isPlaying(): boolean;\n    stop(): void;\n    setVolume(volume: number): void;\n    setPan(pan: number): void;\n}\ninterface Sound {\n    getSoundSpec(key: SoundKey | string): SoundSpec | undefined;\n    playWithOptions(spec: SoundSpec, channel: ChannelType, volume: number, pan: number, limit: number, loops: number): PlaybackHandle | undefined;\n}\ninterface Player {\n}\ninterface Shroud {\n    getShroudTypeByTileCoords(x: number, y: number, z: number): ShroudType;\n}\ninterface WorldViewportHelper {\n    distanceToViewportCenter(pos: WorldPosition): {\n        x: number;\n        y: number;\n    };\n    distanceToViewport(pos: WorldPosition): number;\n}\ninterface MapTileIntersectHelper {\n    getTileAtScreenPoint(point: {\n        x: number;\n        y: number;\n    }): {\n        rx: number;\n        ry: number;\n    } | undefined;\n}\ninterface World {\n    onObjectRemoved: {\n        subscribe(handler: (obj: GameObject) => void): void;\n        unsubscribe(handler: (obj: GameObject) => void): void;\n    };\n}\ninterface WorldScene {\n    viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n}\ninterface Renderer {\n    onFrame: {\n        subscribe(handler: (time: number) => void): void;\n        unsubscribe(handler: (time: number) => void): void;\n    };\n}\ninterface SoundInstance {\n    spec: SoundSpec;\n    gameObject?: GameObject;\n    worldPos: WorldPosition;\n    player: Player;\n    handle: PlaybackHandle;\n    gain: number;\n    volume: number;\n    loop: boolean;\n}\nexport class WorldSound {\n    private static readonly noShroudKeys = [\n        SoundKey.BuildingSlam,\n        SoundKey.SellSound,\n        SoundKey.BuildingGarrisonedSound,\n        SoundKey.BuildingRepairedSound,\n        SoundKey.SpySatActivationSound,\n        SoundKey.SpySatDeactivationSound,\n    ];\n    private sound: Sound;\n    private localPlayer: Player;\n    private shroud: Shroud;\n    private worldViewportHelper: WorldViewportHelper;\n    private mapTileIntersectHelper: MapTileIntersectHelper;\n    private world: World;\n    private worldScene: WorldScene;\n    private renderer: Renderer;\n    private soundInstances: SoundInstance[] = [];\n    private noShroudSpecs: SoundSpec[];\n    private lastViewport?: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    private lastUpdate?: number;\n    private tileAtViewportCenter?: {\n        rx: number;\n        ry: number;\n    };\n    private specCounts = new Map<SoundSpec, number>();\n    constructor(sound: Sound, localPlayer: Player, shroud: Shroud, worldViewportHelper: WorldViewportHelper, mapTileIntersectHelper: MapTileIntersectHelper, world: World, worldScene: WorldScene, renderer: Renderer) {\n        this.sound = sound;\n        this.localPlayer = localPlayer;\n        this.shroud = shroud;\n        this.worldViewportHelper = worldViewportHelper;\n        this.mapTileIntersectHelper = mapTileIntersectHelper;\n        this.world = world;\n        this.worldScene = worldScene;\n        this.renderer = renderer;\n        this.noShroudSpecs = WorldSound.noShroudKeys\n            .map((key) => {\n            const spec = this.sound.getSoundSpec(key);\n            if (spec)\n                return spec;\n            console.warn(`Sound key \"${key}\" doesn't have a corresponding sound.ini entry`);\n        })\n            .filter(isNotNullOrUndefined);\n    }\n    private handleObjectRemoved = (obj: GameObject): void => {\n        this.soundInstances.forEach((instance) => {\n            if (instance.gameObject === obj) {\n                instance.handle.stop();\n            }\n        });\n    };\n    private handleFrame = (time: number): void => {\n        let shouldUpdate = false;\n        if (!this.lastViewport || !rectEquals(this.worldScene.viewport, this.lastViewport)) {\n            this.lastViewport = this.worldScene.viewport;\n            shouldUpdate = true;\n        }\n        if (!this.lastUpdate || time - this.lastUpdate >= 200) {\n            shouldUpdate = true;\n        }\n        if (shouldUpdate) {\n            this.update();\n            this.lastUpdate = time;\n        }\n    };\n    init(): void {\n        this.renderer.onFrame.subscribe(this.handleFrame);\n        this.world.onObjectRemoved.subscribe(this.handleObjectRemoved);\n    }\n    changeLocalPlayer(player: Player, shroud: Shroud): void {\n        this.localPlayer = player;\n        this.shroud = shroud;\n    }\n    dispose(): void {\n        this.renderer.onFrame.unsubscribe(this.handleFrame);\n        this.world.onObjectRemoved.unsubscribe(this.handleObjectRemoved);\n        this.soundInstances.forEach((instance) => instance.handle.stop());\n    }\n    private update(): void {\n        measurePerformanceFeature('worldSoundLoopCache', () => isPerformanceFeatureEnabled('worldSoundLoopCache')\n            ? this.updateOptimized()\n            : this.updateLegacy());\n    }\n    private updateLegacy(): void {\n        const centerTile = this.mapTileIntersectHelper.getTileAtScreenPoint({\n            x: this.worldScene.viewport.x + this.worldScene.viewport.width / 2,\n            y: this.worldScene.viewport.y + this.worldScene.viewport.height / 2,\n        });\n        if (centerTile) {\n            this.tileAtViewportCenter = centerTile;\n            this.cleanOldInstances();\n            const specCounts = new Map<SoundSpec, number>();\n            for (const instance of this.soundInstances) {\n                const worldPos = instance.gameObject?.position.worldPosition ?? instance.worldPos;\n                let { volume, pan } = this.computeVolumeAndPan(instance.spec, worldPos, instance.player, instance.gain);\n                if (volume > 0) {\n                    const count = specCounts.get(instance.spec) ?? 0;\n                    if (instance.loop && count >= instance.spec.limit) {\n                        volume = 0;\n                    }\n                    else {\n                        specCounts.set(instance.spec, count + 1);\n                    }\n                }\n                instance.handle.setVolume(volume);\n                instance.handle.setPan(pan);\n                instance.volume = volume;\n            }\n        }\n        else {\n            console.warn(\"No tile found at viewport center. Can't update local sound positions.\");\n        }\n    }\n    private updateOptimized(): void {\n        const centerTile = this.mapTileIntersectHelper.getTileAtScreenPoint({\n            x: this.worldScene.viewport.x + this.worldScene.viewport.width / 2,\n            y: this.worldScene.viewport.y + this.worldScene.viewport.height / 2,\n        });\n        if (!centerTile) {\n            console.warn(\"No tile found at viewport center. Can't update local sound positions.\");\n            return;\n        }\n        this.tileAtViewportCenter = centerTile;\n        this.cleanOldInstances();\n        this.specCounts.clear();\n        for (const instance of this.soundInstances) {\n            const worldPos = instance.gameObject?.position.worldPosition ?? instance.worldPos;\n            let { volume, pan } = this.computeVolumeAndPan(instance.spec, worldPos, instance.player, instance.gain);\n            if (volume > 0) {\n                const count = this.specCounts.get(instance.spec) ?? 0;\n                if (instance.loop && count >= instance.spec.limit) {\n                    volume = 0;\n                }\n                else {\n                    this.specCounts.set(instance.spec, count + 1);\n                }\n            }\n            instance.handle.setVolume(volume);\n            instance.handle.setPan(pan);\n            instance.volume = volume;\n        }\n    }\n    private cleanOldInstances(): void {\n        this.soundInstances = this.soundInstances.filter((instance) => instance.handle.isPlaying());\n    }\n    playEffect(key: SoundKey | string, target: GameObject | WorldPosition, player: Player, gain: number = 1, loopGain?: number): PlaybackHandle | undefined {\n        const spec = this.sound.getSoundSpec(key);\n        if (!spec)\n            return;\n        if (spec.type.includes(SoundType.Player) && player !== this.localPlayer) {\n            return;\n        }\n        let worldPos: WorldPosition;\n        let gameObject: GameObject | undefined;\n        if ('position' in target) {\n            worldPos = target.position.worldPosition;\n            gameObject = target;\n        }\n        else {\n            worldPos = target;\n        }\n        const isLoop = spec.control.has(SoundControl.Loop) || spec.control.has(SoundControl.Ambient);\n        const loops = isLoop ? spec.loop || Number.POSITIVE_INFINITY : 0;\n        if (isLoop && loopGain !== undefined) {\n            gain = loopGain;\n        }\n        let { volume, pan } = this.computeVolumeAndPan(spec, worldPos, player, gain);\n        let limit = spec.limit;\n        if (isLoop && spec.limit) {\n            limit = 0;\n            this.cleanOldInstances();\n            const activeInstances = this.soundInstances.filter((instance) => instance.spec === spec && instance.volume > 0);\n            if (activeInstances.length >= spec.limit) {\n                volume = 0;\n            }\n        }\n        if (!isLoop && !volume && spec.limit) {\n            return;\n        }\n        const channel = spec.control.has(SoundControl.Ambient) || spec.name.startsWith(\"_Amb_\")\n            ? ChannelType.Ambient\n            : ChannelType.Effect;\n        const handle = this.sound.playWithOptions(spec, channel, volume, pan, limit, loops);\n        if (handle) {\n            this.soundInstances.push({\n                spec,\n                gameObject,\n                worldPos,\n                player,\n                handle,\n                gain,\n                volume,\n                loop: isLoop,\n            });\n        }\n        return handle;\n    }\n    private computeVolumeAndPan(spec: SoundSpec, worldPos: WorldPosition, player: Player, gain: number = 1): {\n        volume: number;\n        pan: number;\n    } {\n        let volume = spec.volume / 100;\n        let pan = 0;\n        if (spec.type.includes(SoundType.Global) && player !== this.localPlayer) {\n            volume = spec.minVolume / 100;\n        }\n        volume *= gain;\n        if (spec.type.includes(SoundType.Screen) || spec.type.includes(SoundType.Global)) {\n            const distanceToCenter = this.worldViewportHelper.distanceToViewportCenter(worldPos);\n            pan = clamp(distanceToCenter.x / (this.worldScene.viewport.width / 2), -1, 1);\n        }\n        if (spec.type.includes(SoundType.Screen)) {\n            const distanceToViewport = this.worldViewportHelper.distanceToViewport(worldPos);\n            const falloffDistance = (this.worldScene.viewport.height + this.worldScene.viewport.width) / 2 / 3;\n            volume *= (window as any).THREE.MathUtils.lerp(1, 0, Math.min(1, distanceToViewport / falloffDistance));\n        }\n        else if (spec.type.includes(SoundType.Local)) {\n            if (this.tileAtViewportCenter) {\n                const tileDistance = new (window as any).THREE.Vector2(worldPos.x / Coords.LEPTONS_PER_TILE - this.tileAtViewportCenter.rx, worldPos.z / Coords.LEPTONS_PER_TILE - this.tileAtViewportCenter.ry).length();\n                const maxRange = spec.range * Math.SQRT2;\n                if (maxRange < tileDistance) {\n                    volume = 0;\n                }\n                else {\n                    if (spec.vShift) {\n                        volume *= getRandomInt(spec.vShift.min, spec.vShift.max) / 100;\n                    }\n                    volume *= 1 - Math.min(1, (tileDistance / maxRange) ** 2);\n                }\n            }\n            else {\n                volume = 0;\n            }\n        }\n        if (this.noShroudSpecs.includes(spec) &&\n            !spec.type.includes(SoundType.Global) &&\n            this.shroud?.getShroudTypeByTileCoords(Math.floor(worldPos.x / Coords.LEPTONS_PER_TILE), Math.floor(worldPos.z / Coords.LEPTONS_PER_TILE), Math.floor(Coords.worldToTileHeight(worldPos.y))) === ShroudType.Unexplored) {\n            volume = 0;\n        }\n        return { volume, pan };\n    }\n}\n"
  },
  {
    "path": "src/engine/type/LightingType.ts",
    "content": "export enum LightingType {\n    None = 0,\n    Global = 1,\n    Level = 2,\n    Ambient = 3,\n    Full = 4,\n    Default = 5\n}\n"
  },
  {
    "path": "src/engine/type/ObjectType.ts",
    "content": "export enum ObjectType {\n    None = 0,\n    Aircraft = 1,\n    Building = 2,\n    Infantry = 3,\n    Overlay = 4,\n    Smudge = 5,\n    Terrain = 6,\n    Vehicle = 7,\n    Animation = 8,\n    Projectile = 9,\n    VoxelAnim = 10,\n    Debris = 11\n}\n"
  },
  {
    "path": "src/engine/type/OverlayTibType.ts",
    "content": "export enum OverlayTibType {\n    NotSpecial = 0,\n    Riparius = 1,\n    Cruentus = 2,\n    Vinifera = 4,\n    Aboreus = 8,\n    Ore = 1,\n    Gems = 2,\n    All = 15\n}\n"
  },
  {
    "path": "src/engine/type/PaletteType.ts",
    "content": "export enum PaletteType {\n    None = 0,\n    Iso = 1,\n    Unit = 2,\n    Overlay = 3,\n    Anim = 4,\n    Custom = 5,\n    Default = 6\n}\n"
  },
  {
    "path": "src/engine/type/PointerType.ts",
    "content": "export enum PointerType {\n    Default = 0,\n    Mini = 1,\n    Scroll = 2,\n    NoScroll = 10,\n    Select = 18,\n    Move = 31,\n    NoMove = 41,\n    MoveMini = 42,\n    NoActionMini = 52,\n    AttackRange = 53,\n    AttackNoRange = 58,\n    AttackMini = 63,\n    Guard = 68,\n    GuardMini = 73,\n    Unknown1 = 78,\n    Unknown2 = 88,\n    Occupy = 89,\n    NoOccupy = 99,\n    OccupyMini = 100,\n    Deploy = 110,\n    NoDeploy = 119,\n    Unknown3 = 120,\n    Sell = 129,\n    SellMini = 139,\n    NoSell = 149,\n    RepairMove = 150,\n    SideRepair = 170,\n    NoRepair = 190,\n    Unknown4 = 191,\n    Unknown5 = 199,\n    Dynamite = 204,\n    Unknown6 = 209,\n    Unknown7 = 214,\n    Unknown8 = 219,\n    Unknown9 = 224,\n    Unknown10 = 229,\n    Unknown11 = 234,\n    Unknwon12 = 239,\n    Unknown13 = 249,\n    Para = 259,\n    Unknown14 = 269,\n    Storm = 279,\n    EngineerDamage = 299,\n    C4 = 309,\n    Nuke = 319,\n    Unknown16 = 329,\n    Power = 339,\n    Unknown17 = 345,\n    Iron = 346,\n    Unknown18 = 351,\n    Unknown19 = 356,\n    Chrono = 357,\n    DefuseBomb = 369,\n    NoAction = 384,\n    Pan = 385,\n    Unknown21 = 394,\n    AttackMove = 404,\n    Unknown23 = 413,\n    Unknown24 = 422,\n    Unknown25 = 431,\n    Unknown26 = 432,\n    Unknown27 = 433,\n    Unknown28 = 434,\n    Beacon = 435\n}\n"
  },
  {
    "path": "src/engine/type/TerrainType.ts",
    "content": "export enum TerrainType {\n    Default = 0,\n    Tunnel = 5,\n    Railroad = 6,\n    Rock1 = 7,\n    Rock2 = 8,\n    Water = 9,\n    Shore = 10,\n    Pavement = 11,\n    Dirt = 12,\n    Clear = 13,\n    Rough = 14,\n    Cliff = 15\n}\n"
  },
  {
    "path": "src/engine/type/TiberiumType.ts",
    "content": "export enum TiberiumType {\n    Riparius = 0,\n    Cruentus = 1,\n    Vinifera = 2,\n    Aboreus = 3,\n    Ore = 0,\n    Gems = 1\n}\n"
  },
  {
    "path": "src/engine/util/EntityIntersectHelper.ts",
    "content": "import * as THREE from 'three';\nimport { rectContainsPoint } from '../../util/geometry';\nimport { isPerformanceFeatureEnabled, measurePerformanceFeature } from '@/performance/PerformanceRuntime';\ninterface Point {\n    x: number;\n    y: number;\n}\ninterface Point3D extends Point {\n    z: number;\n}\ninterface Viewport {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface Scene {\n    viewport: Viewport;\n}\ninterface Position {\n    worldPosition: Point3D;\n}\ninterface GameObject {\n    position: Position;\n    isUnit(): boolean;\n    isBuilding(): boolean;\n    isDestroyed: boolean;\n    isCrashing: boolean;\n    id?: string | number;\n}\ninterface Renderable {\n    gameObject: GameObject;\n    getIntersectTarget?(): THREE.Object3D | THREE.Object3D[] | undefined;\n}\ninterface RenderableContainer {\n    get3DObject(): THREE.Object3D | undefined;\n}\ninterface RenderableManager {\n    getRenderableContainer(): RenderableContainer | undefined;\n    getRenderableById(id: string): Renderable;\n    getRenderableByGameObject(gameObject: GameObject): Renderable;\n}\ninterface MapTile {\n    rx: number;\n    ry: number;\n    z: number;\n}\ninterface MapTileIntersectHelper {\n    getTileAtScreenPoint(point: Point): MapTile | undefined;\n}\ninterface GameMap {\n    getObjectsOnTile(tile: MapTile): GameObject[];\n}\ninterface RaycastHelper {\n    intersect(point: Point, targets: THREE.Object3D[], recursive: boolean): THREE.Intersection[];\n}\ninterface WorldViewportHelper {\n    intersectsScreenBox(worldPosition: Point3D, screenBox: THREE.Box2): boolean;\n}\ninterface IntersectionResult {\n    renderable: Renderable;\n    point: THREE.Vector3;\n}\nexport class EntityIntersectHelper {\n    private map: GameMap;\n    private renderableManager: RenderableManager;\n    private mapTileIntersectHelper: MapTileIntersectHelper;\n    private raycastHelper: RaycastHelper;\n    private scene: Scene;\n    private worldViewportHelper: WorldViewportHelper;\n    private intersectTargetStack: THREE.Object3D[] = [];\n    private intersectTargetsScratch: THREE.Object3D[] = [];\n    constructor(map: GameMap, renderableManager: RenderableManager, mapTileIntersectHelper: MapTileIntersectHelper, raycastHelper: RaycastHelper, scene: Scene, worldViewportHelper: WorldViewportHelper) {\n        this.map = map;\n        this.renderableManager = renderableManager;\n        this.mapTileIntersectHelper = mapTileIntersectHelper;\n        this.raycastHelper = raycastHelper;\n        this.scene = scene;\n        this.worldViewportHelper = worldViewportHelper;\n    }\n    getEntitiesAtScreenBox(screenBox: THREE.Box2): Renderable[] {\n        const container = this.renderableManager.getRenderableContainer();\n        if (!container)\n            return [];\n        const intersectTargets = this.collectIntersectTargets(container.get3DObject());\n        const renderableSet = new Set<Renderable>();\n        intersectTargets.forEach(target => {\n            const renderableId = this.findRenderableId(target);\n            const renderable = this.renderableManager.getRenderableById(renderableId);\n            renderableSet.add(renderable);\n        });\n        return [...renderableSet].filter(renderable => this.worldViewportHelper.intersectsScreenBox(renderable.gameObject.position.worldPosition, screenBox));\n    }\n    getEntityAtScreenPoint(screenPoint: Point): IntersectionResult | undefined {\n        const viewport = this.scene.viewport;\n        if (!rectContainsPoint(viewport, screenPoint)) {\n            return undefined;\n        }\n        const container = this.renderableManager.getRenderableContainer();\n        const tile = this.mapTileIntersectHelper.getTileAtScreenPoint(screenPoint);\n        const buildingOnTile = this.getBuildingRenderableOnTile(tile);\n        if (!container) {\n            return buildingOnTile\n                ? {\n                    renderable: buildingOnTile,\n                    point: this.createFallbackPoint(buildingOnTile),\n                }\n                : undefined;\n        }\n        const intersectTargets = this.collectIntersectTargets(container.get3DObject());\n        const intersections = this.raycastHelper.intersect(screenPoint, intersectTargets, false);\n        if (intersections.length === 0) {\n            return buildingOnTile\n                ? {\n                    renderable: buildingOnTile,\n                    point: this.createFallbackPoint(buildingOnTile),\n                }\n                : undefined;\n        }\n        const renderableIntersections = intersections.map(intersection => ({\n            renderable: this.renderableManager.getRenderableById(this.findRenderableId(intersection.object)),\n            point: intersection.point\n        }));\n        const unitResult = renderableIntersections.find(result => result.renderable.gameObject.isUnit());\n        if (unitResult)\n            return unitResult;\n        if (buildingOnTile) {\n            const tileBuildingResult = renderableIntersections.find((result) => result.renderable.gameObject === buildingOnTile.gameObject);\n            return {\n                renderable: buildingOnTile,\n                point: tileBuildingResult?.point ?? this.createFallbackPoint(buildingOnTile),\n            };\n        }\n        const buildingResult = renderableIntersections.find(result => result.renderable.gameObject.isBuilding() &&\n            result.renderable.getIntersectTarget?.());\n        return buildingResult ?? renderableIntersections[0];\n    }\n    private getBuildingRenderableOnTile(tile: MapTile | undefined): Renderable | undefined {\n        if (!tile) {\n            return undefined;\n        }\n        const building = this.map.getObjectsOnTile(tile).find((obj) => {\n            if (!obj.isBuilding() || obj.isDestroyed || obj.isCrashing) {\n                return false;\n            }\n            const renderable = this.renderableManager.getRenderableByGameObject(obj);\n            return Boolean(renderable);\n        });\n        return building ? this.renderableManager.getRenderableByGameObject(building) : undefined;\n    }\n    private createFallbackPoint(renderable: Renderable): THREE.Vector3 {\n        const { worldPosition } = renderable.gameObject.position;\n        return new THREE.Vector3(worldPosition.x, worldPosition.y, worldPosition.z);\n    }\n    private collectIntersectTargets(object3d: THREE.Object3D | undefined): THREE.Object3D[] {\n        return measurePerformanceFeature('entityIntersectTraversal', () => isPerformanceFeatureEnabled('entityIntersectTraversal')\n            ? this.collectIntersectTargetsOptimized(object3d)\n            : this.collectIntersectTargetsLegacy(object3d));\n    }\n    private collectIntersectTargetsLegacy(object3d: THREE.Object3D | undefined): THREE.Object3D[] {\n        const targets: THREE.Object3D[] = [];\n        if (!object3d || !object3d.visible)\n            return targets;\n        if (object3d.userData.id !== undefined) {\n            const renderable = this.renderableManager.getRenderableById(object3d.userData.id);\n            if (!renderable) {\n                throw new Error(`Entity not found (id = \"${object3d.userData.id}\")`);\n            }\n            if (!renderable.gameObject.isDestroyed && !renderable.gameObject.isCrashing) {\n                const intersectTarget = renderable.getIntersectTarget?.();\n                if (intersectTarget) {\n                    if (Array.isArray(intersectTarget)) {\n                        targets.push(...intersectTarget);\n                    }\n                    else {\n                        targets.push(intersectTarget);\n                    }\n                }\n            }\n        }\n        object3d.children.forEach(child => {\n            if (child.visible) {\n                targets.push(...this.collectIntersectTargetsLegacy(child));\n            }\n        });\n        return targets;\n    }\n    private collectIntersectTargetsOptimized(object3d: THREE.Object3D | undefined): THREE.Object3D[] {\n        const targets = this.intersectTargetsScratch;\n        targets.length = 0;\n        if (!object3d || !object3d.visible) {\n            return [];\n        }\n        const stack = this.intersectTargetStack;\n        stack.length = 0;\n        stack.push(object3d);\n        while (stack.length) {\n            const currentObject = stack.pop()!;\n            if (!currentObject.visible) {\n                continue;\n            }\n            if (currentObject.userData.id !== undefined) {\n                const renderable = this.renderableManager.getRenderableById(currentObject.userData.id);\n                if (!renderable) {\n                    throw new Error(`Entity not found (id = \"${currentObject.userData.id}\")`);\n                }\n                if (!renderable.gameObject.isDestroyed && !renderable.gameObject.isCrashing) {\n                    const intersectTarget = renderable.getIntersectTarget?.();\n                    if (intersectTarget) {\n                        if (Array.isArray(intersectTarget)) {\n                            targets.push(...intersectTarget);\n                        }\n                        else {\n                            targets.push(intersectTarget);\n                        }\n                    }\n                }\n            }\n            for (let index = currentObject.children.length - 1; index >= 0; index -= 1) {\n                const child = currentObject.children[index];\n                if (child.visible) {\n                    stack.push(child);\n                }\n            }\n        }\n        return [...targets];\n    }\n    private findRenderableId(object3d: THREE.Object3D): string {\n        let currentObject: THREE.Object3D | null = object3d;\n        let id: string | undefined;\n        while (currentObject && currentObject.parent) {\n            id = currentObject.userData.id;\n            if (id !== undefined)\n                break;\n            currentObject = currentObject.parent;\n        }\n        if (id === undefined) {\n            throw new Error('No attached renderable ID found for Object3D.');\n        }\n        return id;\n    }\n}\n"
  },
  {
    "path": "src/engine/util/MapPanningHelper.ts",
    "content": "import { IsoCoords } from '../IsoCoords';\ninterface Point {\n    x: number;\n    y: number;\n}\ninterface Point3D extends Point {\n    z: number;\n}\ninterface Tile {\n    z: number;\n}\ninterface TileManager {\n    getByMapCoords(x: number, y: number): Tile | undefined;\n    getPlaceholderTile(x: number, y: number): Tile;\n}\ninterface GameMap {\n    tiles: TileManager;\n}\ninterface Rect {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\nexport class MapPanningHelper {\n    private map: GameMap;\n    constructor(map: GameMap) {\n        this.map = map;\n    }\n    computeCameraPanFromTile(tileX: number, tileY: number): Point {\n        const tile = this.map.tiles.getByMapCoords(tileX, tileY) ??\n            this.map.tiles.getPlaceholderTile(tileX, tileY);\n        const screenPos = IsoCoords.tile3dToScreen(tileX + 0.5, tileY + 0.5, tile.z);\n        return this.computeCameraPanFromScreen(screenPos);\n    }\n    computeCameraPanFromWorld(worldPosition: Point3D): Point {\n        const screenPos = IsoCoords.vecWorldToScreen(worldPosition);\n        return this.computeCameraPanFromScreen(screenPos);\n    }\n    computeCameraPanFromScreen(screenPosition: Point): Point {\n        const origin = this.getScreenPanOrigin();\n        return {\n            x: Math.floor(screenPosition.x - origin.x),\n            y: Math.floor(screenPosition.y - origin.y)\n        };\n    }\n    getScreenPanOrigin(): Point {\n        return IsoCoords.worldToScreen(0, 0);\n    }\n    computeCameraPanLimits(viewport: Rect, mapBounds: Rect): Rect {\n        const origin = this.getScreenPanOrigin();\n        return {\n            x: Math.ceil(mapBounds.x - origin.x + viewport.width / 2),\n            y: Math.ceil(mapBounds.y - origin.y + viewport.height / 2 - 1),\n            width: mapBounds.width - viewport.width - 1,\n            height: mapBounds.height - viewport.height - 1\n        };\n    }\n}\n"
  },
  {
    "path": "src/engine/util/MapTileIntersectHelper.ts",
    "content": "import * as THREE from 'three';\nimport { rectContainsPoint } from '../../util/geometry';\nimport { Coords } from '../../game/Coords';\nimport { IsoCoords } from '../IsoCoords';\nimport { isPerformanceFeatureEnabled, measurePerformanceFeature } from '@/performance/PerformanceRuntime';\ninterface Point {\n    x: number;\n    y: number;\n}\ninterface Viewport {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface CameraPan {\n    getPan(): Point;\n}\ninterface Scene {\n    viewport: Viewport;\n    cameraPan: CameraPan;\n}\ninterface MapTile {\n    rx: number;\n    ry: number;\n    z: number;\n}\ninterface TileManager {\n    getByMapCoords(x: number, y: number): MapTile | undefined;\n}\ninterface GameMap {\n    tiles: TileManager;\n}\nexport class MapTileIntersectHelper {\n    private map: GameMap;\n    private scene: Scene;\n    private intersectTriangle?: THREE.Triangle;\n    private intersectPoint?: THREE.Vector3;\n    private intersectedTilesScratch: MapTile[] = [];\n    constructor(map: GameMap, scene: Scene) {\n        this.map = map;\n        this.scene = scene;\n    }\n    getTileAtScreenPoint(screenPoint: Point): MapTile | undefined {\n        const viewport = this.scene.viewport;\n        if (rectContainsPoint(viewport, screenPoint)) {\n            const intersectedTiles = this.intersectTilesByScreenPos(screenPoint);\n            return intersectedTiles.length > 0 ? intersectedTiles[0] : undefined;\n        }\n        return undefined;\n    }\n    intersectTilesByScreenPos(screenPoint: Point): MapTile[] {\n        return measurePerformanceFeature('mapTileHitTest', () => isPerformanceFeatureEnabled('mapTileHitTest')\n            ? this.intersectTilesByScreenPosOptimized(screenPoint)\n            : this.intersectTilesByScreenPosLegacy(screenPoint));\n    }\n    private intersectTilesByScreenPosLegacy(screenPoint: Point): MapTile[] {\n        const origin = IsoCoords.worldToScreen(0, 0);\n        const pan = this.scene.cameraPan.getPan();\n        const worldScreenPos = {\n            x: screenPoint.x + origin.x + pan.x - this.scene.viewport.width / 2,\n            y: screenPoint.y + origin.y + pan.y - this.scene.viewport.height / 2\n        };\n        const worldPos = IsoCoords.screenToWorld(worldScreenPos.x, worldScreenPos.y);\n        const tileCoords = new THREE.Vector2(worldPos.x, worldPos.y)\n            .multiplyScalar(1 / Coords.LEPTONS_PER_TILE)\n            .floor();\n        const centerTile = this.map.tiles.getByMapCoords(tileCoords.x, tileCoords.y);\n        if (!centerTile) {\n            console.warn(`Tile coordinates (${tileCoords.x},${tileCoords.y}) out of range`);\n            return [];\n        }\n        const candidateTiles: MapTile[] = [];\n        for (let offset = 0; offset < 30; offset++) {\n            const testCoords = [\n                { x: centerTile.rx + offset, y: centerTile.ry + offset },\n                { x: centerTile.rx + offset + 1, y: centerTile.ry + offset },\n                { x: centerTile.rx + offset, y: centerTile.ry + offset + 1 }\n            ];\n            for (const coord of testCoords) {\n                const tile = this.map.tiles.getByMapCoords(coord.x, coord.y);\n                if (tile) {\n                    candidateTiles.push(tile);\n                }\n            }\n        }\n        const intersectedTiles: MapTile[] = [];\n        const triangle = new THREE.Triangle();\n        const testPoint = new THREE.Vector3(worldScreenPos.x, 0, worldScreenPos.y);\n        for (const tile of candidateTiles) {\n            const corner1 = IsoCoords.tile3dToScreen(tile.rx, tile.ry, tile.z);\n            const corner2 = IsoCoords.tile3dToScreen(tile.rx, tile.ry + 1.1, tile.z);\n            const corner3 = IsoCoords.tile3dToScreen(tile.rx + 1.1, tile.ry, tile.z);\n            const corner4 = IsoCoords.tile3dToScreen(tile.rx + 1.1, tile.ry + 1.1, tile.z);\n            triangle.a.set(corner1.x, 0, corner1.y);\n            triangle.b.set(corner2.x, 0, corner2.y);\n            triangle.c.set(corner3.x, 0, corner3.y);\n            const intersects1 = triangle.containsPoint(testPoint);\n            triangle.a.set(corner4.x, 0, corner4.y);\n            triangle.b.set(corner2.x, 0, corner2.y);\n            triangle.c.set(corner3.x, 0, corner3.y);\n            const intersects2 = triangle.containsPoint(testPoint);\n            if (intersects1 || intersects2) {\n                intersectedTiles.unshift(tile);\n            }\n        }\n        if (intersectedTiles.length === 0) {\n            return this.intersectTilesByScreenPosLegacy({\n                x: screenPoint.x,\n                y: screenPoint.y - IsoCoords.tileHeightToScreen(1)\n            });\n        }\n        return intersectedTiles;\n    }\n    private intersectTilesByScreenPosOptimized(screenPoint: Point): MapTile[] {\n        const triangle = this.intersectTriangle ?? (this.intersectTriangle = new THREE.Triangle());\n        const testPoint = this.intersectPoint ?? (this.intersectPoint = new THREE.Vector3());\n        const intersectedTiles = this.intersectedTilesScratch;\n        const origin = IsoCoords.worldToScreen(0, 0);\n        const pan = this.scene.cameraPan.getPan();\n        const fallbackOffsetY = IsoCoords.tileHeightToScreen(1);\n        let currentY = screenPoint.y;\n        for (let attempt = 0; attempt < 4; attempt += 1) {\n            intersectedTiles.length = 0;\n            const worldScreenX = screenPoint.x + origin.x + pan.x - this.scene.viewport.width / 2;\n            const worldScreenY = currentY + origin.y + pan.y - this.scene.viewport.height / 2;\n            const worldPos = IsoCoords.screenToWorld(worldScreenX, worldScreenY);\n            const tileX = Math.floor(worldPos.x / Coords.LEPTONS_PER_TILE);\n            const tileY = Math.floor(worldPos.y / Coords.LEPTONS_PER_TILE);\n            const centerTile = this.map.tiles.getByMapCoords(tileX, tileY);\n            if (!centerTile) {\n                console.warn(`Tile coordinates (${tileX},${tileY}) out of range`);\n                return [];\n            }\n            testPoint.set(worldScreenX, 0, worldScreenY);\n            for (let offset = 0; offset < 30; offset += 1) {\n                const testCoords = [\n                    { x: centerTile.rx + offset, y: centerTile.ry + offset },\n                    { x: centerTile.rx + offset + 1, y: centerTile.ry + offset },\n                    { x: centerTile.rx + offset, y: centerTile.ry + offset + 1 }\n                ];\n                for (const coord of testCoords) {\n                    const tile = this.map.tiles.getByMapCoords(coord.x, coord.y);\n                    if (!tile) {\n                        continue;\n                    }\n                    const corner1 = IsoCoords.tile3dToScreen(tile.rx, tile.ry, tile.z);\n                    const corner2 = IsoCoords.tile3dToScreen(tile.rx, tile.ry + 1.1, tile.z);\n                    const corner3 = IsoCoords.tile3dToScreen(tile.rx + 1.1, tile.ry, tile.z);\n                    const corner4 = IsoCoords.tile3dToScreen(tile.rx + 1.1, tile.ry + 1.1, tile.z);\n                    triangle.a.set(corner1.x, 0, corner1.y);\n                    triangle.b.set(corner2.x, 0, corner2.y);\n                    triangle.c.set(corner3.x, 0, corner3.y);\n                    const intersects1 = triangle.containsPoint(testPoint);\n                    triangle.a.set(corner4.x, 0, corner4.y);\n                    triangle.b.set(corner2.x, 0, corner2.y);\n                    triangle.c.set(corner3.x, 0, corner3.y);\n                    const intersects2 = triangle.containsPoint(testPoint);\n                    if (intersects1 || intersects2) {\n                        intersectedTiles.unshift(tile);\n                    }\n                }\n            }\n            if (intersectedTiles.length > 0) {\n                return [...intersectedTiles];\n            }\n            currentY -= fallbackOffsetY;\n        }\n        return [];\n    }\n}\n"
  },
  {
    "path": "src/engine/util/RaycastHelper.ts",
    "content": "import * as THREE from 'three';\nimport { isPerformanceFeatureEnabled, measurePerformanceFeature } from '@/performance/PerformanceRuntime';\ninterface Point {\n    x: number;\n    y: number;\n}\ninterface Viewport {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface Scene {\n    viewport: Viewport;\n    camera: THREE.Camera;\n}\nexport class RaycastHelper {\n    private scene: Scene;\n    private raycaster?: THREE.Raycaster;\n    private normalizedPointer?: Point;\n    constructor(scene: Scene) {\n        this.scene = scene;\n    }\n    intersect(point: Point, targets: THREE.Object3D[], recursive: boolean = false): THREE.Intersection[] {\n        return measurePerformanceFeature('raycastHelperReuse', () => isPerformanceFeatureEnabled('raycastHelperReuse')\n            ? this.intersectOptimized(point, targets, recursive)\n            : this.intersectLegacy(point, targets, recursive));\n    }\n    private intersectLegacy(point: Point, targets: THREE.Object3D[], recursive: boolean): THREE.Intersection[] {\n        const raycaster = new THREE.Raycaster();\n        const normalizedPointer = this.normalizePointerLegacy(point, this.scene.viewport);\n        raycaster.setFromCamera(normalizedPointer as any, this.scene.camera);\n        return raycaster.intersectObjects(targets, recursive);\n    }\n    private intersectOptimized(point: Point, targets: THREE.Object3D[], recursive: boolean): THREE.Intersection[] {\n        const raycaster = this.raycaster ?? (this.raycaster = new THREE.Raycaster());\n        const normalizedPointer = this.normalizePointerOptimized(point, this.scene.viewport);\n        raycaster.setFromCamera(normalizedPointer as any, this.scene.camera);\n        return raycaster.intersectObjects(targets, recursive);\n    }\n    private normalizePointerLegacy(point: Point, viewport: Viewport): Point {\n        return {\n            x: ((point.x - viewport.x) / viewport.width) * 2 - 1,\n            y: 2 * -((point.y - viewport.y) / viewport.height) + 1,\n        };\n    }\n    private normalizePointerOptimized(point: Point, viewport: Viewport): Point {\n        const target = this.normalizedPointer ?? (this.normalizedPointer = { x: 0, y: 0 });\n        target.x = ((point.x - viewport.x) / viewport.width) * 2 - 1;\n        target.y = 2 * -((point.y - viewport.y) / viewport.height) + 1;\n        return target;\n    }\n}\n"
  },
  {
    "path": "src/engine/util/WorldViewportHelper.ts",
    "content": "import * as THREE from 'three';\nimport { IsoCoords } from '../IsoCoords';\nimport { isPerformanceFeatureEnabled, measurePerformanceFeature } from '@/performance/PerformanceRuntime';\ninterface Point {\n    x: number;\n    y: number;\n}\ninterface Point3D extends Point {\n    z: number;\n}\ninterface Viewport {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface CameraPan {\n    getPan(): Point;\n}\ninterface Scene {\n    viewport: Viewport;\n    cameraPan: CameraPan;\n    camera?: THREE.Camera;\n}\nexport class WorldViewportHelper {\n    private scene: Scene;\n    private viewportBox?: THREE.Box2;\n    private screenPoint?: THREE.Vector2;\n    private viewportCenter?: THREE.Vector2;\n    private projectedWorld?: THREE.Vector3;\n    constructor(scene: Scene) {\n        this.scene = scene;\n    }\n    distanceToViewport(worldPosition: Point3D): number {\n        return measurePerformanceFeature('worldViewportCache', () => isPerformanceFeatureEnabled('worldViewportCache')\n            ? this.distanceToViewportOptimized(worldPosition)\n            : this.distanceToViewportLegacy(worldPosition));\n    }\n    distanceToScreenBox(worldPosition: Point3D, screenBox: THREE.Box2): number {\n        return measurePerformanceFeature('worldViewportCache', () => isPerformanceFeatureEnabled('worldViewportCache')\n            ? this.distanceToScreenBoxOptimized(worldPosition, screenBox)\n            : this.distanceToScreenBoxLegacy(worldPosition, screenBox));\n    }\n    distanceToViewportCenter(worldPosition: Point3D): THREE.Vector2 {\n        return measurePerformanceFeature('worldViewportCache', () => isPerformanceFeatureEnabled('worldViewportCache')\n            ? this.distanceToViewportCenterOptimized(worldPosition)\n            : this.distanceToViewportCenterLegacy(worldPosition));\n    }\n    intersectsScreenBox(worldPosition: Point3D, screenBox: THREE.Box2): boolean {\n        return this.distanceToScreenBox(worldPosition, screenBox) === 0;\n    }\n    private distanceToViewportLegacy(worldPosition: Point3D): number {\n        const viewport = this.scene.viewport;\n        const viewportBox = new THREE.Box2(new THREE.Vector2(viewport.x, viewport.y), new THREE.Vector2(viewport.x + viewport.width - 1, viewport.y + viewport.height - 1));\n        return this.distanceToScreenBoxLegacy(worldPosition, viewportBox);\n    }\n    private distanceToViewportOptimized(worldPosition: Point3D): number {\n        const viewport = this.scene.viewport;\n        const viewportBox = this.viewportBox ?? (this.viewportBox = new THREE.Box2(new THREE.Vector2(), new THREE.Vector2()));\n        viewportBox.min.set(viewport.x, viewport.y);\n        viewportBox.max.set(viewport.x + viewport.width - 1, viewport.y + viewport.height - 1);\n        return this.distanceToScreenBoxOptimized(worldPosition, viewportBox);\n    }\n    private distanceToScreenBoxLegacy(worldPosition: Point3D, screenBox: THREE.Box2): number {\n        return screenBox.distanceToPoint(this.getScreenPositionLegacy(worldPosition));\n    }\n    private distanceToScreenBoxOptimized(worldPosition: Point3D, screenBox: THREE.Box2): number {\n        return screenBox.distanceToPoint(this.getScreenPositionOptimized(worldPosition, this.screenPoint ?? (this.screenPoint = new THREE.Vector2())));\n    }\n    private distanceToViewportCenterLegacy(worldPosition: Point3D): THREE.Vector2 {\n        const viewport = this.scene.viewport;\n        const viewportCenter = new THREE.Vector2(viewport.x + viewport.width / 2, viewport.y + viewport.height / 2);\n        return this.getScreenPositionLegacy(worldPosition).sub(viewportCenter);\n    }\n    private distanceToViewportCenterOptimized(worldPosition: Point3D): THREE.Vector2 {\n        const viewport = this.scene.viewport;\n        const viewportCenter = this.viewportCenter ?? (this.viewportCenter = new THREE.Vector2());\n        viewportCenter.set(viewport.x + viewport.width / 2, viewport.y + viewport.height / 2);\n        return this.getScreenPositionOptimized(worldPosition, this.screenPoint ?? (this.screenPoint = new THREE.Vector2())).sub(viewportCenter);\n    }\n    private getScreenPositionLegacy(worldPosition: Point3D): THREE.Vector2 {\n        const viewport = this.scene.viewport;\n        const camera = this.scene.camera;\n        if (camera) {\n            const projected = new THREE.Vector3(worldPosition.x, worldPosition.y, worldPosition.z).project(camera);\n            if (Number.isFinite(projected.x) && Number.isFinite(projected.y) && Number.isFinite(projected.z)) {\n                return new THREE.Vector2(viewport.x + ((projected.x + 1) / 2) * viewport.width, viewport.y + ((1 - projected.y) / 2) * viewport.height);\n            }\n        }\n        const screenPos = IsoCoords.vecWorldToScreen(worldPosition);\n        const origin = IsoCoords.worldToScreen(0, 0);\n        const pan = this.scene.cameraPan.getPan();\n        return new THREE.Vector2(screenPos.x - origin.x - pan.x + viewport.x + viewport.width / 2, screenPos.y - origin.y - pan.y + viewport.y + viewport.height / 2);\n    }\n    private getScreenPositionOptimized(worldPosition: Point3D, target: THREE.Vector2): THREE.Vector2 {\n        const viewport = this.scene.viewport;\n        const camera = this.scene.camera;\n        if (camera) {\n            const projected = this.projectedWorld ?? (this.projectedWorld = new THREE.Vector3());\n            projected.set(worldPosition.x, worldPosition.y, worldPosition.z).project(camera);\n            if (Number.isFinite(projected.x) && Number.isFinite(projected.y) && Number.isFinite(projected.z)) {\n                target.set(viewport.x + ((projected.x + 1) / 2) * viewport.width, viewport.y + ((1 - projected.y) / 2) * viewport.height);\n                return target;\n            }\n        }\n        const screenPos = IsoCoords.vecWorldToScreen(worldPosition);\n        const origin = IsoCoords.worldToScreen(0, 0);\n        const pan = this.scene.cameraPan.getPan();\n        target.set(screenPos.x - origin.x - pan.x + viewport.x + viewport.width / 2, screenPos.y - origin.y - pan.y + viewport.y + viewport.height / 2);\n        return target;\n    }\n}\n"
  },
  {
    "path": "src/game/Alliances.ts",
    "content": "import { fnv32a } from '@/util/math';\nimport { Player } from './Player';\nimport { PlayerList } from './PlayerList';\nexport enum AllianceStatus {\n    Requested = 0,\n    Formed = 1\n}\nclass PlayerPair {\n    constructor(public first: Player, public second: Player) { }\n    has(player: Player): boolean {\n        return this.first === player || this.second === player;\n    }\n    equals(other: PlayerPair): boolean {\n        return ((this.first === other.first && this.second === other.second) ||\n            (this.first === other.second && this.second === other.first));\n    }\n}\ninterface Alliance {\n    players: PlayerPair;\n    status: AllianceStatus;\n}\nexport class Alliances {\n    private alliances: Alliance[] = [];\n    constructor(private playerList: PlayerList) { }\n    findByPlayers(player1: Player, player2: Player): Alliance | undefined {\n        const pair = new PlayerPair(player1, player2);\n        return this.alliances.find(alliance => alliance.players.equals(pair));\n    }\n    filterByPlayer(player: Player): Alliance[] {\n        return this.alliances.filter(alliance => alliance.players.first === player || alliance.players.second === player);\n    }\n    request(player1: Player, player2: Player): Alliance | undefined {\n        if (!this.canRequestAlliance(player2)) {\n            throw new Error(`Player ${player2.name} is not a human combatant.`);\n        }\n        if (this.canFormAlliance(player1, player2)) {\n            if (this.findByPlayers(player1, player2)) {\n                throw new Error(`Can't request alliance because an alliance is already pending or formed between ${player1.name} and ${player2.name}.`);\n            }\n            return this.setAlliance(player1, player2, AllianceStatus.Requested);\n        }\n    }\n    cancelRequest(player1: Player, player2: Player): void {\n        const alliance = this.findByPlayers(player1, player2);\n        if (!alliance || alliance.status !== AllianceStatus.Requested) {\n            throw new Error(`There is no pending alliance request for player ${player2.name} from player ${player1.name}`);\n        }\n        if (alliance.players.first !== player1) {\n            throw new Error(`Can't cancel request initiated by the other player (${player2.name})`);\n        }\n        this.alliances.splice(this.alliances.indexOf(alliance), 1);\n    }\n    acceptRequest(player1: Player, player2: Player): void {\n        if (this.canFormAlliance(player1, player2)) {\n            const alliance = this.findByPlayers(player1, player2);\n            if (!alliance || alliance.status !== AllianceStatus.Requested) {\n                throw new Error(`There is no pending alliance request for player ${player2.name} from player ${player1.name}`);\n            }\n            if (alliance.players.first !== player1) {\n                throw new Error(`Can't accept own alliance request for player ${player2.name}`);\n            }\n            alliance.status = AllianceStatus.Formed;\n        }\n    }\n    setAlliance(player1: Player, player2: Player, status: AllianceStatus): Alliance {\n        if (!this.canFormAlliance(player1, player2)) {\n            throw new Error(`Can't form alliance between players \"${player1.name}\" and \"${player2.name}\"`);\n        }\n        const existing = this.findByPlayers(player1, player2);\n        if (existing) {\n            throw new Error(`An alliance already exists between players ${player1.name} and ${player2.name}`);\n        }\n        const alliance: Alliance = {\n            players: new PlayerPair(player1, player2),\n            status\n        };\n        this.alliances.push(alliance);\n        return alliance;\n    }\n    breakAlliance(player1: Player, player2: Player): void {\n        const alliance = this.findByPlayers(player1, player2);\n        if (!alliance || alliance.status !== AllianceStatus.Formed) {\n            throw new Error(`There is no alliance between player ${player1.name} and player ${player2.name}`);\n        }\n        this.alliances.splice(this.alliances.indexOf(alliance), 1);\n    }\n    areAllied(player1: Player, player2: Player): boolean {\n        const alliance = this.findByPlayers(player1, player2);\n        return !!alliance && alliance.status === AllianceStatus.Formed;\n    }\n    getAllies(player: Player): Player[] {\n        return this.filterByPlayer(player)\n            .filter(alliance => alliance.status === AllianceStatus.Formed)\n            .map(alliance => alliance.players.first === player\n            ? alliance.players.second\n            : alliance.players.first);\n    }\n    haveSharedIntel(player1: Player, player2: Player): boolean {\n        return (player1.isObserver ||\n            player2.isObserver ||\n            player1 === player2 ||\n            this.areAllied(player1, player2));\n    }\n    canRequestAlliance(player: Player): boolean {\n        return player.isCombatant() && !player.isAi;\n    }\n    canFormAlliance(player1: Player, player2: Player): boolean {\n        const hostilePairs = this.getHostilePlayers();\n        if (hostilePairs.filter(pair => pair.has(player1) && !pair.has(player2)).length === 0) {\n            return false;\n        }\n        if (hostilePairs.filter(pair => pair.has(player2) && !pair.has(player1)).length === 0) {\n            return false;\n        }\n        const newPair = new PlayerPair(player1, player2);\n        return hostilePairs.filter(pair => !pair.equals(newPair)).length > 0;\n    }\n    getHostilePlayers(): PlayerPair[] {\n        const combatants = this.playerList.getCombatants();\n        const hostilePairs: PlayerPair[] = [];\n        for (let i = 0; i < combatants.length; i++) {\n            for (let j = i + 1; j < combatants.length; j++) {\n                if (!this.getAllies(combatants[i]).includes(combatants[j])) {\n                    hostilePairs.push(new PlayerPair(combatants[i], combatants[j]));\n                }\n            }\n        }\n        return hostilePairs;\n    }\n    getHash(): number {\n        return fnv32a(this.alliances\n            .map(alliance => [\n            this.playerList.getPlayerNumber(alliance.players.first),\n            this.playerList.getPlayerNumber(alliance.players.second),\n            alliance.status\n        ])\n            .flat());\n    }\n    debugGetState(): Array<{\n        first: Player;\n        second: Player;\n        status: AllianceStatus;\n    }> {\n        return this.alliances.map(alliance => ({\n            first: alliance.players.first,\n            second: alliance.players.second,\n            status: alliance.status\n        }));\n    }\n}\n"
  },
  {
    "path": "src/game/AttackerInfo.ts",
    "content": "export class AttackerInfo {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/game/BotManager.ts",
    "content": "import { CompositeDisposable } from '../util/disposable/CompositeDisposable';\nimport { AppLogger } from '@/util/logger';\nimport { ActionQueue } from './action/ActionQueue';\nimport { ActionsApi } from './api/ActionsApi';\nimport { EventsApi } from './api/EventsApi';\nimport { GameApi } from './api/GameApi';\nimport { LoggerApi } from './api/LoggerApi';\nimport { ProductionApi } from './api/ProductionApi';\n\nconst logger = AppLogger.get('BotManager');\n\nexport class BotManager {\n    private actionFactory: any;\n    private actionQueue: ActionQueue;\n    private botFactory: any;\n    private botDebugIndex: any;\n    private actionLogger: any;\n    private bots: Map<any, any>;\n    private disposables: CompositeDisposable;\n    private gameApi?: GameApi;\n    static factory(actionFactory: any, botFactory: any, botDebugIndex: any, actionLogger: any): BotManager {\n        return new this(actionFactory, new ActionQueue(), botFactory, botDebugIndex, actionLogger);\n    }\n    constructor(actionFactory: any, actionQueue: ActionQueue, botFactory: any, botDebugIndex: any, actionLogger: any) {\n        this.actionFactory = actionFactory;\n        this.actionQueue = actionQueue;\n        this.botFactory = botFactory;\n        this.botDebugIndex = botDebugIndex;\n        this.actionLogger = actionLogger;\n        this.bots = new Map();\n        this.disposables = new CompositeDisposable();\n    }\n    init(game: any): void {\n        this.gameApi = new GameApi(game, true);\n        const eventsApi = new EventsApi(game.events);\n        const aiCombatants = game.getCombatants().filter((c: any) => c.isAi);\n        logger.info(`[BotManager] Initializing ${aiCombatants.length} AI player(s)`);\n        for (const combatant of aiCombatants) {\n            try {\n                const bot = this.botFactory.create(combatant);\n                this.bots.set(combatant, bot);\n                logger.info(`[BotManager] Created bot \"${bot.name}\" (${bot.constructor.name}) for country \"${combatant.country?.name ?? '?'}\"`);\n            } catch (e) {\n                logger.error(`[BotManager] Failed to create bot for \"${combatant.name}\":`, e);\n            }\n        }\n        this.updateDebugBotIndex(this.botDebugIndex.value, game);\n        const debugIndexHandler = (index: number) => this.updateDebugBotIndex(index, game);\n        this.botDebugIndex.onChange.subscribe(debugIndexHandler);\n        this.disposables.add(() => this.botDebugIndex.onChange.unsubscribe(debugIndexHandler));\n        eventsApi.subscribe((event: any) => {\n            this.bots.forEach(bot => {\n                try {\n                    bot.onGameEvent(event, this.gameApi);\n                } catch (e) {\n                    logger.error(`[BotManager] Bot \"${bot.name}\" onGameEvent error:`, e);\n                }\n            });\n        });\n        this.disposables.add(eventsApi);\n        for (const bot of this.bots.values()) {\n            try {\n                const player = game.getPlayerByName(bot.name);\n                if (!player) {\n                    logger.error(`[BotManager] Player \"${bot.name}\" not found in game`);\n                    continue;\n                }\n                if (!player.production) {\n                    logger.error(`[BotManager] Player \"${bot.name}\" has no production system`);\n                    continue;\n                }\n                bot.setGameApi(this.gameApi);\n                bot.setActionsApi(new ActionsApi(game, this.actionFactory, this.actionQueue, bot));\n                bot.setProductionApi(new ProductionApi(player.production));\n                bot.setLogger(new LoggerApi(AppLogger.get(bot.name) as any, this.gameApi));\n                logger.info(`[BotManager] APIs set for bot \"${bot.name}\", calling onGameStart...`);\n                bot.onGameStart(this.gameApi);\n                logger.info(`[BotManager] Bot \"${bot.name}\" onGameStart completed successfully`);\n            } catch (e) {\n                logger.error(`[BotManager] Bot \"${bot.name}\" initialization failed:`, e);\n            }\n        }\n        logger.info(`[BotManager] Initialization complete. ${this.bots.size} bot(s) active.`);\n    }\n    update(gameState: any): void {\n        for (const action of this.actionQueue.dequeueAll()) {\n            try {\n                (action as any).process();\n                const actionLog = (action as any).print();\n                if (actionLog) {\n                    this.actionLogger?.debug?.(`(${action.player.name})@${gameState.currentTick}: ${actionLog}`);\n                }\n            } catch (e) {\n                logger.error(`[BotManager] Action process error @tick ${gameState.currentTick}:`, e);\n            }\n        }\n        for (const combatant of gameState.getCombatants().filter((c: any) => c.isAi)) {\n            const bot = this.bots.get(combatant);\n            if (!bot) {\n                continue;\n            }\n            try {\n                bot.onGameTick(this.gameApi);\n            } catch (e) {\n                if (gameState.currentTick % 150 === 0) {\n                    logger.error(`[BotManager] Bot \"${bot.name}\" onGameTick error @tick ${gameState.currentTick}:`, e);\n                }\n            }\n        }\n    }\n    private updateDebugBotIndex(index: number, game: any): void {\n        const debugBotName = index > 0 ? game.getAiPlayerName(index) : undefined;\n        for (const bot of this.bots.values()) {\n            bot.setDebugMode(bot.name === debugBotName);\n        }\n    }\n    dispose(): void {\n        this.gameApi = undefined;\n        this.bots.clear();\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/game/Building.ts",
    "content": "export { Building } from '@/game/gameobject/Building';\n"
  },
  {
    "path": "src/game/ConstructionWorker.ts",
    "content": "import { rectIntersect } from '@/util/geometry';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { SpeedType } from '@/game/type/SpeedType';\nimport { PackBuildingTask } from '@/game/gameobject/task/morph/PackBuildingTask';\nimport { CallbackTask } from '@/game/gameobject/task/system/CallbackTask';\nimport { TaskGroup } from '@/game/gameobject/task/system/TaskGroup';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { EventType } from '@/game/event/EventType';\nimport { NotifyTick } from '@/game/gameobject/trait/interface/NotifyTick';\ninterface PlacementOptions {\n    normalizedTile?: boolean;\n    ignoreObjects?: any[];\n    ignoreAdjacent?: boolean;\n}\ninterface PlacementPreviewTile {\n    rx: number;\n    ry: number;\n    buildable: boolean;\n}\ninterface Rect {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface Tile {\n    rx: number;\n    ry: number;\n    landType: any;\n    rampType: number;\n}\ninterface Building {\n    isBuilding(): boolean;\n    rules: any;\n    art: any;\n    tile: Tile;\n    name: string;\n    owner: any;\n    unitOrderTrait: any;\n    purchaseValue?: number;\n}\ninterface Player {\n    buildings: Building[];\n}\ninterface Game {\n    gameOpts: {\n        buildOffAlly: boolean;\n    };\n    alliances: {\n        getAllies(player: Player): Player[];\n    };\n    events: {\n        subscribe(eventType: any, callback: Function): any;\n    };\n    createObject(type: ObjectType, name: string): Building;\n    changeObjectOwner(object: Building, player: Player): void;\n    spawnObject(object: Building, tile: Tile): void;\n    unspawnObject(object: Building): void;\n    sellTrait: {\n        computePurchaseValue(rules: any, player: Player): number;\n    };\n    mapShroudTrait: {\n        getPlayerShroud(player: Player): {\n            isShrouded(tile: Tile): boolean;\n        } | null;\n    };\n}\ninterface GameMap {\n    tiles: {\n        getByMapCoords(x: number, y: number): Tile | null;\n    };\n    tileOccupation: {\n        onChange: {\n            subscribe(callback: Function): void;\n            unsubscribe(callback: Function): void;\n        };\n        calculateTilesForGameObject(tile: Tile, object: Building): Tile[];\n    };\n    isWithinBounds(tile: Tile): boolean;\n    getObjectsOnTile(tile: Tile): any[];\n    getGroundObjectsOnTile(tile: Tile): any[];\n}\ninterface Rules {\n    getBuilding(name: string): any;\n    getLandRules(landType: any): {\n        buildable: boolean;\n        getSpeedModifier(speedType: SpeedType): number;\n    };\n}\ninterface Art {\n    getObject(name: string, type: ObjectType): {\n        foundation: {\n            width: number;\n            height: number;\n        };\n        foundationCenter: {\n            x: number;\n            y: number;\n        };\n    };\n}\nexport class ConstructionWorker {\n    private player: Player;\n    private rules: Rules;\n    private art: Art;\n    private map: GameMap;\n    private game: Game;\n    private adjacencyMaps: Map<number, Rect[]>;\n    private disposables: CompositeDisposable;\n    constructor(player: Player, rules: Rules, art: Art, map: GameMap, game: Game) {\n        this.player = player;\n        this.rules = rules;\n        this.art = art;\n        this.map = map;\n        this.game = game;\n        this.adjacencyMaps = new Map();\n        this.disposables = new CompositeDisposable();\n        const onTileOccupationChange = ({ object }: {\n            object: any;\n        }) => {\n            if (object.isBuilding()) {\n                this.adjacencyMaps.clear();\n            }\n        };\n        this.map.tileOccupation.onChange.subscribe(onTileOccupationChange);\n        this.disposables.add(() => this.map.tileOccupation.onChange.unsubscribe(onTileOccupationChange));\n        this.disposables.add(this.game.events.subscribe(EventType.AllianceChange, () => this.adjacencyMaps.clear()), this.game.events.subscribe(EventType.ObjectOwnerChange, (event: any) => {\n            if (event.target.isBuilding()) {\n                this.adjacencyMaps.clear();\n            }\n        }), this.game.events.subscribe(EventType.ObjectDestroy, (event: any) => {\n            if (event.target.isBuilding() &&\n                event.target.rules.leaveRubble) {\n                this.adjacencyMaps.clear();\n            }\n        }));\n    }\n    private getAdjacentRect(tile: Tile, foundation: any, adjacentRange: number): Rect {\n        return {\n            x: tile.rx - adjacentRange,\n            y: tile.ry - adjacentRange,\n            width: foundation.width + 2 * adjacentRange,\n            height: foundation.height + 2 * adjacentRange,\n        };\n    }\n    private getAdjacencyMap(adjacentRange: number): Rect[] {\n        const adjacentRects: Rect[] = [];\n        const buildings = [\n            ...this.player.buildings,\n            ...(this.game.gameOpts.buildOffAlly\n                ? this.game.alliances\n                    .getAllies(this.player)\n                    .map((ally) => [...ally.buildings].filter((building) => building.rules.eligibileForAllyBuilding))\n                    .flat()\n                : []),\n        ];\n        for (const building of buildings) {\n            if (building.rules.baseNormal) {\n                adjacentRects.push(this.getAdjacentRect(building.tile, building.art.foundation, adjacentRange));\n            }\n        }\n        return adjacentRects;\n    }\n    private meetsAdjacency(rect: Rect, adjacentRange: number): boolean {\n        let adjacencyMap = this.adjacencyMaps.get(adjacentRange);\n        if (!adjacencyMap) {\n            adjacencyMap = this.getAdjacencyMap(adjacentRange);\n            this.adjacencyMaps.set(adjacentRange, adjacencyMap);\n        }\n        for (const adjacentRect of adjacencyMap) {\n            if (rectIntersect(rect, adjacentRect)) {\n                return true;\n            }\n        }\n        return false;\n    }\n    getPlacementPreview(buildingName: string, targetTile: Tile, options: PlacementOptions = {}): PlacementPreviewTile[] {\n        const { normalizedTile = false, ignoreObjects, ignoreAdjacent = false, } = options;\n        const buildingRules = this.rules.getBuilding(buildingName);\n        const buildingArt = this.art.getObject(buildingName, ObjectType.Building);\n        const previewTiles: PlacementPreviewTile[] = [];\n        const foundation = buildingArt.foundation;\n        const placementTile = normalizedTile\n            ? targetTile\n            : this.normalizePlacementTileCoords(buildingArt, targetTile);\n        let canPlace = true;\n        const buildingRect = {\n            x: placementTile.rx,\n            y: placementTile.ry,\n            width: foundation.width,\n            height: foundation.height,\n        };\n        if (!buildingRules.constructionYard &&\n            !ignoreAdjacent &&\n            !this.meetsAdjacency(buildingRect, buildingRules.adjacent)) {\n            canPlace = false;\n        }\n        for (let x = 0; x < foundation.width; x++) {\n            for (let y = 0; y < foundation.height; y++) {\n                const tileCoords = { x: placementTile.rx + x, y: placementTile.ry + y };\n                const tile = this.map.tiles.getByMapCoords(tileCoords.x, tileCoords.y);\n                if (tile) {\n                    previewTiles.push({\n                        rx: tileCoords.x,\n                        ry: tileCoords.y,\n                        buildable: canPlace && this.isTileBuildable(tile, buildingRules, ignoreObjects),\n                    });\n                }\n            }\n        }\n        if (buildingRules.wall && previewTiles[0]?.buildable) {\n            const connectingTiles = this.getWallConnectingTiles(placementTile, buildingRules);\n            connectingTiles.forEach((tile) => {\n                previewTiles.push({ rx: tile.rx, ry: tile.ry, buildable: true });\n            });\n        }\n        return previewTiles;\n    }\n    canPlaceAt(buildingName: string, targetTile: Tile, options: PlacementOptions = {}): boolean {\n        const { normalizedTile = false, ignoreObjects, ignoreAdjacent = false, } = options;\n        const buildingRules = this.rules.getBuilding(buildingName);\n        const buildingArt = this.art.getObject(buildingName, ObjectType.Building);\n        const foundation = buildingArt.foundation;\n        const placementTile = normalizedTile\n            ? targetTile\n            : this.normalizePlacementTileCoords(buildingArt, targetTile);\n        const buildingRect = {\n            x: placementTile.rx,\n            y: placementTile.ry,\n            width: foundation.width,\n            height: foundation.height,\n        };\n        if (!buildingRules.constructionYard &&\n            !ignoreAdjacent &&\n            !this.meetsAdjacency(buildingRect, buildingRules.adjacent)) {\n            return false;\n        }\n        for (let x = 0; x < foundation.width; x++) {\n            for (let y = 0; y < foundation.height; y++) {\n                const tileCoords = { x: placementTile.rx + x, y: placementTile.ry + y };\n                const tile = this.map.tiles.getByMapCoords(tileCoords.x, tileCoords.y);\n                if (!tile || !this.isTileBuildable(tile, buildingRules, ignoreObjects)) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n    placeAt(buildingName: string, targetTile: Tile, isNormalized: boolean = false): Building[] {\n        const placedBuildings: Building[] = [];\n        const buildingRules = this.rules.getBuilding(buildingName);\n        const placementTile = isNormalized\n            ? targetTile\n            : this.normalizePlacementTile(buildingName, targetTile);\n        if (buildingRules.wall) {\n            const wallPlacements: [\n                Tile,\n                any\n            ][] = [[placementTile, buildingRules]];\n            const connectingTiles = this.getWallConnectingTiles(placementTile, buildingRules);\n            connectingTiles.forEach((tile) => {\n                if (tile !== placementTile) {\n                    wallPlacements.push([tile, buildingRules]);\n                }\n            });\n            for (const [tile, rules] of wallPlacements) {\n                placedBuildings.push(this.executePlacement(tile, rules));\n            }\n        }\n        else {\n            const building = this.executePlacement(placementTile, buildingRules);\n            placedBuildings.push(building);\n            const occupiedTiles = this.map.tileOccupation.calculateTilesForGameObject(placementTile, building);\n            for (const tile of occupiedTiles) {\n                const smudge = this.map\n                    .getObjectsOnTile(tile)\n                    .find((obj) => obj.isSmudge());\n                if (smudge) {\n                    this.game.unspawnObject(smudge);\n                }\n            }\n        }\n        return placedBuildings;\n    }\n    private normalizePlacementTileCoords(buildingArt: any, targetTile: Tile): Tile {\n        const foundationCenter = buildingArt.foundationCenter;\n        return {\n            rx: targetTile.rx - foundationCenter.x,\n            ry: targetTile.ry - foundationCenter.y,\n        } as Tile;\n    }\n    private normalizePlacementTile(buildingName: string, targetTile: Tile): Tile {\n        const buildingArt = this.art.getObject(buildingName, ObjectType.Building);\n        const normalizedCoords = this.normalizePlacementTileCoords(buildingArt, targetTile);\n        const tile = this.map.tiles.getByMapCoords(normalizedCoords.rx, normalizedCoords.ry);\n        if (!tile) {\n            throw new Error(`Can't build outside map (${normalizedCoords.rx}, ${normalizedCoords.ry})`);\n        }\n        return tile;\n    }\n    unplace(building: Building, callback: () => void): void {\n        building.unitOrderTrait.cancelAllTasks();\n        building.unitOrderTrait.addTasks(new TaskGroup(new PackBuildingTask(this.game), new CallbackTask(() => {\n            this.game.unspawnObject(building);\n            callback();\n        })).setCancellable(false));\n        building.unitOrderTrait[NotifyTick.onTick](building, this.game);\n    }\n    private executePlacement(tile: Tile, buildingRules: any): Building {\n        const building = this.game.createObject(ObjectType.Building, buildingRules.name);\n        this.game.changeObjectOwner(building, this.player);\n        building.purchaseValue = this.game.sellTrait.computePurchaseValue(buildingRules, this.player);\n        this.game.spawnObject(building, tile);\n        return building;\n    }\n    private getWallConnectingTiles(placementTile: Tile, buildingRules: any): Tile[] {\n        const guardRange = buildingRules.guardRange + 1;\n        const connectingTiles: Tile[] = [];\n        const directions = [\n            [0, 1],\n            [0, -1],\n            [1, 0],\n            [-1, 0],\n        ];\n        for (const direction of directions) {\n            const tilesInDirection: Tile[] = [];\n            for (let distance = 0; distance < guardRange; ++distance) {\n                const coords = {\n                    x: placementTile.rx + direction[0] * distance,\n                    y: placementTile.ry + direction[1] * distance,\n                };\n                const tile = this.map.tiles.getByMapCoords(coords.x, coords.y);\n                if (!tile)\n                    break;\n                const existingWall = this.map\n                    .getObjectsOnTile(tile)\n                    .find((obj) => obj.isBuilding() &&\n                    obj.name === buildingRules.name &&\n                    obj.owner === this.player);\n                if (existingWall) {\n                    connectingTiles.push(...tilesInDirection);\n                    break;\n                }\n                if (!this.isTileBuildable(tile, buildingRules))\n                    break;\n                tilesInDirection.push(tile);\n            }\n        }\n        return connectingTiles;\n    }\n    private isTileBuildable(tile: Tile, buildingRules: any, ignoreObjects?: any[]): boolean {\n        if (!this.map.isWithinBounds(tile)) {\n            return false;\n        }\n        const playerShroud = this.game.mapShroudTrait.getPlayerShroud(this.player);\n        if (playerShroud?.isShrouded(tile)) {\n            return false;\n        }\n        const groundObjects = this.map.getGroundObjectsOnTile(tile);\n        const hasBlockingObject = groundObjects.some((obj) => {\n            if (ignoreObjects?.includes(obj))\n                return false;\n            if (obj.isBuilding() && obj.rules.invisibleInGame)\n                return false;\n            if (obj.isSmudge())\n                return false;\n            return true;\n        });\n        if (hasBlockingObject) {\n            return false;\n        }\n        if (buildingRules.waterBound) {\n            const landRules = this.rules.getLandRules(tile.landType);\n            return landRules.getSpeedModifier(SpeedType.Float) > 0;\n        }\n        else {\n            const landRules = this.rules.getLandRules(tile.landType);\n            return tile.rampType === 0 && landRules.buildable;\n        }\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/game/Coords.ts",
    "content": "import { GameMath } from './math/GameMath';\nimport { Vector2 } from './math/Vector2';\nimport { Vector3 } from './math/Vector3';\nexport class Coords {\n    static readonly ISO_TILE_SIZE = 30;\n    static readonly LEPTONS_PER_TILE = 256;\n    static readonly ISO_WORLD_SCALE = Coords.LEPTONS_PER_TILE / Coords.ISO_TILE_SIZE;\n    static readonly ISO_CAMERA_ALPHA = Math.PI / 6;\n    static readonly ISO_CAMERA_BETA = Math.PI / 4;\n    static readonly COS_ISO_CAMERA_BETA = GameMath.cos(Coords.ISO_CAMERA_BETA);\n    static readonly zScale = Coords.COS_ISO_CAMERA_BETA / GameMath.cos(Coords.ISO_CAMERA_ALPHA);\n    static tileToWorld(x: number, y: number): {\n        x: number;\n        y: number;\n    } {\n        return { x: x * Coords.LEPTONS_PER_TILE, y: y * Coords.LEPTONS_PER_TILE };\n    }\n    static vecWorldToGround(vec: Vector3): Vector2 {\n        return new Vector2(vec.x, vec.z);\n    }\n    static vecGroundToWorld(vec: Vector2): Vector3 {\n        return new Vector3(vec.x, 0, vec.y);\n    }\n    static tileHeightToWorld(height: number): number {\n        return height * (Coords.LEPTONS_PER_TILE / 2) * Coords.zScale;\n    }\n    static worldToTileHeight(height: number): number {\n        return height / ((Coords.LEPTONS_PER_TILE / 2) * Coords.zScale);\n    }\n    static tile3dToWorld(x: number, y: number, height: number): Vector3 {\n        const world = Coords.tileToWorld(x, y);\n        const z = Coords.tileHeightToWorld(height);\n        return new Vector3(world.x, z, world.y);\n    }\n    static screenDistanceToWorld(x: number, y: number): {\n        x: number;\n        y: number;\n    } {\n        return {\n            x: Math.floor(((x + 2 * y) / 2) * Coords.ISO_WORLD_SCALE),\n            y: Math.floor(((2 * y - x) / 2) * Coords.ISO_WORLD_SCALE),\n        };\n    }\n    static getWorldTileSize(): number {\n        return Coords.LEPTONS_PER_TILE;\n    }\n}\n"
  },
  {
    "path": "src/game/CountdownTimer.ts",
    "content": "import { TimerExpireEvent } from './event/TimerExpireEvent';\nimport { GameSpeed } from './GameSpeed';\nexport class CountdownTimer {\n    private ticks: number = 0;\n    private running: boolean = false;\n    getSeconds(): number {\n        return Math.floor(this.ticks / GameSpeed.BASE_TICKS_PER_SECOND);\n    }\n    setSeconds(seconds: number): void {\n        this.ticks = Math.max(0, Math.floor(GameSpeed.BASE_TICKS_PER_SECOND * seconds));\n    }\n    addSeconds(seconds: number): void {\n        this.ticks = Math.max(0, this.ticks + Math.floor(GameSpeed.BASE_TICKS_PER_SECOND * seconds));\n    }\n    start(): void {\n        this.running = true;\n    }\n    stop(): void {\n        this.running = false;\n    }\n    isRunning(): boolean {\n        return this.running;\n    }\n    update(game: {\n        events: {\n            dispatch: (event: TimerExpireEvent) => void;\n        };\n    }): void {\n        if (this.running) {\n            if (this.ticks > 0) {\n                this.ticks--;\n            }\n            else {\n                this.running = false;\n                game.events.dispatch(new TimerExpireEvent(this));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/Country.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\ninterface CountryRules {\n    id: string;\n    side: string;\n    name: string;\n    multiplay: boolean;\n    multiplayPassive: boolean;\n    veteranAircraft: string[];\n    veteranInfantry: string[];\n    veteranUnits: string[];\n    getCountry(id: string): CountryRules;\n}\nexport class Country {\n    private rules: CountryRules;\n    static factory(id: string, rules: CountryRules): Country {\n        return new this(rules.getCountry(id));\n    }\n    constructor(rules: CountryRules) {\n        this.rules = rules;\n    }\n    get id(): string {\n        return this.rules.id;\n    }\n    get side(): string {\n        return this.rules.side;\n    }\n    get name(): string {\n        return this.rules.name;\n    }\n    isPlayable(): boolean {\n        return this.rules.multiplay && !this.rules.multiplayPassive;\n    }\n    hasVeteranUnit(type: ObjectType, name: string): boolean {\n        let veteranUnits: string[];\n        switch (type) {\n            case ObjectType.Aircraft:\n                veteranUnits = this.rules.veteranAircraft;\n                break;\n            case ObjectType.Infantry:\n                veteranUnits = this.rules.veteranInfantry;\n                break;\n            case ObjectType.Vehicle:\n                veteranUnits = this.rules.veteranUnits;\n                break;\n            default:\n                throw new Error(`Unsupported object type \"${ObjectType[type]}\"`);\n        }\n        return veteranUnits.includes(name);\n    }\n}\n"
  },
  {
    "path": "src/game/Game.ts",
    "content": "import { ConstructionWorker } from \"./ConstructionWorker\";\nimport { GameOpts, isHumanPlayerInfo } from \"./gameopts/GameOpts\";\nimport { ObjectType } from \"../engine/type/ObjectType\";\nimport { EventDispatcher } from \"../util/event\";\nimport { OreSpread } from \"./map/OreSpread\";\nimport { Infantry } from \"./gameobject/Infantry\";\nimport { AllianceStatus } from \"./Alliances\";\nimport { BoxedVar } from \"../util/BoxedVar\";\nimport { StartingUnitsGenerator } from \"./StartingUnitsGenerator\";\nimport { CardinalTileFinder } from \"./map/tileFinder/CardinalTileFinder\";\nimport { SpeedType } from \"./type/SpeedType\";\nimport { Target } from \"./Target\";\nimport { BridgeOverlayTypes } from \"./map/BridgeOverlayTypes\";\nimport { fnv32a, isBetween } from \"../util/math\";\nimport { GameEventBus } from \"./GameEventBus\";\nimport { ObjectDestroyEvent } from \"./event/ObjectDestroyEvent\";\nimport { PlayerDefeatedEvent } from \"./event/PlayerDefeatedEvent\";\nimport { GameModeType } from \"./ini/GameModeType\";\nimport { Traits } from \"./Traits\";\nimport { NotifyTick } from \"./trait/interface/NotifyTick\";\nimport { NotifyDestroy } from \"./trait/interface/NotifyDestroy\";\nimport { NotifySpawn } from \"./trait/interface/NotifySpawn\";\nimport { NotifyUnspawn } from \"./trait/interface/NotifyUnspawn\";\nimport { NotifyOwnerChange } from \"./trait/interface/NotifyOwnerChange\";\nimport { ObjectOwnerChangeEvent } from \"./event/ObjectOwnerChangeEvent\";\nimport { ObjectUnspawnEvent } from \"./event/ObjectUnspawnEvent\";\nimport { NotifyTargetDestroy } from \"./trait/interface/NotifyTargetDestroy\";\nimport { VeteranLevel } from \"./gameobject/unit/VeteranLevel\";\nimport { ObjectSpawnEvent } from \"./event/ObjectSpawnEvent\";\nimport { OverlayTibType } from \"../engine/type/OverlayTibType\";\nimport { OreOverlayTypes } from \"./map/OreOverlayTypes\";\nimport { Weapon } from \"./Weapon\";\nimport { GameSpeed } from \"./GameSpeed\";\nimport { DeathType } from \"./gameobject/common/DeathType\";\nimport { BridgeHeadType } from \"./map/Bridges\";\nimport { SuperWeapon } from \"./SuperWeapon\";\nimport { AllianceChangeEvent, AllianceEventType } from \"./event/AllianceChangeEvent\";\nimport { NotifyAllianceChange } from \"./trait/interface/NotifyAllianceChange\";\nimport { OBS_COUNTRY_ID } from \"./gameopts/constants\";\nimport { getZoneType } from \"./gameobject/unit/ZoneType\";\nimport { Prng } from \"./Prng\";\nimport { TriggerManager } from \"./trigger/TriggerManager\";\nimport { CountdownTimer } from \"./CountdownTimer\";\nimport { WeaponType } from \"./WeaponType\";\nimport { Warhead } from \"./Warhead\";\nimport { NotifyObjectTraitAdd } from \"./trait/interface/NotifyObjectTraitAdd\";\nimport { RadarOnOffEvent } from \"./event/RadarOnOffEvent\";\nexport enum GameStatus {\n    NotStarted = 0,\n    Started = 1,\n    Ended = 2\n}\nexport class Game {\n    public updatableObjects = new Set<any>();\n    public constructionWorkers = new Map<any, ConstructionWorker>();\n    public currentTick = 0;\n    public currentTime = 0;\n    public countdownTimer = new CountdownTimer();\n    public _onEnd = new EventDispatcher<Game, void>();\n    public afterTickCallbacks: Array<() => void> = [];\n    public events = new GameEventBus();\n    public traits = new Traits();\n    public debugText = new BoxedVar(\"\");\n    public world: any;\n    public map: any;\n    public rules: any;\n    public art: any;\n    public ai: any;\n    public id: any;\n    public startTimestamp: any;\n    public prng: any;\n    public gameOpts: any;\n    public gameModeType: any;\n    public playerList: any;\n    public unitSelection: any;\n    public alliances: any;\n    public desiredSpeed: BoxedVar<number>;\n    public speed: BoxedVar<number>;\n    public nextObjectId: any;\n    public objectFactory: any;\n    public botManager: any;\n    public triggers = new TriggerManager();\n    public localPlayer: any;\n    public mapShroudTrait: any;\n    public crateGeneratorTrait: any;\n    public status: GameStatus;\n    public lastGameEndCheck: number | undefined;\n    public sellTrait: any;\n    public stalemateDetectTrait: any;\n    get onEnd() {\n        return this._onEnd.asEvent();\n    }\n    constructor(world: any, map: any, rules: any, art: any, ai: any, id: any, startTimestamp: any, gameOpts: any, gameModeType: any, playerList: any, unitSelection: any, alliances: any, nextObjectId: any, objectFactory: any, botManager: any) {\n        this.world = world;\n        this.map = map;\n        this.rules = rules;\n        this.art = art;\n        this.ai = ai;\n        this.id = id;\n        this.startTimestamp = startTimestamp;\n        this.prng = Prng.factory(id, startTimestamp);\n        this.gameOpts = gameOpts;\n        this.gameModeType = gameModeType;\n        this.playerList = playerList;\n        this.unitSelection = unitSelection;\n        this.alliances = alliances;\n        this.desiredSpeed = new BoxedVar(GameSpeed.computeGameSpeed(gameOpts.gameSpeed));\n        this.speed = new BoxedVar(this.desiredSpeed.value);\n        this.nextObjectId = nextObjectId;\n        this.objectFactory = objectFactory;\n        this.botManager = botManager;\n    }\n    addPlayer(player: any) {\n        this.playerList.addPlayer(player);\n        this.constructionWorkers.set(player, this.createConstructionWorker(player));\n    }\n    getPlayer(index: number) {\n        return this.playerList.getPlayerAt(index);\n    }\n    getPlayerByName(name: string) {\n        return this.playerList.getPlayerByName(name);\n    }\n    getAiPlayerName(aiPlayer: any) {\n        let index: number;\n        index = typeof aiPlayer === \"number\" ? aiPlayer : this.gameOpts.aiPlayers.indexOf(aiPlayer);\n        return `@@AI${index + 1}@@`;\n    }\n    getPlayerNumber(player: any) {\n        return this.playerList.getPlayerNumber(player);\n    }\n    getCombatants() {\n        return this.playerList.getCombatants();\n    }\n    getCivilianPlayer() {\n        return this.playerList.getCivilian();\n    }\n    getAllPlayers() {\n        return this.playerList.getAll();\n    }\n    getNonNeutralPlayers() {\n        return this.playerList.getNonNeutral();\n    }\n    areFriendly(obj1: any, obj2: any) {\n        return obj1.owner === obj2.owner || this.alliances.areAllied(obj1.owner, obj2.owner);\n    }\n    getWorld() {\n        return this.world;\n    }\n    createConstructionWorker(player: any) {\n        return new ConstructionWorker(player, this.rules, this.art, this.map, this);\n    }\n    getConstructionWorker(player: any) {\n        const worker = this.constructionWorkers.get(player);\n        if (!worker) {\n            throw new Error(`No construction worker found for player \"${player.name}\"`);\n        }\n        return worker;\n    }\n    getUnitSelection() {\n        return this.unitSelection;\n    }\n    init(localPlayer: any) {\n        this.localPlayer = localPlayer;\n        this.createMapObjects();\n        this.createPlayerInitialUnits();\n        this.map.terrain.computeAllPassabilityGraphs();\n        this.mapShroudTrait.init(this);\n        this.crateGeneratorTrait.init(this);\n        this.playerList.getAll().forEach((player: any) => (player.credits = this.gameOpts.credits));\n        if (this.rules.mpDialogSettings.alliesAllowed) {\n            this.createInitialTeams();\n        }\n    }\n    start() {\n        this.status = GameStatus.Started;\n        this.currentTick = 0;\n        this.currentTime = 0;\n        this.botManager.init(this);\n        this.triggers.init(this);\n    }\n    createInitialTeams() {\n        for (let teamId = 0; teamId < this.gameOpts.maxSlots; teamId++) {\n            const teamMembers = [...this.gameOpts.humanPlayers, ...this.gameOpts.aiPlayers]\n                .filter((player: any) => player?.teamId === teamId && player.countryId !== OBS_COUNTRY_ID)\n                .map((player: any) => isHumanPlayerInfo(player) ? player.name : this.getAiPlayerName(player));\n            if (teamMembers.length > 1) {\n                for (let i = 0; i < teamMembers.length - 1; i++) {\n                    for (let j = i + 1; j < teamMembers.length; j++) {\n                        const player1 = this.getPlayerByName(teamMembers[i]);\n                        const player2 = this.getPlayerByName(teamMembers[j]);\n                        const alliance = this.alliances.setAlliance(player1, player2, AllianceStatus.Formed);\n                        this.onAllianceChange(alliance, player1, true);\n                    }\n                }\n            }\n        }\n    }\n    createMapObjects() {\n        const noHarvesters = this.rules.general.harvesterUnit.every((unitName: string) => !isBetween(this.rules.getObject(unitName, ObjectType.Vehicle).techLevel, 0, this.rules.mpDialogSettings.techLevel));\n        const mapObjects = this.map.getInitialMapObjects();\n        this.createInitialMapTerrains(mapObjects.terrains, noHarvesters);\n        this.createInitialMapOverlays(mapObjects.overlays, noHarvesters);\n        this.createInitialMapSmudges(mapObjects.smudges);\n        this.createInitialMapTechnos(mapObjects.technos);\n    }\n    createInitialMapTerrains(terrains: any[], noHarvesters: boolean) {\n        for (const terrain of terrains) {\n            const name = terrain.name;\n            if (!this.validateMapObjectRulesAndArt(name, ObjectType.Terrain)) {\n                continue;\n            }\n            const tile = this.map.tiles.getByMapCoords(terrain.rx, terrain.ry);\n            if (!tile) {\n                console.warn(`Invalid map object location (${terrain.rx},${terrain.ry})`, terrain);\n                continue;\n            }\n            const terrainRules = this.rules.getObject(name, ObjectType.Terrain);\n            if (noHarvesters && terrainRules.spawnsTiberium) {\n                continue;\n            }\n            const terrainObj = this.createObject(ObjectType.Terrain, name);\n            this.spawnObject(terrainObj, tile);\n        }\n    }\n    createInitialMapOverlays(overlays: any[], noHarvesters: boolean) {\n        const bridgeSegments = new Map<any, number>();\n        const bridgeObjects = new Map<any, any>();\n        for (const overlay of overlays) {\n            const overlayName = this.rules.getOverlayName(overlay.id);\n            if (!this.validateMapObjectRulesAndArt(overlayName, ObjectType.Overlay)) {\n                continue;\n            }\n            let overlayObj = this.createObject(ObjectType.Overlay, overlayName);\n            overlayObj.overlayId = overlay.id;\n            overlayObj.value = overlay.value;\n            let tileX = overlay.rx;\n            let tileY = overlay.ry;\n            if (overlayObj.isBridge() && overlayObj.isHighBridge()) {\n                overlayObj.position.tileElevation = 4;\n                tileX += overlayObj.isXBridge() ? 0 : -1;\n                tileY += overlayObj.isXBridge() ? -1 : 0;\n            }\n            const tile = this.map.tiles.getByMapCoords(tileX, tileY);\n            if (!tile) {\n                console.warn(`Invalid map object location (${tileX},${tileY})`, overlay);\n                overlayObj.dispose();\n                continue;\n            }\n            if (overlayObj.rules.tiberium) {\n                const tibType = OreOverlayTypes.getOverlayTibType(overlay.id);\n                const newOverlayId = OreSpread.calculateOverlayId(tibType, tile);\n                if (newOverlayId !== undefined && newOverlayId !== overlay.id) {\n                    overlayObj.dispose();\n                    overlayObj = this.createObject(ObjectType.Overlay, this.rules.getOverlayName(newOverlayId));\n                    overlayObj.overlayId = newOverlayId;\n                    overlayObj.value = overlay.value;\n                }\n            }\n            if (BridgeOverlayTypes.isLowBridge(overlay.id)) {\n                if (!BridgeOverlayTypes.isBridgePlaceholder(overlay.id)) {\n                    bridgeSegments.set(tile, overlay.value);\n                    if (overlay.value === 1) {\n                        bridgeObjects.set(tile, overlayObj);\n                    }\n                    else {\n                        overlayObj.dispose();\n                    }\n                }\n            }\n            else {\n                if (overlayObj.isTiberium()) {\n                    const tibType = OreOverlayTypes.getOverlayTibType(overlayObj.overlayId);\n                    if (![OverlayTibType.Ore, OverlayTibType.Gems, OverlayTibType.Vinifera].includes(tibType)) {\n                        console.warn(`Found unsupported TS tiberium overlay ${overlayObj.overlayId} @${tile.rx},${tile.ry}. Skipping.`);\n                        continue;\n                    }\n                    if (this.map.getObjectsOnTile(tile).find((obj: any) => obj.isTerrain())) {\n                        overlayObj.dispose();\n                        continue;\n                    }\n                }\n                if (noHarvesters && overlayObj.isTiberium()) {\n                    overlayObj.dispose();\n                }\n                else {\n                    this.spawnObject(overlayObj, tile);\n                }\n            }\n        }\n        for (const [tile, bridgeObj] of bridgeObjects) {\n            const isXBridge = bridgeObj.isXBridge();\n            const prevTile = this.map.tiles.getByMapCoords(tile.rx + (isXBridge ? 0 : -1), tile.ry + (isXBridge ? -1 : 0));\n            const nextTile = this.map.tiles.getByMapCoords(tile.rx + (isXBridge ? 0 : 1), tile.ry + (isXBridge ? 1 : 0));\n            if (prevTile && nextTile && (bridgeSegments.get(prevTile) === 0 || bridgeSegments.get(nextTile) === 2)) {\n                bridgeObj.value = 0;\n                this.spawnObject(bridgeObj, prevTile);\n            }\n            else {\n                bridgeObj.dispose();\n                console.warn(`Invalid bridge segment @${tile.rx},${tile.ry}. Skipping.`);\n            }\n        }\n        const lowBridgeHeadTiles = [...bridgeObjects.keys()].filter((tile: any) => this.map.bridges.getPieceAtTile(tile)?.headType !== BridgeHeadType.None);\n        const highBridgeHeadTiles = this.map.bridges.findMapHighBridgeHeadTiles();\n        const bridgeSpecs = this.map.bridges.findBridgeSpecsForHeadTiles([...lowBridgeHeadTiles, ...highBridgeHeadTiles]);\n        for (const spec of bridgeSpecs) {\n            for (const piece of this.map.bridges.findBridgePieces(spec)) {\n                piece.obj.bridgeTrait.bridgeSpec = spec;\n            }\n        }\n        const allBridgeTiles = bridgeSpecs\n            .map((spec: any) => this.map.bridges.findAllBridgeTiles(spec))\n            .flat();\n        const placeholderId = BridgeOverlayTypes.bridgePlaceholderIds[0];\n        const placeholderName = this.rules.getOverlayName(placeholderId);\n        for (const tile of allBridgeTiles) {\n            const placeholder = this.createObject(ObjectType.Overlay, placeholderName);\n            placeholder.overlayId = placeholderId;\n            this.spawnObject(placeholder, tile);\n        }\n    }\n    createInitialMapSmudges(smudges: any[]) {\n        for (const smudge of smudges) {\n            const name = smudge.name;\n            const tile = this.map.tiles.getByMapCoords(smudge.rx, smudge.ry);\n            if (!tile) {\n                console.warn(`Invalid map object location (${smudge.rx},${smudge.ry})`, smudge);\n                continue;\n            }\n            const smudgeObj = this.createObject(ObjectType.Smudge, name);\n            this.spawnObject(smudgeObj, tile);\n        }\n    }\n    createInitialMapTechnos(technos: any[]) {\n        const playersByCountry = new Map(this.playerList\n            .getAll()\n            .filter((player: any) => !!player.country)\n            .map((player: any) => [player.country.name, player]));\n        const tags = this.map.getTags();\n        for (const techno of technos) {\n            const name = techno.name;\n            if (!this.validateMapObjectRulesAndArt(name, techno.type)) {\n                continue;\n            }\n            const tile = this.map.tiles.getByMapCoords(techno.rx, techno.ry);\n            if (!tile) {\n                console.warn(`Invalid map object location (${techno.rx},${techno.ry})`, techno);\n                continue;\n            }\n            const owner = playersByCountry.get(techno.owner);\n            if (!owner) {\n                console.warn(`Invalid owner \"${techno.owner}\" for map object`, techno);\n                continue;\n            }\n            if (!(owner as any).isNeutral) {\n                continue;\n            }\n            const obj = this.createObject(techno.type, name);\n            if (techno.tag) {\n                obj.tag = tags.find((tag: any) => tag.id === techno.tag);\n            }\n            obj.healthTrait.health = (techno.health / 256) * 100;\n            let shouldDestroy = false;\n            if (!obj.healthTrait.health) {\n                if (!obj.isBuilding() || !obj.rules.leaveRubble) {\n                    obj.dispose();\n                    continue;\n                }\n                shouldDestroy = true;\n            }\n            if (techno.isInfantry() || techno.isVehicle() || techno.isAircraft()) {\n                obj.direction = ((-techno.direction / 256) * 360 + 360) % 360;\n                if (techno.isInfantry()) {\n                    obj.position.subCell = techno.subCell;\n                }\n                let onBridge = false;\n                if (techno.onBridge) {\n                    if (tile.onBridgeLandType === undefined) {\n                        console.warn(`Cannot place unit \"${techno.name}\" on a bridge because no bridge was found at ${tile.rx}, ${tile.ry}`);\n                    }\n                    else {\n                        onBridge = true;\n                    }\n                }\n                obj.onBridge = onBridge;\n                obj.zone = getZoneType(onBridge ? tile.onBridgeLandType : tile.landType);\n                if (onBridge) {\n                    obj.position.tileElevation += this.map.tileOccupation.getBridgeOnTile(tile)?.tileElevation ?? 0;\n                }\n                if (techno.veterancy) {\n                    obj.veteranTrait?.setRelativeXP(techno.veterancy);\n                }\n            }\n            else {\n                obj.poweredTrait?.setTurnedOn(techno.poweredOn);\n            }\n            this.changeObjectOwner(obj, owner);\n            this.spawnObject(obj, tile);\n            if (shouldDestroy) {\n                this.destroyObject(obj, undefined, true);\n            }\n        }\n    }\n    validateMapObjectRulesAndArt(name: string, type: ObjectType): boolean {\n        if (!this.rules.hasObject(name, type)) {\n            console.warn(`Map object '${name}' has no rules section. Skipping.`);\n            return false;\n        }\n        if (!this.art.hasObject(name, type)) {\n            console.warn(`Map object '${name}' has no art section. Skipping.`);\n            return false;\n        }\n        return true;\n    }\n    createPlayerInitialUnits() {\n        const countries = this.playerList.getCombatants().map((player: any) => player.country);\n        const availableUnits = [...this.rules.infantryRules.values(), ...this.rules.vehicleRules.values()].filter((unit: any) => unit.allowedToStartInMultiplayer &&\n            !unit.naval &&\n            unit.techLevel !== -1 &&\n            unit.techLevel <= this.rules.mpDialogSettings.techLevel &&\n            !this.rules.general.baseUnit.includes(unit.name) &&\n            countries.some((country: any) => unit.isAvailableTo(country) && unit.hasOwner(country)));\n        for (const player of this.playerList.getCombatants()) {\n            const startLoc = this.map.startingLocations[player.startLocation];\n            const startTile = this.map.tiles.getByMapCoords(startLoc.x, startLoc.y);\n            const mcvName = this.rules.general.baseUnit.find((unitName: string) => {\n                const unit = this.rules.getObject(unitName, ObjectType.Vehicle);\n                return unit.isAvailableTo(player.country) && unit.hasOwner(player.country);\n            });\n            if (!mcvName) {\n                throw new Error(\"No suitable MCV found for player country \" + player.country?.name);\n            }\n            const mcvRules = this.rules.getObject(mcvName, ObjectType.Vehicle);\n            const mcv = this.createUnitForPlayer(mcvRules, player);\n            this.spawnObject(mcv, startTile);\n            const startingUnits = StartingUnitsGenerator.generate(this.gameOpts.unitCount, [...this.rules.vehicleRules.keys()], availableUnits, player.country);\n            if (this.gameModeType === GameModeType.Unholy) {\n                startingUnits.push(...this.rules.general.baseUnit\n                    .filter((unitName: string) => unitName !== mcvName)\n                    .map((unitName: string) => ({\n                    name: unitName,\n                    type: ObjectType.Vehicle,\n                    count: 1,\n                })));\n            }\n            const spawnTiles: any[] = [];\n            let useSpawnTiles = false;\n            const tileFinder = new CardinalTileFinder(this.map.tiles, this.map.mapBounds, startTile, 4, 4, (tile: any) => !this.map\n                .getGroundObjectsOnTile(tile)\n                .find((obj: any) => !(obj.isSmudge() || (obj.isOverlay() && obj.isTiberium()))) &&\n                this.map.terrain.getPassableSpeed(tile, SpeedType.Foot, false, false) > 0);\n            const tileFinderMap = new Map<any, any>();\n            let tileIndex = 0;\n            for (const { name, type, count } of startingUnits) {\n                let remaining = count;\n                while (remaining > 0) {\n                    let tile;\n                    if (!useSpawnTiles) {\n                        tile = tileFinder.getNextTile();\n                        if (tile) {\n                            spawnTiles.push(tile);\n                        }\n                        else {\n                            useSpawnTiles = true;\n                        }\n                    }\n                    if (useSpawnTiles && spawnTiles.length) {\n                        const baseTile = spawnTiles[tileIndex];\n                        let finder = tileFinderMap.get(baseTile);\n                        if (!finder) {\n                            finder = new CardinalTileFinder(this.map.tiles, this.map.mapBounds, baseTile, 1, 0, (tile: any) => !this.map\n                                .getGroundObjectsOnTile(tile)\n                                .find((obj: any) => !(obj.isSmudge() || (obj.isOverlay() && obj.isTiberium()))) &&\n                                this.map.terrain.getPassableSpeed(tile, SpeedType.Foot, false, false) > 0);\n                            tileFinderMap.set(baseTile, finder);\n                        }\n                        tileIndex = (tileIndex + 1) % spawnTiles.length;\n                        tile = finder.getNextTile();\n                    }\n                    if (tile) {\n                        const unitRules = this.rules.getObject(name, type);\n                        if (type === ObjectType.Vehicle) {\n                            const unit = this.createUnitForPlayer(unitRules, player);\n                            this.applyInitialVeteran(unit, player);\n                            this.spawnObject(unit, tile);\n                            remaining--;\n                        }\n                        else if (type === ObjectType.Infantry) {\n                            for (const subCell of Infantry.SUB_CELLS.slice(0, remaining)) {\n                                const unit = this.createUnitForPlayer(unitRules, player);\n                                unit.position.subCell = subCell;\n                                this.applyInitialVeteran(unit, player);\n                                this.spawnObject(unit, tile);\n                                remaining--;\n                            }\n                        }\n                        else {\n                            throw new Error(\"Should not reach this line\");\n                        }\n                    }\n                    else {\n                        remaining--;\n                    }\n                }\n            }\n        }\n    }\n    applyInitialVeteran(unit: any, player: any) {\n        if (unit.veteranTrait) {\n            if (this.rules.general.veteran.initialVeteran) {\n                unit.veteranTrait.setVeteranLevel(VeteranLevel.Elite);\n            }\n            else if (player.country.hasVeteranUnit(unit.type, unit.name)) {\n                unit.veteranTrait.setVeteranLevel(VeteranLevel.Veteran);\n            }\n        }\n    }\n    createObject(type: ObjectType, name: string) {\n        return this.objectFactory.create(type, name, this.rules, this.art);\n    }\n    createUnitForPlayer(unitRules: any, player: any) {\n        if (![ObjectType.Aircraft, ObjectType.Vehicle, ObjectType.Infantry].includes(unitRules.type)) {\n            throw new Error(`Attempted to create an invalid unit type \"${unitRules.type}\"`);\n        }\n        const unit = this.createObject(unitRules.type, unitRules.name);\n        this.changeObjectOwner(unit, player);\n        unit.purchaseValue = this.sellTrait.computePurchaseValue(unit.rules, player);\n        return unit;\n    }\n    createProjectile(projectileName: string, fromObject: any, weapon: any, target: any, isShrapnel: boolean) {\n        const projectile = this.createObject(ObjectType.Projectile, projectileName);\n        projectile.fromWeapon = weapon;\n        projectile.fromObject = fromObject;\n        projectile.fromPlayer = fromObject.owner;\n        projectile.target = target;\n        projectile.isShrapnel = isShrapnel;\n        return projectile;\n    }\n    createLooseProjectile(weaponName: string, fromPlayer: any, target: any) {\n        const weaponRules = this.rules.getWeapon(weaponName);\n        const projectileName = weaponRules.projectile;\n        const projectileRules = this.rules.getProjectile(projectileName);\n        const warheadRules = this.rules.getWarhead(weaponRules.warhead);\n        const weapon = {\n            minRange: 0,\n            projectileRules: projectileRules,\n            range: Number.POSITIVE_INFINITY,\n            rules: weaponRules,\n            speed: Weapon.computeSpeed(weaponRules, projectileRules),\n            type: WeaponType.Primary,\n            warhead: new Warhead(warheadRules),\n        };\n        const projectile = this.createObject(ObjectType.Projectile, projectileName);\n        projectile.fromWeapon = weapon;\n        projectile.fromObject = undefined;\n        projectile.fromPlayer = fromPlayer;\n        projectile.target = target;\n        return projectile;\n    }\n    createSuperWeapon(name: string, owner: any, isReady: boolean = false) {\n        const rules = this.rules.getSuperWeapon(name);\n        return new SuperWeapon(name, rules, owner, isReady);\n    }\n    createTarget(obj: any, tile: any) {\n        return new Target(obj, tile, this.map.tileOccupation);\n    }\n    isValidTarget(obj: any): boolean {\n        if (obj) {\n            if (!obj.isSpawned || obj.isCrashing) {\n                return false;\n            }\n            if (!(obj.rules.legalTarget || (obj.isBuilding() && obj.rules.hospital))) {\n                return false;\n            }\n            if (obj.isBuilding() && obj.rules.invisibleInGame) {\n                return false;\n            }\n        }\n        return true;\n    }\n    spawnObject(obj: any, tile: any) {\n        if (obj.isTechno() && obj.limboData) {\n            throw new Error(`Object ${obj.name}#${obj.id} is in limbo. Use unlimboObject instead or clear limboData first`);\n        }\n        this.doSpawnObject(obj, tile);\n    }\n    unspawnObject(obj: any) {\n        if (obj.isTechno() && obj.owner) {\n            obj.owner.removeOwnedObject(obj);\n        }\n        this.doUnspawnObject(obj);\n    }\n    limboObject(obj: any, limboData: any) {\n        obj.limboData = limboData;\n        this.doUnspawnObject(obj);\n    }\n    unlimboObject(obj: any, tile: any, skipSelection: boolean = false) {\n        const limboData = obj.limboData;\n        if (!limboData) {\n            throw new Error(`Object ${obj.name}#${obj.id} has no limboData attached`);\n        }\n        obj.limboData = undefined;\n        this.doSpawnObject(obj, tile);\n        const selection = this.getUnitSelection();\n        if (limboData.selected && !skipSelection) {\n            selection.addToSelection(obj);\n        }\n        if (limboData.controlGroup !== undefined) {\n            selection.addUnitsToGroup(limboData.controlGroup, [obj], false);\n        }\n    }\n    private doSpawnObject(obj: any, tile: any) {\n        obj.position.tile = tile;\n        if (obj.isBuilding()) {\n            const center = obj.art.foundationCenter;\n            const centerX = tile.rx + center.x;\n            const centerY = tile.ry + center.y;\n            obj.centerTile = this.map.tiles.getByMapCoords(centerX, centerY) ?? this.map.tiles.getPlaceholderTile(centerX, centerY);\n        }\n        this.world.spawnObject(obj);\n        if (obj.cachedTraits.tick.length || obj.isProjectile() || obj.isDebris() || obj.isTechno()) {\n            this.updatableObjects.add(obj);\n        }\n        if (obj.isTechno()) {\n            this.map.technosByTile.add(obj);\n        }\n        if (!obj.isProjectile() && !obj.isDebris()) {\n            this.map.tileOccupation.occupyTileRange(tile, obj);\n        }\n        if (obj.art.canHideThings) {\n            this.map.tileOcclusion.addOccluder(obj);\n        }\n        obj.onSpawn(this);\n        this.traits.filter(NotifySpawn).forEach((trait: NotifySpawn) => {\n            trait[NotifySpawn.onSpawn](obj, this);\n        });\n        this.events.dispatch(new ObjectSpawnEvent(obj));\n    }\n    private doUnspawnObject(obj: any) {\n        const tile = obj.tile;\n        if (!obj.isProjectile() && !obj.isDebris()) {\n            this.map.tileOccupation.unoccupyTileRange(tile, obj);\n        }\n        if (obj.art.canHideThings) {\n            this.map.tileOcclusion.removeOccluder(obj);\n        }\n        if (obj.isTechno()) {\n            this.unitSelection.cleanupUnit(obj);\n            this.map.technosByTile.remove(obj);\n        }\n        this.world.removeObject(obj);\n        this.updatableObjects.delete(obj);\n        obj.onUnspawn(this);\n        this.traits.filter(NotifyUnspawn).forEach((trait: NotifyUnspawn) => {\n            trait[NotifyUnspawn.onUnspawn](obj, this);\n        });\n        this.events.dispatch(new ObjectUnspawnEvent(obj));\n    }\n    destroyObject(obj: any, killer?: any, silent: boolean = false, skipEvents: boolean = false) {\n        if (obj.isDestroyed) {\n            throw new Error(`Object with ID \"${obj.id}\" is already destroyed`);\n        }\n        if (obj.isTechno()) {\n            const originalOwner = obj.mindControllableTrait?.getOriginalOwner() ?? obj.owner;\n            if (killer && (obj.isBuilding() || originalOwner.isCombatant())) {\n                killer.player.addUnitsKilled(obj.type, 1);\n                if (killer.player !== originalOwner && !this.alliances.areAllied(killer.player, originalOwner)) {\n                    killer.player.score += obj.rules.points;\n                }\n            }\n            if (!originalOwner.isNeutral) {\n                originalOwner.addUnitsLost(obj.type, 1);\n            }\n        }\n        obj.isDestroyed = true;\n        if (obj.healthTrait) {\n            obj.healthTrait.health = 0;\n        }\n        obj.onDestroy(this, killer, silent);\n        this.traits.filter(NotifyDestroy).forEach((trait: NotifyDestroy) => {\n            trait[NotifyDestroy.onDestroy](obj, this, killer);\n        });\n        killer?.obj?.traits.filter(NotifyTargetDestroy).forEach((trait: NotifyTargetDestroy) => {\n            trait[NotifyTargetDestroy.onDestroy](killer.obj, obj, killer.weapon, this);\n        });\n        this.events.dispatch(new ObjectDestroyEvent(obj, killer, skipEvents));\n        if (obj.isBuilding() && obj.rules.leaveRubble && obj.deathType !== DeathType.Temporal) {\n            obj.owner.removeOwnedObject(obj);\n            this.unitSelection.cleanupUnit(obj);\n            const tiles = this.map.tileOccupation.calculateTilesForGameObject(obj.tile, obj);\n            this.map.terrain.invalidateTiles(tiles);\n            if (obj.art.canHideThings) {\n                this.map.tileOcclusion.removeOccluder(obj);\n            }\n            this.updatableObjects.delete(obj);\n            obj.onUnspawn(this);\n            this.traits.filter(NotifyUnspawn).forEach((trait: NotifyUnspawn) => {\n                trait[NotifyUnspawn.onUnspawn](obj, this);\n            });\n            this.events.dispatch(new ObjectUnspawnEvent(obj));\n        }\n        else if (obj.isSpawned) {\n            this.unspawnObject(obj);\n        }\n        else if (obj.isTechno() && obj.owner) {\n            if (!obj.limboData) {\n                throw new Error(`Object with ID \"${obj.id}\" should be in limbo but has no limboData`);\n            }\n            obj.owner.removeOwnedObject(obj);\n        }\n        obj.dispose();\n    }\n    getObjectById(id: number) {\n        return this.world.getObjectById(id);\n    }\n    changeObjectOwner(obj: any, newOwner: any) {\n        const oldOwner = obj.owner;\n        if (oldOwner) {\n            oldOwner.removeOwnedObject(obj);\n        }\n        newOwner.addOwnedObject(obj);\n        if (oldOwner && oldOwner !== newOwner) {\n            this.traits.filter(NotifyOwnerChange).forEach((trait: NotifyOwnerChange) => {\n                trait[NotifyOwnerChange.onChange](obj, oldOwner, this);\n            });\n            obj.onOwnerChange(oldOwner, this);\n            this.events.dispatch(new ObjectOwnerChangeEvent(obj, oldOwner));\n            if (oldOwner === this.localPlayer && obj.owner !== this.localPlayer) {\n                this.unitSelection.removeFromSelection([obj]);\n                this.unitSelection.removeUnitsFromGroup([obj]);\n            }\n        }\n    }\n    addObjectTrait(obj: any, trait: any) {\n        obj.addTrait(trait);\n        this.traits.filter(NotifyObjectTraitAdd).forEach((t: NotifyObjectTraitAdd) => {\n            t[NotifyObjectTraitAdd.onAdd](obj, trait, this);\n        });\n    }\n    onAllianceChange(alliance: any, initiator: any, formed: boolean) {\n        this.events.dispatch(new AllianceChangeEvent(alliance, formed ? AllianceEventType.Formed : AllianceEventType.Broken, initiator));\n        this.traits.filter(NotifyAllianceChange).forEach((trait: NotifyAllianceChange) => {\n            trait[NotifyAllianceChange.onChange](alliance, formed, this);\n        });\n    }\n    update() {\n        if (this.status === GameStatus.NotStarted) {\n            return;\n        }\n        this.botManager.update(this);\n        if (this.status !== GameStatus.Ended) {\n            if (this.lastGameEndCheck === undefined || this.currentTime - this.lastGameEndCheck >= 1000) {\n                this.checkGameEndConditions();\n                this.lastGameEndCheck = this.currentTime;\n            }\n        }\n        for (const obj of [...this.updatableObjects]) {\n            if (obj.isSpawned) {\n                obj.update(this);\n            }\n        }\n        this.playerList.getCombatants().forEach((player: any) => {\n            player.cheerCooldownTicks = Math.max(0, player.cheerCooldownTicks - 1);\n        });\n        this.traits.filter(NotifyTick).forEach((trait: NotifyTick) => {\n            trait[NotifyTick.onTick](this);\n        });\n        if (this.localPlayer && !this.localPlayer.isObserver && !this.localPlayer.defeated) {\n            const selectedUnits = this.unitSelection.getSelectedUnits();\n            if (selectedUnits.length === 1) {\n                const unit = selectedUnits[0];\n                if (unit.isTechno() && unit.owner !== this.localPlayer) {\n                    const shroud = this.mapShroudTrait.getPlayerShroud(this.localPlayer);\n                    const tiles = this.map.tileOccupation.calculateTilesForGameObject(unit.tile, unit);\n                    const isVisible = tiles.find((tile: any) => !shroud.isShrouded(tile, unit.tileElevation));\n                    if (!isVisible) {\n                        this.unitSelection.deselectAll();\n                        this.unitSelection.cleanupUnit(unit);\n                    }\n                }\n            }\n        }\n        for (const callback of this.afterTickCallbacks) {\n            callback();\n        }\n        this.afterTickCallbacks.length = 0;\n        this.triggers.update(this);\n        this.countdownTimer.update(this);\n        this.currentTick++;\n        this.currentTime += 1000 / GameSpeed.BASE_TICKS_PER_SECOND;\n    }\n    afterTick(callback: () => void) {\n        this.afterTickCallbacks.push(callback);\n    }\n    checkGameEndConditions() {\n        this.updateDefeatedPlayers(this.playerList.getCombatants());\n        const shouldEnd = (this.localPlayer?.defeated && !this.localPlayer.isObserver) ||\n            (!this.alliances.getHostilePlayers().length &&\n                this.gameOpts.humanPlayers.length + this.gameOpts.aiPlayers.filter((p: any) => !!p).length > 1);\n        if (shouldEnd) {\n            this.end();\n        }\n    }\n    end() {\n        if (this.status !== GameStatus.Ended) {\n            this.status = GameStatus.Ended;\n            this._onEnd.dispatch(this, undefined);\n        }\n    }\n    updateDefeatedPlayers(players: any[]) {\n        const isStalemate = this.stalemateDetectTrait?.isStale() && this.stalemateDetectTrait.getCountdownTicks() === 0;\n        const shortGame = this.gameOpts.shortGame;\n        players.forEach((player: any) => {\n            let isDefeated: boolean;\n            if (isStalemate) {\n                isDefeated = true;\n            }\n            else {\n                let hasAssets: boolean;\n                if (shortGame) {\n                    const hasSignificantBuilding = [...player.getOwnedObjectsByType(ObjectType.Building, true)].some((obj: any) => !obj.rules.insignificant);\n                    hasAssets = hasSignificantBuilding || player.getOwnedObjects(true).some((obj: any) => this.rules.general.baseUnit.includes(obj.name));\n                }\n                else {\n                    hasAssets = player.getOwnedObjects(true).some((obj: any) => !obj.rules.insignificant && !obj.limboData?.inTransport);\n                }\n                isDefeated = !hasAssets;\n            }\n            if (isDefeated) {\n                player.defeated = true;\n                const hasHumanConflict = this.alliances.getHostilePlayers().some((pair: any) => !pair.first.isAi || !pair.second.isAi);\n                if (hasHumanConflict) {\n                    player.isObserver = true;\n                }\n                this.removeAllPlayerAssets(player);\n                this.events.dispatch(new PlayerDefeatedEvent(player));\n                if (hasHumanConflict) {\n                    this.mapShroudTrait.getPlayerShroud(player)?.revealAll();\n                    const wasRadarDisabled = player.radarTrait.isDisabled();\n                    player.radarTrait.setDisabled(false);\n                    if (wasRadarDisabled) {\n                        this.events.dispatch(new RadarOnOffEvent(player, true));\n                    }\n                }\n            }\n        });\n    }\n    removeAllPlayerAssets(player: any) {\n        player.getOwnedObjects().forEach((obj: any) => {\n            if (!obj.isDestroyed) {\n                if (obj.isBuilding() && obj.rules.returnable && obj.rules.needsEngineer && !obj.garrisonTrait) {\n                    this.changeObjectOwner(obj, this.getCivilianPlayer());\n                }\n                else if (!(obj.isBuilding() && obj.wallTrait)) {\n                    this.destroyObject(obj, undefined, true);\n                }\n            }\n        });\n        player.getOwnedObjects(true).forEach((obj: any) => {\n            if (!obj.isDestroyed) {\n                if (obj.limboData?.inTransport || (obj.isBuilding() && obj.wallTrait)) {\n                    this.changeObjectOwner(obj, this.getCivilianPlayer());\n                }\n                else {\n                    this.destroyObject(obj, undefined, true);\n                }\n            }\n        });\n    }\n    redistributeAllPlayerAssets(player: any): boolean {\n        if (player.isObserver) {\n            return false;\n        }\n        if (!(this.rules.mpDialogSettings.mustAlly && !this.rules.mpDialogSettings.allyChangeAllowed)) {\n            return false;\n        }\n        const allies = this.alliances.getAllies(player).filter((p: any) => !p.isAi && !p.defeated);\n        if (allies.length > 0) {\n            const topAlly = [...allies].sort((a: any, b: any) => b.score - a.score)[0];\n            for (const obj of player.getOwnedObjects(true)) {\n                this.changeObjectOwner(obj, topAlly);\n            }\n            const creditsPerAlly = Math.floor(player.credits / allies.length);\n            const remainder = player.credits % allies.length;\n            for (const ally of allies) {\n                ally.credits += creditsPerAlly;\n            }\n            allies[0].credits += remainder;\n            return true;\n        }\n        return false;\n    }\n    generateRandomInt(min: number, max: number): number {\n        return this.prng.generateRandomInt(min, max);\n    }\n    generateRandom(): number {\n        return this.prng.generateRandom();\n    }\n    getHash(): number {\n        return fnv32a([\n            ...new Uint8Array(new Float64Array([this.prng.getLastRandom()]).buffer),\n            this.nextObjectId.value,\n            ...this.world.getAllObjects().map((obj: any) => obj.getHash()),\n            ...this.playerList.getAll().map((player: any) => player.getHash()),\n            this.alliances.getHash(),\n            ...this.traits.getAll().map((trait: any) => trait.getHash?.() ?? 0),\n        ]);\n    }\n    debugGetState() {\n        return {\n            currentTick: this.currentTick,\n            lastRandom: this.prng.getLastRandom(),\n            nextObjectId: this.nextObjectId.value,\n            objects: this.world.getAllObjects().map((obj: any) => obj.debugGetState()),\n            players: this.playerList.getAll().map((player: any) => player.debugGetState()),\n            alliances: this.alliances.debugGetState(),\n            traits: this.traits.getAll().reduce((acc: any, trait: any) => {\n                const state = trait.debugGetState?.();\n                if (state !== undefined) {\n                    acc[trait.constructor.name] = state;\n                }\n                return acc;\n            }, {}),\n        };\n    }\n    dispose() {\n        this.world.getAllObjects().forEach((obj: any) => obj.dispose());\n        this.playerList.getAll().forEach((player: any) => player.dispose());\n        this.constructionWorkers.forEach((worker: any) => worker.dispose());\n        this.botManager.dispose();\n        this.triggers.dispose();\n        this.map.dispose();\n        this.traits.dispose();\n    }\n}\n"
  },
  {
    "path": "src/game/GameEventBus.ts",
    "content": "import { EventDispatcher } from '@/util/event';\nexport class GameEventBus {\n    private dispatcher: EventDispatcher;\n    private dispatchersByType: Map<string, EventDispatcher>;\n    constructor() {\n        this.dispatcher = new EventDispatcher();\n        this.dispatchersByType = new Map();\n    }\n    dispatch(event: any): void {\n        this.dispatcher.dispatch(undefined, event);\n        this.dispatchersByType.get(event.type)?.dispatch(undefined, event);\n    }\n    subscribe(typeOrHandler: string | ((event: any) => void), handler?: (event: any) => void): () => void {\n        let type: string | undefined;\n        let callback: (event: any) => void;\n        if (typeof typeOrHandler === 'function') {\n            callback = typeOrHandler;\n        }\n        else {\n            type = typeOrHandler;\n            callback = handler!;\n        }\n        if (type === undefined) {\n            this.dispatcher.subscribe(callback);\n            return () => this.unsubscribe(callback);\n        }\n        else {\n            return this.subscribeType(type, callback);\n        }\n    }\n    unsubscribe(typeOrHandler: string | ((event: any) => void), handler?: (event: any) => void): void {\n        let type: string | undefined;\n        let callback: (event: any) => void;\n        if (typeof typeOrHandler === 'function') {\n            callback = typeOrHandler;\n        }\n        else {\n            type = typeOrHandler;\n            callback = handler!;\n        }\n        if (type === undefined) {\n            this.dispatcher.unsubscribe(callback);\n        }\n        else {\n            this.unsubscribeType(type, callback);\n        }\n    }\n    private subscribeType(type: string, handler: (event: any) => void): () => void {\n        let dispatcher = this.dispatchersByType.get(type);\n        if (!dispatcher) {\n            dispatcher = new EventDispatcher();\n            this.dispatchersByType.set(type, dispatcher);\n        }\n        dispatcher.subscribe(handler);\n        return () => this.unsubscribeType(type, handler);\n    }\n    private unsubscribeType(type: string, handler: (event: any) => void): void {\n        this.dispatchersByType.get(type)?.unsubscribe(handler);\n    }\n}\n"
  },
  {
    "path": "src/game/GameFactory.ts",
    "content": "import { Rules } from './rules/Rules';\nimport { Art } from './art/Art';\nimport { IniFile } from '../data/IniFile';\nimport { Country } from './Country';\nimport { ObjectFactory } from './gameobject/ObjectFactory';\nimport { World } from './World';\nimport { GameMap } from './GameMap';\nimport { GameOpts } from './gameopts/GameOpts';\nimport { OBS_COUNTRY_ID, RANDOM_COUNTRY_ID, RANDOM_COLOR_ID, RANDOM_START_POS } from './gameopts/constants';\nimport { isNotNullOrUndefined } from '../util/typeGuard';\nimport { Alliances } from './Alliances';\nimport { PlayerList } from './PlayerList';\nimport { UnitSelection } from './gameobject/selection/UnitSelection';\nimport { BoxedVar } from '../util/BoxedVar';\nimport { PlayerFactory } from './player/PlayerFactory';\nimport { PowerTrait } from './trait/PowerTrait';\nimport { SellTrait } from './trait/SellTrait';\nimport { RadarTrait } from './trait/RadarTrait';\nimport { ProductionTrait } from './trait/ProductionTrait';\nimport { MapShroudTrait } from './trait/MapShroudTrait';\nimport { Game } from './Game';\nimport { MapRadiationTrait } from './trait/MapRadiationTrait';\nimport { ActionFactory } from './action/ActionFactory';\nimport { ActionFactoryReg } from './action/ActionFactoryReg';\nimport { SuperWeaponsTrait } from './trait/SuperWeaponsTrait';\nimport { SharedDetectDisguiseTrait } from './trait/SharedDetectDisguiseTrait';\nimport { SharedDetectCloakTrait } from './trait/SharedDetectCloakTrait';\nimport { CrateGeneratorTrait } from './trait/CrateGeneratorTrait';\nimport { StalemateDetectTrait } from './trait/StalemateDetectTrait';\nimport { GameOptSanitizer } from './gameopts/GameOptSanitizer';\nimport { GameOptRandomGen } from './gameopts/GameOptRandomGen';\nimport { MapLightingTrait } from './trait/MapLightingTrait';\nimport { Prng } from './Prng';\nimport { Ai } from './ai/Ai';\nimport { BotFactory } from './bot/BotFactory';\nimport { BotManager } from './BotManager';\nimport { isHumanPlayerInfo } from './gameopts/GameOpts';\ninterface GameMode {\n    type: string;\n}\ninterface GameModeRegistry {\n    getById(modeId: string): GameMode;\n}\ninterface PlayerInfo {\n    countryId: string;\n    colorId: string;\n    startPos: number;\n    name?: string;\n}\ninterface HumanPlayerInfo extends PlayerInfo {\n    name: string;\n}\ninterface AiPlayerInfo extends PlayerInfo {\n    difficulty: string;\n}\ninterface GameCreationOptions {\n    artOverrides?: IniFile;\n    specialFlags: string[];\n}\ninterface StartingLocations {\n    [key: number]: any;\n}\ninterface MultiplayerCountry {\n    name: string;\n}\nexport class GameFactory {\n    static create(gameOptions: GameCreationOptions, mapData: any, baseRules: IniFile, baseArt: IniFile, aiConfig: any, modRules: IniFile, additionalRules: IniFile[], randomSeed1: number | string, randomSeed2: number, gameOpts: GameOpts, gameModeRegistry: GameModeRegistry, skipStalemate: boolean, botConfig: any, debugFlags: any, speedCheat: any, debugBotIndex?: any, actionLogger?: any): Game {\n        const mergedRules: IniFile = baseRules.clone().mergeWith(modRules);\n        for (const additionalRule of additionalRules) {\n            mergedRules.mergeWith(additionalRule);\n        }\n        mergedRules.mergeWith(gameOptions as any);\n        const mergedArt: IniFile = baseArt.clone().mergeWith(gameOptions.artOverrides ?? new IniFile());\n        const rules: Rules = new Rules(mergedRules, debugFlags);\n        const art: Art = new Art(rules, mergedArt, gameOptions, debugFlags);\n        const ai: Ai = new Ai(aiConfig);\n        rules.applySpecialFlags(gameOptions.specialFlags as any);\n        GameOptSanitizer.sanitize(gameOpts, rules);\n        const baseMultiplayerRules: Rules = new Rules(baseRules);\n        const multiplayerCountries: MultiplayerCountry[] = baseMultiplayerRules.getMultiplayerCountries();\n        const multiplayerColors: string[] = [...baseMultiplayerRules.getMultiplayerColors().values()] as any;\n        const prng: Prng = Prng.factory(randomSeed1, randomSeed2);\n        const gameMap: GameMap = new GameMap(gameOptions as any, mapData, rules, prng.generateRandomInt.bind(prng));\n        const world: World = new World();\n        const gameMode: GameMode = gameModeRegistry.getById(gameOpts.gameMode as any);\n        const playerList: PlayerList = new PlayerList();\n        const alliances: Alliances = new Alliances(playerList);\n        const unitSelection: UnitSelection = new UnitSelection();\n        const tickCounter: BoxedVar<number> = new BoxedVar<number>(1);\n        const objectFactory: ObjectFactory = new ObjectFactory(gameMap.tiles, gameMap.tileOccupation, gameMap.bridges, tickCounter);\n        const actionFactory: ActionFactory = new ActionFactory();\n        const botFactory: BotFactory = new BotFactory(botConfig);\n        const botManager: BotManager = BotManager.factory(actionFactory, botFactory, debugBotIndex, actionLogger);\n        const game: Game = new Game(world, gameMap, rules, art, ai, randomSeed1, randomSeed2, gameOpts, gameMode.type, playerList, unitSelection, alliances, tickCounter, objectFactory, botManager);\n        new ActionFactoryReg().register(actionFactory, game, undefined);\n        this.setupGameTraits(game, rules, gameMap, alliances, gameOpts, skipStalemate, speedCheat);\n        const productionTrait: ProductionTrait = game.traits.get(ProductionTrait) as ProductionTrait;\n        const playerFactory: PlayerFactory = new PlayerFactory(rules, gameOpts, productionTrait.getAvailableObjects());\n        const randomGen: GameOptRandomGen = GameOptRandomGen.factory(randomSeed1, randomSeed2);\n        const generatedColors: Map<PlayerInfo, string> = randomGen.generateColors(gameOpts) as any;\n        const generatedCountries: Map<PlayerInfo, string> = randomGen.generateCountries(gameOpts, baseMultiplayerRules) as any;\n        const generatedStartLocations: Map<PlayerInfo, number> = randomGen.generateStartLocations(gameOpts, gameMap.startingLocations as any);\n        const allPlayers: (HumanPlayerInfo | AiPlayerInfo)[] = [\n            ...gameOpts.humanPlayers,\n            ...gameOpts.aiPlayers\n        ].filter(isNotNullOrUndefined) as any;\n        this.createPlayers(game, allPlayers, playerFactory, multiplayerCountries, multiplayerColors, rules, generatedCountries, generatedColors, generatedStartLocations);\n        game.addPlayer(playerFactory.createNeutral(rules, \"@@NEUTRAL@@\"));\n        return game;\n    }\n    private static setupGameTraits(game: Game, rules: Rules, gameMap: GameMap, alliances: Alliances, gameOpts: GameOpts, skipStalemate: boolean, speedCheat: any): void {\n        game.traits.add(new PowerTrait());\n        const sellTrait: SellTrait = new SellTrait(game, rules.general);\n        game.sellTrait = sellTrait;\n        game.traits.add(sellTrait);\n        game.traits.add(new RadarTrait());\n        const productionTrait: ProductionTrait = new ProductionTrait(rules, speedCheat);\n        game.traits.add(productionTrait);\n        const mapShroudTrait: MapShroudTrait = new MapShroudTrait(gameMap, alliances);\n        game.mapShroudTrait = mapShroudTrait;\n        game.traits.add(mapShroudTrait);\n        const mapRadiationTrait: MapRadiationTrait = new MapRadiationTrait(gameMap);\n        (game as any).mapRadiationTrait = mapRadiationTrait;\n        game.traits.add(mapRadiationTrait);\n        const mapLightingTrait: MapLightingTrait = new MapLightingTrait(rules.audioVisual as any, gameMap.getLighting());\n        (game as any).mapLightingTrait = mapLightingTrait;\n        game.traits.add(mapLightingTrait);\n        game.traits.add(new SuperWeaponsTrait());\n        game.traits.add(new SharedDetectDisguiseTrait());\n        game.traits.add(new SharedDetectCloakTrait());\n        const crateGeneratorTrait: CrateGeneratorTrait = new CrateGeneratorTrait(gameOpts.cratesAppear);\n        game.crateGeneratorTrait = crateGeneratorTrait;\n        game.traits.add(crateGeneratorTrait);\n        if (!skipStalemate) {\n            const stalemateDetectTrait: StalemateDetectTrait = new StalemateDetectTrait();\n            game.stalemateDetectTrait = stalemateDetectTrait;\n            game.traits.add(stalemateDetectTrait);\n        }\n    }\n    private static createPlayers(game: Game, allPlayers: (HumanPlayerInfo | AiPlayerInfo)[], playerFactory: PlayerFactory, multiplayerCountries: MultiplayerCountry[], multiplayerColors: string[], rules: Rules, generatedCountries: Map<PlayerInfo, string>, generatedColors: Map<PlayerInfo, string>, generatedStartLocations: Map<PlayerInfo, number>): void {\n        allPlayers.forEach((playerInfo: HumanPlayerInfo | AiPlayerInfo) => {\n            let playerName: string;\n            let isAi: boolean;\n            let aiDifficulty: string | undefined;\n            let customBotId: string | undefined;\n            if (isHumanPlayerInfo(playerInfo)) {\n                playerName = playerInfo.name;\n                isAi = false;\n            }\n            else {\n                playerName = game.getAiPlayerName(playerInfo);\n                isAi = true;\n                aiDifficulty = (playerInfo as any).difficulty;\n                customBotId = (playerInfo as any).customBotId;\n            }\n            if (playerInfo.countryId === (OBS_COUNTRY_ID as any)) {\n                game.addPlayer(playerFactory.createObserver(playerName, rules));\n                return;\n            }\n            const resolvedCountryId: string = generatedCountries.get(playerInfo) ?? playerInfo.countryId;\n            const resolvedColorId: string = generatedColors.get(playerInfo) ?? playerInfo.colorId;\n            const resolvedStartPos: number = generatedStartLocations.get(playerInfo) ?? playerInfo.startPos;\n            this.validateResolvedValues(resolvedCountryId, resolvedColorId, resolvedStartPos);\n            const countryName: string = multiplayerCountries[parseInt(resolvedCountryId)].name;\n            const country: Country = Country.factory(countryName, rules as any);\n            const color: string = multiplayerColors[parseInt(resolvedColorId)];\n            const player = playerFactory.createCombatant(playerName, country, resolvedStartPos, color, isAi, aiDifficulty, customBotId);\n            game.addPlayer(player);\n        });\n    }\n    private static validateResolvedValues(countryId: string, colorId: string, startPos: number): void {\n        if (countryId === (RANDOM_COUNTRY_ID as any)) {\n            throw new Error(\"Random country should have been resolved by now\");\n        }\n        if (colorId === (RANDOM_COLOR_ID as any)) {\n            throw new Error(\"Random color should have been resolved by now\");\n        }\n        if (startPos === (RANDOM_START_POS as any)) {\n            throw new Error(\"Random start location should have been resolved by now\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/GameMap.ts",
    "content": "import { TileCollection } from '@/game/map/TileCollection';\nimport { TileOccupation } from '@/game/map/TileOccupation';\nimport { Terrain } from '@/game/map/Terrain';\nimport { MapBounds } from '@/game/map/MapBounds';\nimport { Bridges } from '@/game/map/Bridges';\nimport { QuadTree } from '@/util/QuadTree';\nimport { TileOcclusion } from '@/game/map/TileOcclusion';\nimport { AutoLat } from '@/game/theater/AutoLat';\nimport { TheaterType } from '@/engine/TheaterType';\nimport { Vector2 } from '@/game/math/Vector2';\nimport { Box2 } from '@/game/math/Box2';\ninterface MapFile {\n    startingLocations: any[];\n    tiles: any[];\n    theaterType: TheaterType;\n    tags: Tag[];\n    cellTags: CellTag[];\n    lighting: any;\n    ionLighting: any;\n    triggers: any[];\n    variables: any[];\n    waypoints: Waypoint[];\n    terrains: any[];\n    overlays: any[];\n    smudges: any[];\n    structures: any[];\n    infantries: any[];\n    vehicles: any[];\n    aircrafts: any[];\n}\ninterface Tag {\n    id: string;\n}\ninterface CellTag {\n    coords: {\n        x: number;\n        y: number;\n    };\n    tagId: string;\n}\ninterface Waypoint {\n    number: number;\n    rx: number;\n    ry: number;\n}\ninterface Tile {\n    rx: number;\n    ry: number;\n    dx: number;\n    dy: number;\n    z: number;\n    tag?: Tag;\n}\ninterface Techno {\n    isBuilding(): boolean;\n    centerTile: Tile;\n    tile: Tile;\n}\ninterface InitialMapObjects {\n    terrains: any[];\n    overlays: any[];\n    smudges: any[];\n    technos: any[];\n}\ninterface QuadTreeOptions {\n    getKey: (item: Techno) => Vector2;\n    maxDepth: number;\n    splitThreshold: number;\n    joinThreshold: number;\n}\nexport class GameMap {\n    private mapFile: MapFile;\n    public tiles: TileCollection;\n    public mapBounds: MapBounds;\n    public tileOccupation: TileOccupation;\n    private tileOcclusion: TileOcclusion;\n    public terrain: Terrain;\n    public bridges: Bridges;\n    private technosByTile: QuadTree<Techno>;\n    get startingLocations() {\n        return this.mapFile.startingLocations;\n    }\n    constructor(mapFile: MapFile, t: any, i: any, r: any) {\n        this.mapFile = mapFile;\n        this.tiles = new TileCollection(this.mapFile.tiles, t, i.general, r);\n        this.mapBounds = new MapBounds().fromMapFile(this.mapFile as any, this.tiles);\n        this.tileOccupation = new TileOccupation(this.tiles);\n        this.tileOcclusion = new TileOcclusion(this.tiles);\n        this.terrain = new Terrain(this.tiles, this.mapFile.theaterType, this.mapBounds, this.tileOccupation, i);\n        this.bridges = new Bridges(t, this.tiles, this.tileOccupation, this.mapBounds, i);\n        const tags = this.mapFile.tags;\n        for (const cellTag of this.mapFile.cellTags) {\n            const tile = this.tiles.getByMapCoords(cellTag.coords.x, cellTag.coords.y);\n            if (tile) {\n                (tile as any).tag = tags.find((tag) => tag.id === cellTag.tagId);\n            }\n        }\n        const mapSize = this.tiles.getMapSize();\n        const n = Math.max(mapSize.width, mapSize.height) / 5;\n        this.technosByTile = new QuadTree<Techno>(new Box2(new Vector2(0, 0), new Vector2(mapSize.width, mapSize.height)), {\n            getKey: (techno: Techno) => {\n                const tile = techno.isBuilding() ? techno.centerTile : techno.tile;\n                return new Vector2(tile.rx, tile.ry);\n            },\n            maxDepth: this.computeQuadDepth(n),\n            splitThreshold: 10,\n            joinThreshold: 5,\n        });\n        if (this.mapFile.theaterType !== TheaterType.Snow) {\n            AutoLat.calculate(this.tiles, t);\n        }\n    }\n    private computeQuadDepth(e: number): number {\n        if (e <= 1)\n            return 1;\n        let depth = 0;\n        while (e / 2 >= 1) {\n            e /= 2;\n            depth++;\n        }\n        return depth + (e > 1 ? 1 : 0);\n    }\n    getLighting(): any {\n        return this.mapFile.lighting;\n    }\n    getIonLighting(): any {\n        return this.mapFile.ionLighting;\n    }\n    getTheaterType(): TheaterType {\n        return this.mapFile.theaterType;\n    }\n    getTags(): Tag[] {\n        return this.mapFile.tags;\n    }\n    getTriggers(): any[] {\n        return this.mapFile.triggers;\n    }\n    getCellTags(): CellTag[] {\n        return this.mapFile.cellTags;\n    }\n    getVariables(): any[] {\n        return this.mapFile.variables;\n    }\n    getWaypoint(waypointNumber: number): Waypoint | undefined {\n        return this.mapFile.waypoints.find((waypoint) => waypoint.number === waypointNumber);\n    }\n    getTileAtWaypoint(waypointNumber: number): Tile | undefined {\n        const waypoint = this.getWaypoint(waypointNumber);\n        if (waypoint) {\n            const tile = this.tiles.getByMapCoords(waypoint.rx, waypoint.ry);\n            if (tile)\n                return tile;\n        }\n    }\n    isWithinBounds(tile: Tile): boolean {\n        return this.mapBounds.isWithinBounds(tile);\n    }\n    clampWithinBounds(tile: Tile): Tile {\n        const clampedTile = this.mapBounds.clampWithinBounds(tile);\n        let resultTile = this.tiles.getByDisplayCoords(clampedTile.dx, clampedTile.dy);\n        if (resultTile && this.mapBounds.isWithinBounds(resultTile)) {\n            let currentTile = resultTile;\n            let currentZ = resultTile.z;\n            while (currentZ >= 0 && currentTile && this.mapBounds.isWithinBounds(currentTile)) {\n                resultTile = currentTile;\n                currentTile = this.tiles.getByDisplayCoords(currentTile.dx, currentTile.dy + 2);\n                currentZ -= 2;\n            }\n        }\n        else {\n            let elevation = 0;\n            while (!resultTile || !this.mapBounds.isWithinBounds(resultTile)) {\n                if (elevation > 30) {\n                    throw new Error(\"Exceeded max elevation while trying to clamp tile to map bounds\");\n                }\n                resultTile = this.tiles.getByDisplayCoords(clampedTile.dx, clampedTile.dy + elevation);\n                elevation += 2;\n            }\n        }\n        return resultTile;\n    }\n    isWithinHardBounds(tile: Tile): boolean {\n        return this.mapBounds.isWithinHardBounds(tile as any);\n    }\n    getInitialMapObjects(): InitialMapObjects {\n        return {\n            terrains: this.mapFile.terrains,\n            overlays: this.mapFile.overlays,\n            smudges: this.mapFile.smudges,\n            technos: [\n                ...this.mapFile.structures,\n                ...this.mapFile.infantries,\n                ...this.mapFile.vehicles,\n                ...this.mapFile.aircrafts,\n            ],\n        };\n    }\n    getObjectsOnTile(tile: Tile): any[] {\n        return this.tileOccupation.getObjectsOnTile(tile);\n    }\n    getGroundObjectsOnTile(tile: Tile): any[] {\n        return this.tileOccupation.getGroundObjectsOnTile(tile);\n    }\n    getTileZone(tile: Tile, includeAdjacent: boolean = false): any {\n        return this.tileOccupation.getTileZone(tile, includeAdjacent);\n    }\n    dispose(): void {\n        this.terrain.dispose();\n        this.bridges.dispose();\n    }\n}\n"
  },
  {
    "path": "src/game/GameSpeed.ts",
    "content": "export class GameSpeed {\n    static BASE_TICKS_PER_SECOND = 15;\n    static computeGameSpeed(speed: number): number {\n        let ticksPerSecond: number;\n        if (speed === 6) {\n            ticksPerSecond = 60;\n        }\n        else if (speed === 5) {\n            ticksPerSecond = 45;\n        }\n        else {\n            ticksPerSecond = 60 / (6 - speed);\n        }\n        return ticksPerSecond / GameSpeed.BASE_TICKS_PER_SECOND;\n    }\n}\n"
  },
  {
    "path": "src/game/GameTurnManager.ts",
    "content": "import { EventDispatcher } from '@/util/event';\nexport class GameTurnManager {\n    private gameTurnMillis: number = 33;\n    private errorState = false;\n    public readonly onActionsSent = new EventDispatcher<this, void>();\n    constructor(private game?: {\n        update(): void;\n    }, private actionQueue?: {\n        dequeueAll(): any[];\n    }) { }\n    init(): void {\n    }\n    getTurnMillis(): number {\n        return this.gameTurnMillis;\n    }\n    setRate(rate: number): void {\n        const r = Number(rate) > 0 ? Number(rate) : 1;\n        this.gameTurnMillis = Math.max(1, Math.floor(1000 / r));\n    }\n    doGameTurn(_timestamp: number): boolean {\n        if (this.actionQueue) {\n            const actions = this.actionQueue.dequeueAll();\n            if (actions.length) {\n                for (const action of actions) {\n                    action.process?.();\n                }\n                this.onActionsSent.dispatch(this);\n            }\n        }\n        this.game?.update();\n        return true;\n    }\n    setPassiveMode(_passive: boolean): void {\n    }\n    setErrorState(): void {\n        this.errorState = true;\n    }\n    getErrorState(): boolean {\n        return this.errorState;\n    }\n    dispose(): void {\n    }\n}\n"
  },
  {
    "path": "src/game/Hashable.ts",
    "content": "export class Hashable {\n}\n"
  },
  {
    "path": "src/game/Player.ts",
    "content": "import { Color } from '@/util/Color';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { Traits } from '@/game/Traits';\nimport { fnv32a } from '@/util/math';\nimport { Country } from '@/game/Country';\nimport type { Production } from '@/game/player/production/Production';\ninterface PlayerOwnedObject {\n    id: string;\n    name: string;\n    type: ObjectType;\n    owner: Player;\n    buildLimit: number;\n    limboData?: any;\n}\nexport class Player {\n    private _credits: number = 0;\n    public readonly name: string;\n    public readonly country?: Country;\n    public readonly startLocation: any;\n    public readonly color: Color;\n    public isAi: boolean = false;\n    public defeated: boolean = false;\n    public resigned: boolean = false;\n    public dropped: boolean = false;\n    private objectsByType: Map<ObjectType, Set<PlayerOwnedObject>> = new Map();\n    private objectsById: Map<string, PlayerOwnedObject> = new Map();\n    public readonly traits: Traits = new Traits();\n    public score: number = 0;\n    private limitedUnitsBuiltByName: Map<string, number> = new Map();\n    private unitsBuiltByType: Map<ObjectType, number> = new Map();\n    private unitsKilledByType: Map<ObjectType, number> = new Map();\n    private unitsLostByType: Map<ObjectType, number> = new Map();\n    public buildingsCaptured: number = 0;\n    public cratesPickedUp: number = 0;\n    public cheerCooldownTicks: number = 0;\n    public readonly isObserver: boolean;\n    public readonly isNeutral: boolean;\n    public aiDifficulty?: any;\n    public customBotId?: string;\n    public powerTrait?: any;\n    public radarTrait?: any;\n    public superWeaponsTrait?: any;\n    public sharedDetectDisguiseTrait?: any;\n    public production?: Production;\n    get credits(): number {\n        return this._credits;\n    }\n    set credits(value: number) {\n        if (value < 0) {\n            throw new RangeError(\"Can't set credits to a negative value\");\n        }\n        this._credits = value;\n    }\n    constructor(name: string, country?: Country, startLocation?: any, color: Color = new Color(255, 0, 0)) {\n        this.name = name;\n        this.country = country;\n        this.startLocation = startLocation;\n        this.color = color;\n        this.isObserver = !country;\n        this.isNeutral = !!country && !country.isPlayable();\n    }\n    getOrCreateObjectsForType(type: ObjectType): Set<PlayerOwnedObject> {\n        let objects = this.objectsByType.get(type);\n        if (!objects) {\n            objects = new Set();\n            this.objectsByType.set(type, objects);\n        }\n        return objects;\n    }\n    addOwnedObject(object: PlayerOwnedObject): void {\n        const objects = this.getOrCreateObjectsForType(object.type);\n        objects.add(object);\n        object.owner = this;\n        this.objectsById.set(object.id, object);\n    }\n    removeOwnedObject(object: PlayerOwnedObject): void {\n        const objects = this.objectsByType.get(object.type);\n        if (!objects || !objects.has(object)) {\n            throw new Error(`GameObject ${object.name} does not belong to player ${this.name}`);\n        }\n        objects.delete(object);\n        this.objectsById.delete(object.id);\n    }\n    getOwnedObjectById(id: string): PlayerOwnedObject | undefined {\n        return this.objectsById.get(id);\n    }\n    getOwnedObjectsByType(type: ObjectType, includeLimbo: boolean = false): PlayerOwnedObject[] {\n        let objects = [...(this.objectsByType.get(type) || new Set())];\n        if (!includeLimbo) {\n            objects = objects.filter(obj => !obj.limboData);\n        }\n        return objects;\n    }\n    getOwnedObjects(includeLimbo: boolean = false): PlayerOwnedObject[] {\n        let objects: PlayerOwnedObject[] = [];\n        [...this.objectsByType.values()].forEach(set => {\n            set.forEach(obj => objects.push(obj));\n        });\n        if (!includeLimbo) {\n            objects = objects.filter(obj => !obj.limboData);\n        }\n        return objects;\n    }\n    removeAllOwnedObjects(): void {\n        this.objectsByType.forEach(set => set.clear());\n        this.objectsById.clear();\n    }\n    get buildings(): Set<PlayerOwnedObject> {\n        return this.getOrCreateObjectsForType(ObjectType.Building);\n    }\n    addUnitsBuilt(object: PlayerOwnedObject, count: number): void {\n        this.unitsBuiltByType.set(object.type, (this.unitsBuiltByType.get(object.type) ?? 0) + count);\n        if (object.buildLimit < 0) {\n            this.limitedUnitsBuiltByName.set(object.name, (this.limitedUnitsBuiltByName.get(object.name) ?? 0) + count);\n        }\n    }\n    getUnitsBuilt(type?: ObjectType): number {\n        if (type !== undefined) {\n            return this.unitsBuiltByType.get(type) ?? 0;\n        }\n        return [...this.unitsBuiltByType.values()].reduce((sum, count) => sum + count, 0);\n    }\n    getLimitedUnitsBuilt(name: string): number {\n        return this.limitedUnitsBuiltByName.get(name) ?? 0;\n    }\n    addUnitsKilled(type: ObjectType, count: number): void {\n        this.unitsKilledByType.set(type, (this.unitsKilledByType.get(type) ?? 0) + count);\n    }\n    getUnitsKilled(type?: ObjectType): number {\n        if (type !== undefined) {\n            return this.unitsKilledByType.get(type) ?? 0;\n        }\n        return [...this.unitsKilledByType.values()].reduce((sum, count) => sum + count, 0);\n    }\n    addUnitsLost(type: ObjectType, count: number): void {\n        this.unitsLostByType.set(type, (this.unitsLostByType.get(type) ?? 0) + count);\n    }\n    getUnitsLost(type?: ObjectType): number {\n        if (type !== undefined) {\n            return this.unitsLostByType.get(type) ?? 0;\n        }\n        return [...this.unitsLostByType.values()].reduce((sum, count) => sum + count, 0);\n    }\n    isCombatant(): boolean {\n        return !this.isNeutral && !this.isObserver && !this.defeated;\n    }\n    canProduceVeteran(object: PlayerOwnedObject): boolean {\n        if (!this.production || !this.country) {\n            throw new Error(\"Non-combatants can't produce units\");\n        }\n        const queueType = this.production.getQueueTypeForObject(object);\n        const factoryType = this.production.getFactoryTypeForQueueType(queueType);\n        return (this.production.hasVeteranType(factoryType) ||\n            this.country.hasVeteranUnit(object.type, object.name));\n    }\n    getHash(): number {\n        return fnv32a([\n            this.credits,\n            ...this.traits.getAll().map(trait => trait.getHash?.() ?? 0)\n        ]);\n    }\n    debugGetState(): Record<string, any> {\n        return {\n            name: this.name,\n            credits: this.credits,\n            traits: this.traits.getAll().reduce((acc, trait) => {\n                const state = trait.debugGetState?.();\n                if (state !== undefined) {\n                    acc[trait.constructor.name] = state;\n                }\n                return acc;\n            }, {} as Record<string, any>)\n        };\n    }\n    dispose(): void {\n        this.traits.dispose();\n        this.production?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/game/PlayerList.ts",
    "content": "import { Player } from './Player';\nexport class PlayerList {\n    private players: Player[] = [];\n    addPlayer(player: Player): void {\n        this.players.push(player);\n    }\n    getPlayerAt(index: number): Player {\n        if (index >= this.players.length) {\n            throw new RangeError(`Player #${index} out of bounds`);\n        }\n        return this.players[index];\n    }\n    getPlayerByName(name: string): Player {\n        const player = this.players.find(p => p.name === name);\n        if (!player) {\n            throw new Error(`Player with name \"${name}\" not found`);\n        }\n        return player;\n    }\n    getPlayerNumber(player: Player): number {\n        const index = this.players.indexOf(player);\n        if (index === -1) {\n            throw new Error(`Player ${player.name} not found`);\n        }\n        return index;\n    }\n    getCombatants(): Player[] {\n        return this.players.filter(p => p.isCombatant());\n    }\n    getNonNeutral(): Player[] {\n        return this.players.filter(p => !p.isNeutral);\n    }\n    getCivilian(): Player | undefined {\n        return this.players.find(p => p.country?.side === 'Civilian');\n    }\n    getAll(): Player[] {\n        return this.players;\n    }\n}\n"
  },
  {
    "path": "src/game/Prng.ts",
    "content": "import MersenneTwister from \"mersenne-twister\";\nimport { Crc32 } from \"@/data/Crc32\";\nimport { binaryStringToUint8Array } from \"@/util/string\";\nexport class Prng {\n    private prng: MersenneTwister;\n    private lastRandom: number;\n    static factory(seed: number | string, sequence: number): Prng {\n        const numericSeed = Number.isNaN(Number(seed))\n            ? Crc32.calculateCrc(binaryStringToUint8Array(seed as string))\n            : Number(seed + \"\" + sequence);\n        return new Prng(numericSeed);\n    }\n    constructor(seed: number) {\n        this.prng = new MersenneTwister(seed);\n    }\n    generateRandomInt(min: number, max: number): number {\n        const random = this.prng.random();\n        this.lastRandom = random;\n        return Math.floor(random * (max - min + 1)) + min;\n    }\n    generateRandom(): number {\n        const random = this.prng.random();\n        this.lastRandom = random;\n        return random;\n    }\n    getLastRandom(): number {\n        return this.lastRandom;\n    }\n}\n"
  },
  {
    "path": "src/game/SideType.ts",
    "content": "export enum SideType {\n    GDI = 0,\n    Nod = 1,\n    Civilian = 2,\n    Mutant = 3\n}\n"
  },
  {
    "path": "src/game/StartingUnitsGenerator.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\ninterface Unit {\n    name: string;\n    cost: number;\n    isAvailableTo: (owner: any) => boolean;\n    hasOwner: (owner: any) => boolean;\n}\ninterface GeneratedUnit {\n    name: string;\n    type: ObjectType;\n    count: number;\n}\nexport class StartingUnitsGenerator {\n    static generate(multiplier: number, preferredUnits: string[], availableUnits: Unit[], owner: any): GeneratedUnit[] {\n        const totalCost = (availableUnits.reduce((sum, unit) => sum + unit.cost, 0) / availableUnits.length) * multiplier;\n        const generatedUnits: GeneratedUnit[] = [];\n        let remainingCost = totalCost;\n        const filteredUnits = availableUnits.filter(unit => unit.isAvailableTo(owner) && unit.hasOwner(owner));\n        const preferredUnitList = filteredUnits.filter(unit => preferredUnits.includes(unit.name));\n        for (const unit of preferredUnitList) {\n            if (remainingCost <= 0)\n                break;\n            const costPerUnit = (2 / 3) / preferredUnitList.length;\n            const unitCount = Math.ceil((costPerUnit * totalCost) / unit.cost);\n            remainingCost -= unitCount * unit.cost;\n            generatedUnits.push({\n                name: unit.name,\n                type: ObjectType.Vehicle,\n                count: unitCount\n            });\n        }\n        const remainingUnits = filteredUnits.filter(unit => !preferredUnitList.includes(unit));\n        const costPerRemainingUnit = remainingCost / remainingUnits.length;\n        for (const unit of remainingUnits) {\n            if (remainingCost <= 0)\n                break;\n            const unitCount = Math.ceil(costPerRemainingUnit / unit.cost);\n            remainingCost -= unitCount * unit.cost;\n            generatedUnits.push({\n                name: unit.name,\n                type: ObjectType.Infantry,\n                count: unitCount\n            });\n        }\n        return generatedUnits;\n    }\n}\n"
  },
  {
    "path": "src/game/SuperWeapon.ts",
    "content": "import { SuperWeaponReadyEvent } from './event/SuperWeaponReadyEvent';\nimport { GameSpeed } from './GameSpeed';\nexport enum SuperWeaponStatus {\n    Charging = 0,\n    Paused = 1,\n    Ready = 2\n}\nexport class SuperWeapon {\n    public name: string;\n    public rules: any;\n    public owner: any;\n    public oneTimeOnly: boolean;\n    public status: SuperWeaponStatus;\n    public isGift: boolean;\n    public rechargeTicks: number;\n    public chargeTicks: number;\n    constructor(name: string, rules: any, owner: any, oneTimeOnly: boolean = false) {\n        this.name = name;\n        this.rules = rules;\n        this.owner = owner;\n        this.oneTimeOnly = oneTimeOnly;\n        this.status = SuperWeaponStatus.Charging;\n        this.isGift = false;\n        this.rechargeTicks = 60 * rules.rechargeTime * GameSpeed.BASE_TICKS_PER_SECOND;\n        this.chargeTicks = this.rechargeTicks;\n        if (oneTimeOnly) {\n            this.status = SuperWeaponStatus.Ready;\n            this.chargeTicks = 0;\n        }\n    }\n    update(game: any): void {\n        if (this.chargeTicks > 0 && this.status !== SuperWeaponStatus.Paused) {\n            this.chargeTicks--;\n            if (this.chargeTicks === 0) {\n                this.status = SuperWeaponStatus.Ready;\n                game.events.dispatch(new SuperWeaponReadyEvent(this));\n            }\n        }\n    }\n    pauseTimer(): void {\n        this.status = SuperWeaponStatus.Paused;\n    }\n    resumeTimer(): void {\n        this.status = this.chargeTicks > 0 ? SuperWeaponStatus.Charging : SuperWeaponStatus.Ready;\n    }\n    resetTimer(): void {\n        this.chargeTicks = this.rechargeTicks;\n        if (this.status === SuperWeaponStatus.Ready) {\n            this.status = SuperWeaponStatus.Charging;\n        }\n    }\n    getTimerSeconds(): number {\n        return this.chargeTicks / GameSpeed.BASE_TICKS_PER_SECOND;\n    }\n    getChargeProgress(): number {\n        return (this.rechargeTicks - this.chargeTicks) / this.rechargeTicks;\n    }\n}\n"
  },
  {
    "path": "src/game/Target.ts",
    "content": "import { Coords } from './Coords';\nimport { LandType } from './type/LandType';\nexport class Target {\n    private tileOccupation: any;\n    private isOre: boolean;\n    private bridge?: any;\n    public tile: any;\n    public obj?: any;\n    constructor(obj: any, tile: any, tileOccupation: any) {\n        this.tileOccupation = tileOccupation;\n        this.isOre = false;\n        if (obj) {\n            if (obj.isOverlay() && obj.isBridge()) {\n                this.bridge = obj;\n                this.tile = tile;\n            }\n            else if (obj.isOverlay() && obj.isTiberium()) {\n                this.isOre = true;\n                this.tile = obj.tile;\n            }\n            else {\n                this.obj = obj;\n                this.tile = obj.isBuilding() ? obj.centerTile : obj.tile;\n            }\n        }\n        else {\n            if (tile.landType === LandType.Tiberium) {\n                this.isOre = true;\n            }\n            if (tile.onBridgeLandType !== undefined) {\n                this.bridge = tileOccupation.getBridgeOnTile(tile);\n            }\n            this.tile = tile;\n        }\n    }\n    equals(other: Target): boolean {\n        return (this.obj === other.obj &&\n            this.tile === other.tile &&\n            this.bridge === other.bridge &&\n            this.isOre === other.isOre);\n    }\n    getWorldCoords() {\n        return this.obj\n            ? this.obj.position.worldPosition\n            : Coords.tile3dToWorld(this.tile.rx + 0.5, this.tile.ry + 0.5, this.tile.z + (this.bridge?.tileElevation ?? 0));\n    }\n    isBridge(): boolean {\n        return !this.obj && !!this.bridge;\n    }\n    getBridge() {\n        return (this.bridge ||\n            (this.obj?.isUnit() && this.obj.onBridge\n                ? this.tileOccupation.getBridgeOnTile(this.obj.tile)\n                : undefined));\n    }\n}\n"
  },
  {
    "path": "src/game/Traits.ts",
    "content": "export class Traits {\n    private allTraits: any[] = [];\n    private traitsByTypeCache: Map<any, any[]> = new Map();\n    add(trait: any): void {\n        this.allTraits.push(trait);\n        this.traitsByTypeCache.clear();\n    }\n    addToFront(trait: any): void {\n        this.allTraits.unshift(trait);\n        this.traitsByTypeCache.clear();\n    }\n    remove(trait: any): void {\n        const index = this.allTraits.indexOf(trait);\n        if (index !== -1) {\n            this.allTraits.splice(index, 1);\n            this.traitsByTypeCache.clear();\n        }\n    }\n    filter(type: any): any[] {\n        let cached = this.traitsByTypeCache.get(type);\n        if (cached) {\n            return cached;\n        }\n        cached = typeof type === 'function'\n            ? this.allTraits.filter(trait => trait instanceof type)\n            : this.allTraits.filter(trait => this.traitImplements(trait, type));\n        this.traitsByTypeCache.set(type, cached);\n        return cached;\n    }\n    get(type: any): any {\n        const trait = this.find(type);\n        if (!trait) {\n            throw new Error(\"No matching trait found\");\n        }\n        return trait;\n    }\n    find(type: any): any {\n        return this.filter(type)[0];\n    }\n    getAll(): any[] {\n        return this.allTraits;\n    }\n    private traitImplements(trait: any, type: any): boolean {\n        for (const prop of Object.getOwnPropertyNames(type)) {\n            if (trait[type[prop]] === undefined) {\n                return false;\n            }\n        }\n        return true;\n    }\n    clear(): void {\n        this.allTraits.length = 0;\n        this.traitsByTypeCache.clear();\n    }\n    dispose(): void {\n        this.getAll().forEach(trait => trait.dispose?.());\n        this.clear();\n    }\n}\n"
  },
  {
    "path": "src/game/Warhead.ts",
    "content": "import { DeathType } from \"@/game/gameobject/common/DeathType\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { ScatterTask } from \"@/game/gameobject/task/ScatterTask\";\nimport { BridgeOverlayTypes, OverlayBridgeType } from \"@/game/map/BridgeOverlayTypes\";\nimport { NotifyAttack } from \"@/game/trait/interface/NotifyAttack\";\nimport { ArmorType } from \"@/game/type/ArmorType\";\nimport { CollisionType } from \"@/game/gameobject/unit/CollisionType\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { Coords } from \"@/game/Coords\";\nimport * as MathUtils from \"@/util/math\";\nimport { FacingUtil } from \"@/game/gameobject/unit/FacingUtil\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { WarheadDetonateEvent } from \"@/game/event/WarheadDetonateEvent\";\nimport { WeaponType } from \"@/game/WeaponType\";\nimport { WeaponRules } from \"@/game/rules/WeaponRules\";\nimport { IniSection } from \"@/data/IniSection\";\nimport { ProjectileRules } from \"@/game/rules/ProjectileRules\";\nimport { AnimTerrainEffect } from \"@/game/gameobject/common/AnimTerrainEffect\";\nimport { ObjectAttackedEvent } from \"@/game/event/ObjectAttackedEvent\";\ninterface GameObject {\n    isSpawned: boolean;\n    isDisposed: boolean;\n    isDestroyed: boolean;\n    isCrashing: boolean;\n    healthTrait?: HealthTrait;\n    rules: GameObjectRules;\n    position: Position;\n    direction: number;\n    owner: Player;\n    name: string;\n    zone?: ZoneType;\n    overlayId?: number;\n    tileElevation: number;\n    onBridge?: boolean;\n    isTechno(): boolean;\n    isUnit(): boolean;\n    isBuilding(): boolean;\n    isInfantry(): boolean;\n    isAircraft(): boolean;\n    isVehicle(): boolean;\n    isOverlay(): boolean;\n    isTerrain(): boolean;\n    isBridge(): boolean;\n    onAttack(source: GameObject, weaponInfo?: WeaponInfo): void;\n    applyRocking(direction: number, intensity: number): void;\n    getBridge?(): GameObject;\n}\ninterface TechnoObject extends GameObject {\n    warpedOutTrait: WarpedOutTrait;\n    invulnerableTrait: InvulnerableTrait;\n    veteranTrait?: VeteranTrait;\n    moveTrait: MoveTrait;\n    unitOrderTrait: UnitOrderTrait;\n    suppressionTrait?: SuppressionTrait;\n    missileSpawnTrait?: MissileSpawnTrait;\n    crashableTrait?: CrashableTrait;\n    submergibleTrait?: SubmergibleTrait;\n    delayedKillTrait?: DelayedKillTrait;\n}\ninterface UnitObject extends TechnoObject {\n    crateBonuses: CrateBonuses;\n}\ninterface InfantryObject extends UnitObject {\n    stance: StanceType;\n    isPanicked: boolean;\n    infDeathType: DeathType;\n}\ninterface HealthTrait {\n    health: number;\n    getHitPoints(): number;\n    inflictDamage(amount: number, weaponInfo?: WeaponInfo, gameWorld?: GameWorld): void;\n    healBy(amount: number, healer: GameObject, gameWorld: GameWorld): void;\n}\ninterface GameObjectRules {\n    armor: ArmorType;\n    warpable: boolean;\n    immune: boolean;\n    immuneToRadiation: boolean;\n    immuneToPsionics: boolean;\n    invisibleInGame: boolean;\n    fraidycat: boolean;\n    insignificant: boolean;\n    typeImmune: boolean;\n    wall: boolean;\n}\ninterface WarheadRules {\n    temporal: boolean;\n    radiation: boolean;\n    psychicDamage: boolean;\n    proneDamage: number;\n    verses: Map<ArmorType, number>;\n    wallAbsoluteDestroyer: boolean;\n    wall: boolean;\n    wood: boolean;\n    infDeath: DeathType;\n    affectsAllies: boolean;\n    causesDelayKill: boolean;\n    delayKillAtMax: number;\n    delayKillFrames: number;\n    rocker: boolean;\n    conventional: boolean;\n    emEffect: boolean;\n    animList: string[];\n    name: string;\n    cellSpread: number;\n    percentAtMax: number;\n    radLevel: number;\n}\ninterface WeaponInfo {\n    minRange: number;\n    range: number;\n    speed: number;\n    type: WeaponType;\n    rules: WeaponRules;\n    projectileRules: ProjectileRules;\n    warhead: Warhead;\n    weapon?: WeaponRules;\n    obj?: GameObject;\n    player?: Player;\n}\ninterface GameWorld {\n    map: GameMap;\n    alliances: AllianceManager;\n    traits: TraitContainer;\n    events: EventDispatcher;\n    rules: GameRules;\n    gameOpts: GameOptions;\n    mapRadiationTrait: MapRadiationTrait;\n    destroyObject(obj: GameObject, source?: WeaponInfo, cause?: any, isDirectHit?: boolean): void;\n    generateRandomInt(min: number, max: number): number;\n}\ninterface GameMap {\n    tiles: Tile[][];\n    mapBounds: Rectangle;\n    tileOccupation: TileOccupation;\n    getObjectsOnTile(tile: Position): GameObject[];\n}\ninterface Player {\n    isCombatant(): boolean;\n}\ninterface Position {\n    getMapPosition(): Vector3;\n    clone(): Position;\n    sub(other: Vector3): Position;\n}\ninterface Vector3 {\n    x: number;\n    y: number;\n    z: number;\n}\ninterface Rectangle {\n    width: number;\n    height: number;\n}\ninterface WarpedOutTrait {\n    isInvulnerable(): boolean;\n}\ninterface InvulnerableTrait {\n    isActive(): boolean;\n}\ninterface VeteranTrait {\n    getVeteranArmorMultiplier(): number;\n}\ninterface MoveTrait {\n    reservedPathNodes: PathNode[];\n    isIdle(): boolean;\n}\ninterface PathNode {\n    tile: Position;\n}\ninterface UnitOrderTrait {\n    hasTasks(): boolean;\n    addTask(task: any): void;\n}\ninterface SuppressionTrait {\n    isSuppressed(): boolean;\n    suppress(): void;\n}\ninterface MissileSpawnTrait {\n}\ninterface CrashableTrait {\n    crash(source?: WeaponInfo): void;\n}\ninterface SubmergibleTrait {\n}\ninterface DelayedKillTrait {\n    isActive(): boolean;\n    activate(frames: number, weaponInfo: WeaponInfo): void;\n}\ninterface CrateBonuses {\n    armor: number;\n}\ninterface TraitContainer {\n    filter(trait: any): any[];\n}\ninterface EventDispatcher {\n    dispatch(event: any): void;\n}\ninterface GameRules {\n    audioVisual: AudioVisualRules;\n    combatDamage: CombatDamageRules;\n}\ninterface AudioVisualRules {\n    weaponNullifyAnim: string;\n    weatherConBoltExplosion: string;\n}\ninterface CombatDamageRules {\n    splashList: string[];\n    c4Warhead: string;\n}\ninterface GameOptions {\n    destroyableBridges: boolean;\n}\ninterface MapRadiationTrait {\n    createRadSite(position: Position, level: number, radius: number): void;\n}\ninterface AllianceManager {\n    areAllied(player1: Player, player2: Player): boolean;\n}\ninterface Tile {\n}\ninterface TileOccupation {\n}\nexport class Warhead {\n    static readonly SPECIAL_WARHEAD_NAME = \"Special\";\n    static readonly HE_WARHEAD_NAME = \"HE\";\n    constructor(public rules: WarheadRules) { }\n    canDamage(obj: GameObject, tile: Position, zone: ZoneType): boolean {\n        if (!obj.isSpawned || obj.isDisposed || obj.isDestroyed || obj.isCrashing) {\n            return false;\n        }\n        if (obj.isTechno() && (obj as TechnoObject).warpedOutTrait.isInvulnerable() && !this.rules.temporal) {\n            return false;\n        }\n        if (obj.isUnit()) {\n            const unitObj = obj as UnitObject;\n            if (unitObj.moveTrait.reservedPathNodes.find(node => node.tile === tile)) {\n                return false;\n            }\n        }\n        if (!obj.healthTrait) {\n            return false;\n        }\n        if (obj.isUnit() && obj.zone === ZoneType.Air && zone !== ZoneType.Air) {\n            return false;\n        }\n        if (!obj.isUnit() && zone === ZoneType.Air) {\n            return false;\n        }\n        if (obj.isBuilding() && obj.rules.invisibleInGame) {\n            return false;\n        }\n        if ((obj.isTechno() || obj.isTerrain()) && obj.rules.immune && !this.rules.temporal) {\n            return false;\n        }\n        if (obj.isTechno() && !obj.rules.warpable && this.rules.temporal) {\n            return false;\n        }\n        if (this.rules.radiation && (!obj.isUnit() || obj.rules.immuneToRadiation)) {\n            return false;\n        }\n        if (this.rules.psychicDamage && !obj.isInfantry()) {\n            return false;\n        }\n        if (obj.isOverlay() && BridgeOverlayTypes.isLowBridgeHead(obj.overlayId!)) {\n            return false;\n        }\n        return true;\n    }\n    computeDamage(baseDamage: number, target: GameObject, gameWorld: GameWorld, isWeatherStorm = false): number {\n        let damage = baseDamage;\n        if (damage > 0 && target.isTechno() && (target as TechnoObject).invulnerableTrait.isActive()) {\n            return 0;\n        }\n        if (target.isAircraft()) {\n            const aircraft = target as TechnoObject;\n            if (aircraft.missileSpawnTrait && target.zone !== ZoneType.Air) {\n                return 0;\n            }\n        }\n        if (!gameWorld.gameOpts.destroyableBridges && target.isOverlay() && target.isBridge()) {\n            return 0;\n        }\n        if (!this.rules.radiation && !this.rules.temporal && target.isInfantry()) {\n            const infantry = target as InfantryObject;\n            if (infantry.stance === StanceType.Prone) {\n                damage *= this.rules.proneDamage;\n            }\n        }\n        if (target.isTechno() || target.isOverlay() || target.isTerrain()) {\n            let armorType = target.isTerrain() ? ArmorType.Wood : target.rules.armor;\n            if (target.isOverlay() && target.isBridge()) {\n                const bridgeType = BridgeOverlayTypes.getOverlayBridgeType(target.overlayId!);\n                if (bridgeType === OverlayBridgeType.Wood) {\n                    armorType = ArmorType.Wood;\n                }\n                else if (bridgeType === OverlayBridgeType.Concrete) {\n                    armorType = ArmorType.Concrete;\n                }\n            }\n            if (!(isWeatherStorm && target.isOverlay() && (target.isBridge() || target.rules.wall))) {\n                damage *= this.rules.verses.get(armorType) || 1;\n            }\n            if (damage > 0 && target.isTechno()) {\n                const techno = target as TechnoObject;\n                if (techno.veteranTrait) {\n                    damage /= techno.veteranTrait.getVeteranArmorMultiplier();\n                }\n            }\n            if (damage > 0 && target.isUnit()) {\n                const unit = target as UnitObject;\n                damage /= unit.crateBonuses.armor;\n            }\n        }\n        if ((target.isOverlay() || target.isBuilding()) && target.rules.wall) {\n            if (this.rules.wallAbsoluteDestroyer) {\n                damage = Number.POSITIVE_INFINITY;\n            }\n            else if (!this.rules.wall && !(this.rules.wood && target.rules.armor === ArmorType.Wood)) {\n                damage = 0;\n            }\n        }\n        if (target.isOverlay() && target.isBridge() && !this.rules.wall) {\n            damage = 0;\n        }\n        return damage > 0 ? Math.floor(damage) : Math.ceil(damage);\n    }\n    inflictDamage(damage: number, target: GameObject, weaponInfo: WeaponInfo | undefined, gameWorld: GameWorld, isDirectHit = false): boolean {\n        const healthTrait = target.healthTrait!;\n        if (damage === Number.POSITIVE_INFINITY) {\n            damage = healthTrait.getHitPoints();\n        }\n        healthTrait.inflictDamage(damage, weaponInfo, gameWorld);\n        gameWorld.traits.filter(NotifyAttack).forEach((trait: any) => {\n            trait[NotifyAttack.onAttack](target, weaponInfo?.obj, gameWorld);\n        });\n        target.onAttack(gameWorld as any, weaponInfo);\n        gameWorld.events.dispatch(new ObjectAttackedEvent(target, weaponInfo, isDirectHit));\n        if (target.isTechno() && !this.rules.temporal) {\n            this.suppressOrScatterTarget(target as TechnoObject, gameWorld);\n        }\n        if (!healthTrait.health) {\n            if (target.isInfantry()) {\n                (target as InfantryObject).infDeathType = this.rules.infDeath;\n            }\n            if (this.rules.temporal) {\n                (target as any).deathType = DeathType.Temporal;\n            }\n            if (target.isUnit() && (target as TechnoObject).crashableTrait && target.zone === ZoneType.Air && !this.rules.temporal) {\n                (target as TechnoObject).crashableTrait!.crash(weaponInfo);\n            }\n            else {\n                gameWorld.destroyObject(target, weaponInfo, undefined, isDirectHit);\n            }\n            return true;\n        }\n        return false;\n    }\n    private suppressOrScatterTarget(target: TechnoObject, gameWorld: GameWorld): void {\n        if (target.rules.fraidycat || (target.isVehicle() && !target.owner.isCombatant() && target.rules.insignificant)) {\n            if (!target.unitOrderTrait.hasTasks()) {\n                if (target.isInfantry()) {\n                    (target as InfantryObject).isPanicked = true;\n                }\n                target.unitOrderTrait.addTask(new ScatterTask(gameWorld, undefined as any, undefined as any));\n                if (target.isInfantry()) {\n                    target.unitOrderTrait.addTask(new CallbackTask(() => (target as InfantryObject).isPanicked = false).setCancellable(false));\n                }\n            }\n        }\n        else if (target.isInfantry()) {\n            const infantry = target as InfantryObject;\n            if ((infantry.moveTrait.isIdle() || infantry.suppressionTrait?.isSuppressed()) && infantry.suppressionTrait) {\n                infantry.suppressionTrait.suppress();\n            }\n        }\n    }\n    createDummyWeaponInfo(): WeaponInfo {\n        return {\n            minRange: 0,\n            range: 0,\n            speed: Number.POSITIVE_INFINITY,\n            type: WeaponType.Primary,\n            rules: new WeaponRules(new IniSection(\"Dummy\")),\n            projectileRules: new ProjectileRules(ObjectType.Projectile, new IniSection(\"Dummy\")),\n            warhead: this\n        };\n    }\n    detonate(gameWorld: GameWorld, baseDamage: number, centerTile: Position, elevation: number, centerCoords: Vector3, zone: ZoneType, collisionType: CollisionType | undefined, target: {\n        obj?: GameObject;\n        getBridge?(): GameObject;\n    }, weaponInfo: WeaponInfo | undefined, friendly: boolean, areaEffectSmudge: string | undefined, customSpread?: number, isWeatherStorm = false): void {\n        const weapon = weaponInfo?.weapon ?? this.createDummyWeaponInfo() as any;\n        const sourceObj = weaponInfo?.obj;\n        const sourcePlayer = weaponInfo?.player;\n        const cellSpread = customSpread ? customSpread / Coords.LEPTONS_PER_TILE : this.rules.cellSpread;\n        const percentAtMax = this.rules.percentAtMax;\n        const processedObjects = new Set<GameObject>();\n        const objectDistances = new Map<GameObject, number[]>();\n        const rangeHelper = new RangeHelper(gameWorld.map.tileOccupation as any);\n        const tileFinder = new RadialTileFinder(gameWorld.map.tiles as any, gameWorld.map.mapBounds as any, centerTile as any, { width: 1, height: 1 }, 0, Math.ceil(cellSpread), () => true);\n        let currentTile: any;\n        while ((currentTile = tileFinder.getNextTile())) {\n            for (const obj of gameWorld.map.getObjectsOnTile(currentTile)) {\n                if (processedObjects.has(obj) && !obj.isBuilding())\n                    continue;\n                if (collisionType === CollisionType.UnderBridge && obj.isUnit() && (obj as UnitObject).onBridge)\n                    continue;\n                if (sourceObj && obj.isTechno() && obj.rules.typeImmune && obj.owner === sourcePlayer && obj.name === sourceObj.name)\n                    continue;\n                if (!this.canDamage(obj, currentTile, zone))\n                    continue;\n                if (obj.isOverlay()) {\n                    if ((!collisionType && Math.abs(obj.tileElevation - elevation) > 0.1) ||\n                        (collisionType === CollisionType.OnBridge && !obj.isBridge())) {\n                        continue;\n                    }\n                }\n                let distance: number;\n                if (obj.isBuilding()) {\n                    distance = currentTile === centerTile ? 0 : rangeHelper.distance3(currentTile as any, centerCoords) / Coords.LEPTONS_PER_TILE;\n                }\n                else if (obj.isTerrain() || obj.isOverlay()) {\n                    distance = rangeHelper.distance3(currentTile as any, centerTile as any) / Coords.LEPTONS_PER_TILE;\n                }\n                else {\n                    distance = rangeHelper.distance3(obj as any, centerCoords) / Coords.LEPTONS_PER_TILE;\n                }\n                if (distance < 0.001)\n                    distance = 0;\n                if (friendly && obj.isInfantry() && sourcePlayer) {\n                    if (obj.owner === sourcePlayer || gameWorld.alliances.areAllied(obj.owner, sourcePlayer)) {\n                        continue;\n                    }\n                }\n                if (!cellSpread) {\n                    if (obj.isTerrain()) {\n                        if (currentTile !== centerTile || !this.rules.wall)\n                            continue;\n                    }\n                    else if (!friendly && (currentTile !== centerTile || (!obj.isBuilding() && obj !== (target.obj || target.getBridge?.())))) {\n                        continue;\n                    }\n                }\n                if (cellSpread && distance > cellSpread)\n                    continue;\n                processedObjects.add(obj);\n                const distances = obj.isBuilding() ? (objectDistances.get(obj) || []).concat(distance) : [distance];\n                objectDistances.set(obj, distances);\n            }\n        }\n        let hasInvulnerableHit = false;\n        let directHitTarget: GameObject | undefined;\n        for (const obj of processedObjects) {\n            if (obj.isDestroyed || obj.isCrashing)\n                continue;\n            let damage = this.computeDamage(baseDamage, obj, gameWorld, isWeatherStorm);\n            if (baseDamage > 0 && !this.rules.affectsAllies && obj.isTechno() && sourcePlayer) {\n                if (gameWorld.alliances.areAllied(obj.owner, sourcePlayer) || obj.owner === sourcePlayer) {\n                    damage = 0;\n                }\n            }\n            if (!damage)\n                continue;\n            for (const distance of objectDistances.get(obj)!) {\n                let finalDamage = damage;\n                if (cellSpread > 0 && Number.isFinite(finalDamage)) {\n                    finalDamage = MathUtils.lerp(finalDamage, percentAtMax * finalDamage, distance / cellSpread);\n                }\n                if (Math.abs(finalDamage) < 1 && (!cellSpread || finalDamage / damage >= 0.25)) {\n                    finalDamage = Math.sign(finalDamage);\n                }\n                finalDamage = finalDamage > 0 ? Math.floor(finalDamage) : Math.ceil(finalDamage);\n                if (!finalDamage)\n                    continue;\n                const healthTrait = obj.healthTrait!;\n                if (finalDamage < 0) {\n                    if (!sourceObj)\n                        throw new Error(\"Expected healer object to be set\");\n                    healthTrait.healBy(-finalDamage, sourceObj, gameWorld);\n                    if (healthTrait.health === 100)\n                        break;\n                }\n                else {\n                    if (obj === target.obj && distance < 1) {\n                        directHitTarget = obj;\n                    }\n                    if (this.rules.causesDelayKill && obj.isBuilding() && (obj as any).delayedKillTrait) {\n                        const currentHP = healthTrait.getHitPoints();\n                        if (finalDamage >= currentHP) {\n                            finalDamage = currentHP - 1;\n                            const delayedKill = (obj as any).delayedKillTrait;\n                            if (!delayedKill.isActive()) {\n                                const maxDelay = this.rules.delayKillAtMax;\n                                let delayFrames = this.rules.delayKillFrames;\n                                delayFrames = MathUtils.lerp(delayFrames, maxDelay * delayFrames, distance / cellSpread);\n                                delayedKill.activate(delayFrames, weaponInfo);\n                            }\n                        }\n                    }\n                    if (this.inflictDamage(finalDamage, obj, weaponInfo, gameWorld, !directHitTarget)) {\n                        break;\n                    }\n                    if (obj.isVehicle() && this.rules.rocker) {\n                        const rockIntensity = MathUtils.clamp(damage / 300, 0, 1);\n                        if (rockIntensity > 0) {\n                            const rockDirection = FacingUtil.fromMapCoords((obj.position.getMapPosition() as any).clone().sub(Coords.vecWorldToGround(centerCoords as any) as any) as any) - obj.direction;\n                            obj.applyRocking(rockDirection, rockIntensity);\n                        }\n                    }\n                }\n            }\n            if (obj.isTechno() && (obj as TechnoObject).invulnerableTrait.isActive()) {\n                hasInvulnerableHit = true;\n            }\n        }\n        const radLevel = (weapon as any).rules.radLevel;\n        if (radLevel && cellSpread) {\n            gameWorld.mapRadiationTrait.createRadSite(centerTile, radLevel, cellSpread + 1);\n        }\n        const animation = isWeatherStorm ? undefined :\n            hasInvulnerableHit ? gameWorld.rules.audioVisual.weaponNullifyAnim :\n                this.pickExplodeAnim(baseDamage, directHitTarget, zone, gameWorld, isWeatherStorm);\n        if (!hasInvulnerableHit && zone === ZoneType.Ground) {\n            const terrainEffect = new AnimTerrainEffect();\n            if (animation)\n                terrainEffect.destroyOre(animation, centerTile, gameWorld);\n            if (areaEffectSmudge)\n                terrainEffect.spawnSmudges(areaEffectSmudge, centerTile, gameWorld);\n            if (animation)\n                terrainEffect.spawnSmudges(animation, centerTile, gameWorld);\n        }\n        gameWorld.events.dispatch(new WarheadDetonateEvent(this, centerCoords, animation, isWeatherStorm));\n    }\n    private pickExplodeAnim(damage: number, directHitTarget: GameObject | undefined, zone: ZoneType, gameWorld: GameWorld, isWeatherStorm: boolean): string | undefined {\n        if (!damage)\n            return undefined;\n        if (isWeatherStorm) {\n            return gameWorld.rules.audioVisual.weatherConBoltExplosion;\n        }\n        if (this.rules.conventional && zone === ZoneType.Water) {\n            if (!directHitTarget || directHitTarget.isBuilding() ||\n                (directHitTarget.isVehicle() && (directHitTarget as TechnoObject).submergibleTrait)) {\n                const splashList = gameWorld.rules.combatDamage.splashList;\n                const index = MathUtils.clamp(Math.floor(damage / 50), 0, splashList.length - 1);\n                return splashList[index];\n            }\n        }\n        const animCount = this.rules.animList.length;\n        if (!animCount)\n            return undefined;\n        let animIndex: number;\n        if (gameWorld.rules.combatDamage.c4Warhead === this.rules.name) {\n            animIndex = animCount - 1;\n        }\n        else if (this.rules.emEffect) {\n            animIndex = gameWorld.generateRandomInt(0, animCount - 1);\n        }\n        else {\n            animIndex = MathUtils.clamp(Math.floor(damage / 25), 0, animCount - 1);\n        }\n        return this.rules.animList[animIndex];\n    }\n}\n"
  },
  {
    "path": "src/game/Weapon.ts",
    "content": "import { Warhead } from \"@/game/Warhead\";\nimport { FlhCoords } from \"@/game/art/FlhCoords\";\nimport { WeaponFireEvent } from \"@/game/event/WeaponFireEvent\";\nimport * as geometry from \"@/game/math/geometry\";\nimport { ObjectRules } from \"@/game/rules/ObjectRules\";\nimport { Coords } from \"@/game/Coords\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { WeaponTargeting } from \"@/game/WeaponTargeting\";\nimport { WeaponType } from \"@/game/WeaponType\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { Vector3 } from \"@/game/math/Vector3\";\ninterface GameMap {\n    isWithinHardBounds(position: any): boolean;\n}\ninterface GameEngine {\n    map: GameMap;\n    events: {\n        dispatch(event: WeaponFireEvent): void;\n    };\n    createProjectile(name: string, gameObject: GameObject, weapon: Weapon, target: Target, flag: boolean): GameObject;\n    spawnObject(obj: GameObject, tile: any): void;\n    unlimboObject(obj: GameObject, tile: any): void;\n    limboObject(obj: GameObject, options: {\n        selected: boolean;\n        controlGroup: number;\n    }): void;\n    getUnitSelection(): {\n        isSelected(obj: GameObject): boolean;\n        getOrCreateSelectionModel(obj: GameObject): {\n            getControlGroupNumber(): number;\n        };\n    };\n    generateRandomInt(min: number, max: number): number;\n    mapShroudTrait: {\n        getPlayerShroud(owner: any): {\n            isShrouded(tile: any, elevation: number): boolean;\n            revealTemporarily(obj: GameObject): void;\n        } | null;\n    };\n}\ninterface GameObject {\n    rules: any;\n    position: {\n        getMapPosition(): any;\n        moveToLeptons(position: any): void;\n        moveByLeptons(x: number, y: number): void;\n        moveByLeptons3(vector: Vector3): void;\n        tileElevation: number;\n        tile: any;\n        worldPosition: Vector3;\n    };\n    direction: number;\n    art: {\n        turretOffset: number;\n    };\n    tile: any;\n    tileElevation: number;\n    owner: {\n        removeOwnedObject(obj: GameObject): void;\n    };\n    baseDamageMultiplier?: number;\n    isBuilding(): boolean;\n    isUnit(): boolean;\n    isAircraft(): boolean;\n    isInfantry(): boolean;\n    isVehicle(): boolean;\n    isTechno(): boolean;\n    dispose(): void;\n    overpoweredTrait?: any;\n    primaryWeapon?: Weapon;\n    garrisonTrait?: {\n        isOccupied(): boolean;\n        units: {\n            length: number;\n        };\n    };\n    veteranTrait?: {\n        getVeteranRofMultiplier(): number;\n    };\n    ammoTrait?: {\n        ammo: number;\n    };\n    airSpawnTrait?: {\n        prepareLaunch(gameObject: GameObject, target: Target, engine: GameEngine): GameObject | null;\n        availableSpawns: number;\n    };\n    turretTrait?: {\n        facing: number;\n    };\n    cloakableTrait?: {\n        uncloak(engine: GameEngine): void;\n    };\n    parasiteableTrait?: {\n        beingBoarded: boolean;\n    };\n    crateBonuses: {\n        firepower: number;\n    };\n    getFoundationCenterOffset(): {\n        x: number;\n        y: number;\n    };\n}\ninterface Target {\n    obj?: GameObject;\n}\ninterface WeaponRules {\n    name: string;\n    warhead: string;\n    projectile: string;\n    spawner?: boolean;\n    minimumRange: number;\n    range: number;\n    rof: number;\n    burst: number;\n    iniSpeed: number;\n    limboLaunch?: boolean;\n    revealOnFire?: boolean;\n    decloakToFire?: boolean;\n}\ninterface ProjectileRules {\n    name: string;\n    arcing?: boolean;\n    rot?: boolean;\n    inviso?: boolean;\n    iniRot: number;\n}\ninterface WarheadRules {\n    parasite?: boolean;\n}\ninterface RulesEngine {\n    getWeapon(name: string): WeaponRules;\n    getWarhead(name: string): any;\n    getProjectile(name: string): ProjectileRules;\n    getObject(type: string, objectType: ObjectType): GameObject;\n    general: {\n        v3Rocket: {\n            type: string;\n        };\n        dMisl: {\n            type: string;\n        };\n    };\n    combatDamage: {\n        v3Warhead: string;\n        dMislWarhead: string;\n    };\n}\nconst ARCING_PROJECTILE_SPEED = 50;\nconst AIRCRAFT_BURST_COUNT_HIGH_ROT = 5;\nconst AIRCRAFT_BURST_COUNT_MEDIUM = 2;\nconst AIRCRAFT_BURST_COUNT_LOW = 1;\nexport class Weapon {\n    static readonly NUKE_PAYLOAD_NAME = \"NukePayload\";\n    public readonly type: WeaponType;\n    public readonly gameObject: GameObject;\n    public readonly rules: WeaponRules;\n    public readonly warhead: Warhead;\n    public readonly projectileRules: ProjectileRules;\n    public readonly flh: FlhCoords;\n    public readonly targeting: WeaponTargeting;\n    private cooldownTicks: number = 0;\n    private burstsLeft: number = 0;\n    private burstIndex: number = 0;\n    private useBurstDelay: boolean = false;\n    private lateralMuzzleMult: number = 1;\n    private distributedFireAngle: number;\n    static factory(weaponName: string, weaponType: WeaponType, gameObject: GameObject, rulesEngine: RulesEngine, flh?: FlhCoords): Weapon {\n        const weaponRules = rulesEngine.getWeapon(weaponName);\n        let warheadName = weaponRules.warhead;\n        if (warheadName === Warhead.SPECIAL_WARHEAD_NAME) {\n            warheadName = this.findSpecialWarheadName(weaponRules, gameObject, rulesEngine);\n        }\n        const warhead = new Warhead(rulesEngine.getWarhead(warheadName));\n        const projectileRules = rulesEngine.getProjectile(weaponRules.projectile);\n        const targeting = new WeaponTargeting(weaponType, projectileRules as any, weaponRules as any, warhead.rules, gameObject as any, rulesEngine.general as any);\n        return new this(weaponType, gameObject, weaponRules, warhead, projectileRules, flh || new FlhCoords(), targeting);\n    }\n    static findSpecialWarheadName(weaponRules: WeaponRules, gameObject: GameObject, rulesEngine: RulesEngine): string {\n        if (!weaponRules.spawner) {\n            throw new Error(`Weapon \"${weaponRules.name}\" can't use \"Special\" warhead without Spawner=yes`);\n        }\n        let warheadName: string;\n        if ((gameObject as any).rules.spawns === rulesEngine.general.v3Rocket.type) {\n            warheadName = rulesEngine.combatDamage.v3Warhead;\n        }\n        else if ((gameObject as any).rules.spawns === rulesEngine.general.dMisl.type) {\n            warheadName = rulesEngine.combatDamage.dMislWarhead;\n        }\n        else {\n            if (!(gameObject as any).rules.spawns) {\n                throw new Error(`Can't use \"Special\" warhead on unit type \"${(gameObject as any).rules.name || (gameObject as any).name}\" without \"Spawns\"`);\n            }\n            const spawnedUnitRules: any = rulesEngine.getObject((gameObject as any).rules.spawns, ObjectType.Aircraft);\n            if (!spawnedUnitRules.primary) {\n                throw new Error(`Spawned unit doesn't have a primary weapon`);\n            }\n            warheadName = rulesEngine.getWeapon(spawnedUnitRules.primary).warhead;\n        }\n        return warheadName;\n    }\n    static computeSpeed(weaponRules: WeaponRules, projectileRules: ProjectileRules): number {\n        if (projectileRules.arcing) {\n            return 0.75 * ObjectRules.iniSpeedToLeptonsPerTick(ARCING_PROJECTILE_SPEED, 100);\n        }\n        if (!projectileRules.rot ||\n            projectileRules.inviso ||\n            (weaponRules as any).isLaser ||\n            (weaponRules as any).isElectricBolt) {\n            return Number.POSITIVE_INFINITY;\n        }\n        return (weaponRules as any).speed;\n    }\n    constructor(type: WeaponType, gameObject: GameObject, rules: WeaponRules, warhead: Warhead, projectileRules: ProjectileRules, flh: FlhCoords, targeting: WeaponTargeting) {\n        this.type = type;\n        this.gameObject = gameObject;\n        this.rules = rules;\n        this.warhead = warhead;\n        this.projectileRules = projectileRules;\n        this.flh = flh;\n        this.targeting = targeting;\n        this.distributedFireAngle =\n            gameObject.rules.distributedFire && gameObject.rules.radialFireSegments ? -90 : 0;\n    }\n    get name(): string {\n        return this.rules.name;\n    }\n    get minRange(): number {\n        return this.rules.minimumRange;\n    }\n    get range(): number {\n        if (this.gameObject.isBuilding() &&\n            !this.gameObject.overpoweredTrait &&\n            this.type === WeaponType.Secondary &&\n            this.gameObject.primaryWeapon) {\n            return Math.min(this.gameObject.primaryWeapon.rules.range, this.rules.range);\n        }\n        return this.rules.range;\n    }\n    get speed(): number {\n        return Weapon.computeSpeed(this.rules, this.projectileRules);\n    }\n    get rof(): number {\n        let rateOfFire = this.rules.rof;\n        if (this.gameObject.isBuilding() &&\n            this.gameObject.garrisonTrait?.isOccupied()) {\n            rateOfFire /= this.gameObject.garrisonTrait.units.length;\n        }\n        if (this.gameObject.veteranTrait) {\n            rateOfFire *= this.gameObject.veteranTrait.getVeteranRofMultiplier();\n        }\n        return Math.floor(rateOfFire);\n    }\n    getCooldownTicks(): number {\n        return this.cooldownTicks;\n    }\n    expireCooldown(): void {\n        this.cooldownTicks = 0;\n    }\n    resetCooldown(): void {\n        this.cooldownTicks = this.rof;\n    }\n    hasBurstsLeft(): boolean {\n        return this.burstsLeft > 0;\n    }\n    resetBursts(): void {\n        this.burstsLeft = 0;\n        this.burstIndex = 0;\n        this.resetCooldown();\n        if (this.gameObject.ammoTrait && this.gameObject.ammoTrait.ammo > 0) {\n            this.gameObject.ammoTrait.ammo--;\n        }\n    }\n    tick(): void {\n        if (this.cooldownTicks > 0) {\n            this.cooldownTicks--;\n        }\n    }\n    getBurstsFired(): number {\n        return this.burstIndex;\n    }\n    fire(target: Target, engine: GameEngine, damageMultiplier: number = 1): void {\n        const gameObject = this.gameObject;\n        let spawnedProjectile: GameObject | null = null;\n        let availableSpawns = 0;\n        if (gameObject.airSpawnTrait && this.rules.spawner) {\n            spawnedProjectile = gameObject.airSpawnTrait.prepareLaunch(gameObject, target, engine);\n            availableSpawns = gameObject.airSpawnTrait.availableSpawns;\n            if (!spawnedProjectile) {\n                return;\n            }\n        }\n        if (this.burstsLeft > 0) {\n            this.burstsLeft--;\n            this.burstIndex++;\n            this.lateralMuzzleMult *= -1;\n        }\n        else {\n            this.useBurstDelay = false;\n            this.burstIndex = 0;\n            if (spawnedProjectile) {\n                this.burstsLeft = availableSpawns;\n            }\n            else if (gameObject.isAircraft()) {\n                this.burstsLeft =\n                    this.projectileRules.iniRot <= 1\n                        ? AIRCRAFT_BURST_COUNT_HIGH_ROT - 1\n                        : gameObject.rules.fighter\n                            ? AIRCRAFT_BURST_COUNT_LOW - 1\n                            : AIRCRAFT_BURST_COUNT_MEDIUM - 1;\n            }\n            else {\n                this.burstsLeft = this.rules.burst - 1;\n                this.useBurstDelay = true;\n            }\n            this.lateralMuzzleMult = 1;\n        }\n        if (this.burstsLeft > 0) {\n            if (spawnedProjectile && availableSpawns > 0) {\n                this.cooldownTicks = this.rules.iniSpeed;\n            }\n            else if (gameObject.isAircraft()) {\n                this.cooldownTicks = this.rules.rof;\n            }\n            else {\n                this.cooldownTicks =\n                    this.useBurstDelay && gameObject.rules.burstDelay?.[this.burstIndex] !== undefined\n                        ? gameObject.rules.burstDelay[this.burstIndex]\n                        : engine.generateRandomInt(3, 5);\n            }\n        }\n        else {\n            this.resetBursts();\n        }\n        if (this.rules.limboLaunch) {\n            const unitSelection = engine.getUnitSelection();\n            engine.limboObject(gameObject, {\n                selected: unitSelection.isSelected(gameObject),\n                controlGroup: unitSelection.getOrCreateSelectionModel(gameObject).getControlGroupNumber(),\n            });\n            if ((this.warhead.rules as any).parasite &&\n                (target.obj?.isVehicle() || target.obj?.isAircraft()) &&\n                target.obj.parasiteableTrait) {\n                target.obj.parasiteableTrait.beingBoarded = true;\n            }\n        }\n        const projectile = spawnedProjectile ??\n            engine.createProjectile(this.projectileRules.name, gameObject, this, target, false);\n        if (!projectile.isAircraft()) {\n            projectile.baseDamageMultiplier =\n                damageMultiplier *\n                    (gameObject.isUnit() ? gameObject.crateBonuses.firepower : 1);\n        }\n        const firingFlh = this.flh.clone();\n        firingFlh.lateral *= this.lateralMuzzleMult;\n        const gameObjectPosition = gameObject.position.getMapPosition();\n        if (!engine.map.isWithinHardBounds(gameObjectPosition)) {\n            if (spawnedProjectile) {\n                spawnedProjectile.owner.removeOwnedObject(spawnedProjectile);\n                spawnedProjectile.dispose();\n            }\n            return;\n        }\n        projectile.position.moveToLeptons(gameObjectPosition);\n        projectile.position.tileElevation = gameObject.position.tileElevation;\n        let muzzleOffset = new Vector2(firingFlh.lateral, firingFlh.forward);\n        const muzzleFacing = this.getMuzzleFacing() + this.distributedFireAngle;\n        muzzleOffset = geometry.rotateVec2(muzzleOffset, muzzleFacing);\n        let turretOffset = new Vector2(0, gameObject.art.turretOffset);\n        turretOffset = geometry.rotateVec2(turretOffset, gameObject.direction);\n        muzzleOffset.add(turretOffset);\n        if (gameObject.rules.radialFireSegments && gameObject.rules.distributedFire) {\n            const segmentAngle = Math.floor(180 / gameObject.rules.radialFireSegments);\n            this.distributedFireAngle =\n                ((this.distributedFireAngle + segmentAngle + 90) % 180) - 90;\n        }\n        projectile.direction = muzzleFacing;\n        if (gameObject.isBuilding() && gameObject.rules.turretAnim) {\n            const turretAnimOffset = Coords.screenDistanceToWorld(gameObject.rules.turretAnimX, gameObject.rules.turretAnimY);\n            const foundationOffset = gameObject.getFoundationCenterOffset();\n            projectile.position.moveByLeptons(-foundationOffset.x + turretAnimOffset.x, -foundationOffset.y + turretAnimOffset.y);\n        }\n        const position3D = new Vector3(muzzleOffset.x, firingFlh.vertical, -muzzleOffset.y);\n        const worldPosition = position3D.clone().add(projectile.position.worldPosition);\n        if (engine.map.isWithinHardBounds(worldPosition)) {\n            projectile.position.moveByLeptons3(position3D);\n        }\n        if (projectile.tileElevation < 0) {\n            projectile.position.tileElevation = 0;\n        }\n        if (projectile.isAircraft()) {\n            engine.unlimboObject(projectile, projectile.position.tile);\n        }\n        else {\n            engine.spawnObject(projectile, projectile.position.tile);\n        }\n        if (this.rules.revealOnFire && target.obj?.isTechno()) {\n            const playerShroud = engine.mapShroudTrait.getPlayerShroud(target.obj.owner);\n            if (playerShroud?.isShrouded(gameObject.tile, gameObject.tileElevation)) {\n                playerShroud.revealTemporarily(gameObject);\n            }\n        }\n        if (this.rules.decloakToFire) {\n            gameObject.cloakableTrait?.uncloak(engine);\n        }\n        engine.events.dispatch(new WeaponFireEvent(this, gameObject));\n    }\n    getMuzzleFacing(): number {\n        const gameObject = this.gameObject;\n        if (!gameObject.isInfantry() &&\n            !gameObject.isAircraft() &&\n            (gameObject.isBuilding() || gameObject.isVehicle()) &&\n            gameObject.turretTrait) {\n            return gameObject.turretTrait.facing;\n        }\n        return gameObject.direction;\n    }\n}\n"
  },
  {
    "path": "src/game/WeaponInfo.ts",
    "content": "export class WeaponInfo {\n}\n"
  },
  {
    "path": "src/game/WeaponTargeting.ts",
    "content": "import { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { LandTargeting } from \"@/game/type/LandTargeting\";\nimport { LandType } from \"@/game/type/LandType\";\nimport { NavalTargeting } from \"@/game/type/NavalTargeting\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { WeaponType } from \"@/game/WeaponType\";\ninterface GameObject {\n    name: string;\n    owner: any;\n    rules: {\n        attackCursorOnFriendlies?: boolean;\n        ivan?: boolean;\n        natural?: boolean;\n        unnatural?: boolean;\n        spawned?: boolean;\n        organic?: boolean;\n        naval?: boolean;\n        speedType?: SpeedType;\n        navalTargeting: NavalTargeting;\n        landTargeting: LandTargeting;\n    };\n    zone?: ZoneType;\n    stance?: StanceType;\n    tileElevation?: number;\n    healthTrait?: {\n        health: number;\n    };\n    overpoweredTrait?: any;\n    cloakableTrait?: {\n        isCloaked(): boolean;\n    };\n    tntChargeTrait?: {\n        hasCharge(): boolean;\n    };\n    parasiteableTrait?: {\n        isInfested(): boolean;\n    };\n    mindControllableTrait?: any;\n    warpedOutTrait?: {\n        isInvulnerable(): boolean;\n    };\n    submergibleTrait?: {\n        isSubmerged(): boolean;\n    };\n    isUnit(): boolean;\n    isInfantry(): boolean;\n    isVehicle(): boolean;\n    isAircraft(): boolean;\n    isBuilding(): boolean;\n    isTechno(): boolean;\n}\ninterface ProjectileRules {\n    isAntiGround: boolean;\n    isAntiAir: boolean;\n}\ninterface WeaponRules {\n    damage: number;\n    limboLaunch?: boolean;\n}\ninterface WarheadRules {\n    electricAssault?: boolean;\n    bombDisarm?: boolean;\n    mindControl?: boolean;\n    parasite?: boolean;\n    temporal?: boolean;\n}\ninterface GeneralRules {\n    prism: {\n        type: string;\n    };\n}\ninterface GameContext {\n    landType: LandType;\n}\ninterface AllianceSystem {\n    areFriendly(unit1: GameObject, unit2: GameObject): boolean;\n    alliances: {\n        haveSharedIntel(owner1: any, owner2: any): boolean;\n    };\n}\ntype TargetCheckFunction = (target?: GameObject, context?: GameContext, alliances?: AllianceSystem, forcefire?: boolean, shift?: boolean) => boolean;\nexport class WeaponTargeting {\n    private targetChecks: TargetCheckFunction[] = [];\n    constructor(private weaponType: WeaponType, private projectileRules: ProjectileRules, private weaponRules: WeaponRules, private warheadRules: WarheadRules, private gameObject: GameObject, private generalRules: GeneralRules) {\n        this.initConditions();\n    }\n    private initConditions(): void {\n        if (!this.projectileRules.isAntiGround) {\n            this.targetChecks.push((target) => !!target);\n        }\n        const prismType = this.generalRules.prism.type;\n        if (this.gameObject.name === prismType && this.weaponType === WeaponType.Secondary) {\n            this.targetChecks.push((target, context, alliances, forcefire, shift) => !(!shift || !target?.isBuilding() || target.name !== prismType || target.owner !== this.gameObject.owner));\n        }\n        else if (this.warheadRules.electricAssault) {\n            this.targetChecks.push((target, context, alliances, forcefire, shift) => !((!forcefire && !shift) || !target?.isBuilding() || !target.overpoweredTrait || target.owner !== this.gameObject.owner));\n        }\n        else if (this.weaponRules.damage < 0) {\n            this.targetChecks.push((target, context, alliances) => !!(target !== this.gameObject &&\n                target?.isUnit() &&\n                alliances?.areFriendly(target, this.gameObject) &&\n                target.healthTrait && target.healthTrait.health < 100 &&\n                this.gameObject.isAircraft() === target.isAircraft()));\n        }\n        else {\n            if (this.gameObject.rules.attackCursorOnFriendlies || this.warheadRules.bombDisarm) {\n                this.targetChecks.push((target, context, alliances, forcefire, shift) => !shift && !!(!this.warheadRules.bombDisarm ||\n                    (target?.isTechno() && target.tntChargeTrait?.hasCharge())));\n            }\n            else {\n                this.targetChecks.push((target, context, alliances, forcefire) => !((!forcefire || this.warheadRules.mindControl) &&\n                    target?.isTechno() &&\n                    alliances?.areFriendly(target, this.gameObject)));\n            }\n            this.targetChecks.push((target, context, alliances) => !(target?.isTechno() &&\n                target.cloakableTrait?.isCloaked() &&\n                !alliances?.alliances.haveSharedIntel(this.gameObject.owner, target.owner)));\n            if (this.weaponRules.limboLaunch) {\n                this.targetChecks.push((target, context, alliances, forcefire, shift) => !(shift && target &&\n                    (target.isVehicle() || target.isAircraft()) &&\n                    target.parasiteableTrait?.isInfested()));\n            }\n            if (this.gameObject.rules.ivan) {\n                this.targetChecks.push((target) => !(!target?.isTechno() || !target.tntChargeTrait || target.tntChargeTrait.hasCharge()));\n            }\n            if (this.warheadRules.parasite) {\n                this.targetChecks.push((target, context, alliances, forcefire) => !!((!target && forcefire) ||\n                    target?.isInfantry() ||\n                    ((target?.isVehicle() || target?.isAircraft()) && target.parasiteableTrait)));\n            }\n            if (this.warheadRules.mindControl) {\n                this.targetChecks.push((target) => !(!target?.isTechno() || !target.mindControllableTrait));\n            }\n            if (!this.warheadRules.temporal) {\n                this.targetChecks.push((target, context, alliances, forcefire, shift) => !(shift && target?.isTechno() && target.warpedOutTrait?.isInvulnerable()));\n            }\n            if (this.gameObject.rules.natural) {\n                this.targetChecks.push((target) => !target?.isTechno() || !target.rules.unnatural);\n            }\n        }\n        this.targetChecks.push((target, context) => this.canTargetZone(target, context));\n    }\n    public canTarget(target?: GameObject, context?: GameContext, alliances?: AllianceSystem, forcefire?: boolean, shift?: boolean): boolean {\n        return this.targetChecks.every(check => check(target, context, alliances, forcefire, shift));\n    }\n    private canTargetZone(target?: GameObject, context?: GameContext): boolean {\n        let zone: ZoneType;\n        if (target?.isUnit()) {\n            if (target?.isInfantry() &&\n                target.stance === StanceType.Paradrop &&\n                (target.tileElevation ?? 0) > 2) {\n                return this.projectileRules.isAntiAir &&\n                    (this.projectileRules.isAntiGround || this.weaponType === WeaponType.Secondary);\n            }\n            if (target.zone === ZoneType.Air) {\n                return this.projectileRules.isAntiAir;\n            }\n            if (this.weaponType === WeaponType.Secondary &&\n                this.projectileRules.isAntiAir &&\n                !this.projectileRules.isAntiGround) {\n                return false;\n            }\n            zone = target.zone ?? ZoneType.Ground;\n        }\n        else {\n            zone = context?.landType === LandType.Water ? ZoneType.Water : ZoneType.Ground;\n        }\n        return zone === ZoneType.Water\n            ? this.canTargetNaval(this.gameObject.rules.navalTargeting, this.gameObject, target, this.weaponType)\n            : this.canTargetLand(this.gameObject.rules.landTargeting, this.weaponType);\n    }\n    private canTargetLand(landTargeting: LandTargeting, weaponType: WeaponType): boolean {\n        switch (landTargeting) {\n            case LandTargeting.LandOk:\n                return true;\n            case LandTargeting.LandNotOk:\n                return false;\n            case LandTargeting.LandSecondary:\n                return weaponType === WeaponType.Secondary;\n            default:\n                throw new Error(`Unhandled LandTargeting value \"${landTargeting}\"`);\n        }\n    }\n    private canTargetNaval(navalTargeting: NavalTargeting, shooter: GameObject, target?: GameObject, weaponType?: WeaponType): boolean {\n        switch (navalTargeting) {\n            case NavalTargeting.UnderwaterNever:\n                return !target || !(target.isVehicle() && target.submergibleTrait?.isSubmerged());\n            case NavalTargeting.UnderwaterSecondary:\n                return target && target.isVehicle() && target.submergibleTrait && !shooter.rules.spawned\n                    ? weaponType === WeaponType.Secondary\n                    : weaponType === WeaponType.Primary;\n            case NavalTargeting.UnderwaterOnly:\n                return !!(target && target.isVehicle() && target.submergibleTrait);\n            case NavalTargeting.OrganicSecondary:\n                return target?.isTechno() && target.rules.organic\n                    ? weaponType === WeaponType.Secondary\n                    : weaponType === WeaponType.Primary;\n            case NavalTargeting.SealSpecial:\n                return target?.isTechno() &&\n                    target.rules.naval &&\n                    !target.rules.organic &&\n                    (target.isBuilding() || target.rules.speedType === SpeedType.Float)\n                    ? weaponType === WeaponType.Secondary\n                    : weaponType === WeaponType.Primary;\n            case NavalTargeting.NavalAll:\n                return true;\n            case NavalTargeting.NavalNone:\n                return false;\n            default:\n                throw new Error(`Unhandled NavalTargeting value \"${navalTargeting}\"`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/WeaponType.ts",
    "content": "export enum WeaponType {\n    Primary = 0,\n    Secondary = 1,\n    DeathWeapon = 2\n}\n"
  },
  {
    "path": "src/game/World.ts",
    "content": "import { EventDispatcher } from '@/util/event';\nimport { GameObject } from './gameobject/GameObject';\nexport class World {\n    private allObjects: Map<number, GameObject>;\n    private _onObjectSpawned: EventDispatcher;\n    private _onObjectRemoved: EventDispatcher;\n    [key: string]: any;\n    constructor() {\n        this.allObjects = new Map();\n        this._onObjectSpawned = new EventDispatcher();\n        this._onObjectRemoved = new EventDispatcher();\n    }\n    get onObjectSpawned() {\n        return this._onObjectSpawned.asEvent();\n    }\n    get onObjectRemoved() {\n        return this._onObjectRemoved.asEvent();\n    }\n    spawnObject(object: GameObject, tile?: any): void {\n        if (this.allObjects.has(object.id)) {\n            throw new Error(\"Trying to add an already existing object\");\n        }\n        this.allObjects.set(object.id, object);\n        this._onObjectSpawned.dispatch(this, object);\n    }\n    removeObject(object: GameObject): void {\n        if (!this.allObjects.has(object.id)) {\n            throw new Error(\"Trying to remove non-existent object\");\n        }\n        this.allObjects.delete(object.id);\n        this._onObjectRemoved.dispatch(this, object);\n    }\n    hasObjectId(id: number): boolean {\n        return this.allObjects.has(id);\n    }\n    getObjectById(id: number): GameObject {\n        if (!this.allObjects.has(id)) {\n            throw new Error(`Object with id ${id} doesn't exist`);\n        }\n        return this.allObjects.get(id)!;\n    }\n    getAllObjects(): GameObject[] {\n        return [...this.allObjects.values()];\n    }\n}\n"
  },
  {
    "path": "src/game/action/Action.ts",
    "content": "import { ActionType } from './ActionType';\nexport abstract class Action {\n    protected actionType: ActionType;\n    public player: any;\n    constructor(actionType: ActionType) {\n        this.actionType = actionType;\n    }\n    abstract unserialize(data: any): void;\n    serialize(): Uint8Array {\n        return new Uint8Array();\n    }\n    print(): string {\n        return \"\";\n    }\n}\n"
  },
  {
    "path": "src/game/action/ActionFactory.ts",
    "content": "import { ActionType } from './ActionType';\nexport class ActionFactory {\n    private factories: Map<ActionType, any>;\n    constructor() {\n        this.factories = new Map();\n    }\n    registerFactory(actionType: ActionType, factory: any): void {\n        this.factories.set(actionType, factory);\n    }\n    create(actionType: ActionType): any {\n        const factory = this.factories.get(actionType);\n        if (!factory) {\n            throw new Error(`No factory registered for action type ${actionType}`);\n        }\n        return factory.create();\n    }\n}\n"
  },
  {
    "path": "src/game/action/ActionFactoryReg.ts",
    "content": "import { OrderActionContext } from './OrderActionContext';\nimport { ActionType } from './ActionType';\nimport { NoActionFactory } from './factories/NoActionFactory';\nimport { PlaceBuildingActionFactory } from './factories/PlaceBuildingActionFactory';\nimport { SellObjectActionFactory } from './factories/SellObjectActionFactory';\nimport { SelectUnitsActionFactory } from './factories/SelectUnitsActionFactory';\nimport { OrderUnitsActionFactory } from './factories/OrderUnitsActionFactory';\nimport { UpdateQueueActionFactory } from './factories/UpdateQueueActionFactory';\nimport { DropPlayerActionFactory } from './factories/DropPlayerActionFactory';\nimport { ToggleRepairActionFactory } from './factories/ToggleRepairActionFactory';\nimport { ToggleAllianceActionFactory } from './factories/ToggleAllianceFactory';\nimport { ActivateSuperWeaponActionFactory } from './factories/ActivateSuperWeaponActionFactory';\nimport { PingLocationActionFactory } from './factories/PingLocationActionFactory';\nimport { ObserveGameActionFactory } from './factories/ObserveGameActionFactory';\nimport { ResignGameActionFactory } from './factories/ResignGameActionFactory';\nimport { DebugActionFactory } from './factories/DebugActionFactory';\nexport class ActionFactoryReg {\n    register(actionRegistry: any, gameContext: any, playerContext: any): void {\n        const orderActionContext = new OrderActionContext();\n        actionRegistry.registerFactory(ActionType.NoAction, new NoActionFactory());\n        actionRegistry.registerFactory(ActionType.PlaceBuilding, new PlaceBuildingActionFactory(gameContext));\n        actionRegistry.registerFactory(ActionType.SellObject, new SellObjectActionFactory(gameContext));\n        actionRegistry.registerFactory(ActionType.ToggleRepair, new ToggleRepairActionFactory(gameContext));\n        actionRegistry.registerFactory(ActionType.SelectUnits, new SelectUnitsActionFactory(gameContext, orderActionContext));\n        actionRegistry.registerFactory(ActionType.OrderUnits, new OrderUnitsActionFactory(gameContext, gameContext.map, orderActionContext));\n        actionRegistry.registerFactory(ActionType.UpdateQueue, new UpdateQueueActionFactory(gameContext));\n        actionRegistry.registerFactory(ActionType.ToggleAlliance, new ToggleAllianceActionFactory(gameContext));\n        actionRegistry.registerFactory(ActionType.ActivateSuperWeapon, new ActivateSuperWeaponActionFactory(gameContext));\n        actionRegistry.registerFactory(ActionType.PingLocation, new PingLocationActionFactory(gameContext));\n        actionRegistry.registerFactory(ActionType.DropPlayer, new DropPlayerActionFactory(gameContext, playerContext));\n        actionRegistry.registerFactory(ActionType.ObserveGame, new ObserveGameActionFactory(gameContext));\n        actionRegistry.registerFactory(ActionType.ResignGame, new ResignGameActionFactory(gameContext, playerContext));\n        actionRegistry.registerFactory(ActionType.DebugCommand, new DebugActionFactory(gameContext));\n    }\n}\n"
  },
  {
    "path": "src/game/action/ActionQueue.ts",
    "content": "import { Action } from './Action';\nexport class ActionQueue {\n    private actions: Action[];\n    constructor() {\n        this.actions = [];\n    }\n    push(...actions: Action[]): void {\n        this.actions.push(...actions);\n    }\n    getLast(): Action | undefined {\n        return this.actions[this.actions.length - 1];\n    }\n    dequeueAll(): Action[] {\n        const actions = [...this.actions];\n        this.actions.length = 0;\n        return actions;\n    }\n    dequeueLast(): Action | undefined {\n        return this.actions.pop();\n    }\n    clear(): void {\n        this.actions.length = 0;\n    }\n}\n"
  },
  {
    "path": "src/game/action/ActionType.ts",
    "content": "export enum ActionType {\n    NoAction = 0,\n    DropPlayer = 1,\n    ObserveGame = 2,\n    ResignGame = 3,\n    DebugCommand = 4,\n    PlaceBuilding = 5,\n    SellObject = 6,\n    ToggleRepair = 7,\n    SelectUnits = 8,\n    OrderUnits = 9,\n    UpdateQueue = 10,\n    ToggleAlliance = 11,\n    ActivateSuperWeapon = 12,\n    PingLocation = 13\n}\n"
  },
  {
    "path": "src/game/action/ActivateSuperWeaponAction.ts",
    "content": "import { DataStream } from '@/data/DataStream';\nimport { Action } from '@/game/action/Action';\nimport { ActionType } from '@/game/action/ActionType';\nimport { SuperWeaponType } from '@/game/type/SuperWeaponType';\nimport { SuperWeaponsTrait } from '@/game/trait/SuperWeaponsTrait';\nimport { Game } from '@/game/Game';\nexport class ActivateSuperWeaponAction extends Action {\n    private game: Game;\n    private superWeaponType: number;\n    private tile: {\n        x: number;\n        y: number;\n    };\n    private tile2?: {\n        x: number;\n        y: number;\n    };\n    constructor(game: Game) {\n        super(ActionType.ActivateSuperWeapon);\n        this.game = game;\n    }\n    unserialize(data: Uint8Array): void {\n        const stream = new DataStream(data);\n        this.superWeaponType = stream.readUint8();\n        const tileCount = stream.readUint8();\n        this.tile = {\n            x: stream.readUint16(),\n            y: stream.readUint16()\n        };\n        if (tileCount > 2) {\n            this.tile2 = {\n                x: stream.readUint16(),\n                y: stream.readUint16()\n            };\n        }\n    }\n    serialize(): Uint8Array {\n        const stream = new DataStream(6 + (this.tile2 ? 4 : 0));\n        stream.dynamicSize = false;\n        stream.writeUint8(this.superWeaponType);\n        stream.writeUint8(this.tile2 ? 4 : 2);\n        stream.writeUint16(this.tile.x);\n        stream.writeUint16(this.tile.y);\n        if (this.tile2) {\n            stream.writeUint16(this.tile2.x);\n            stream.writeUint16(this.tile2.y);\n        }\n        return stream.toUint8Array();\n    }\n    print(): string {\n        return `Activate SuperW ${SuperWeaponType[this.superWeaponType]} at tile (${this.tile.x}, ${this.tile.y})` +\n            (this.tile2 ? `, (${this.tile2.x}, ${this.tile2.y})` : '');\n    }\n    process(): void {\n        const player = this.player;\n        const tile = this.game.map.tiles.getByMapCoords(this.tile.x, this.tile.y);\n        if (!tile) {\n            console.warn(`Tile ${this.tile.x},${this.tile.y} doesn't exist`);\n            return;\n        }\n        const tile2 = this.tile2\n            ? this.game.map.tiles.getByMapCoords(this.tile2.x, this.tile2.y)\n            : undefined;\n        this.game.traits\n            .get(SuperWeaponsTrait)\n            .activateSuperWeapon(this.superWeaponType, player, this.game, tile, tile2);\n    }\n}\n"
  },
  {
    "path": "src/game/action/DebugAction.ts",
    "content": "import { DataStream } from '@/data/DataStream';\nimport { Action } from '@/game/action/Action';\nimport { ActionType } from '@/game/action/ActionType';\nimport { Game } from '@/game/Game';\nexport enum DebugCommandType {\n    SetGlobalDebugText = 0,\n    SetUnitDebugText = 1\n}\nexport class DebugCommand {\n    constructor(public type: DebugCommandType, public params: {\n        unitId?: number;\n        label?: string;\n        text?: string;\n    }) { }\n}\nexport class DebugAction extends Action {\n    private game: Game;\n    private command: DebugCommand;\n    constructor(game: Game) {\n        super(ActionType.DebugCommand);\n        this.game = game;\n    }\n    unserialize(data: Uint8Array): void {\n        const stream = new DataStream(data);\n        const type = stream.readUint8();\n        if (type === DebugCommandType.SetUnitDebugText) {\n            this.command = new DebugCommand(type, {\n                unitId: stream.readUint32(),\n                label: stream.readCString() || undefined\n            });\n        }\n        else if (type === DebugCommandType.SetGlobalDebugText) {\n            this.command = new DebugCommand(type, {\n                text: stream.readCString()\n            });\n        }\n        else {\n            console.warn(`Debug command ${type} not implemented`);\n        }\n    }\n    serialize(): Uint8Array {\n        const stream = new DataStream();\n        stream.writeUint8(this.command.type);\n        if (this.command.type === DebugCommandType.SetUnitDebugText) {\n            const { unitId, label } = this.command.params;\n            stream.writeUint32(unitId);\n            stream.writeCString(label || '');\n        }\n        else if (this.command.type === DebugCommandType.SetGlobalDebugText) {\n            const { text } = this.command.params;\n            stream.writeCString(text);\n        }\n        else {\n            throw new Error(`Debug command ${this.command.type} not implemented`);\n        }\n        return stream.toUint8Array();\n    }\n    process(): void {\n        if (this.command.type === DebugCommandType.SetUnitDebugText) {\n            const { unitId, label } = this.command.params;\n            if (this.game.getWorld().hasObjectId(unitId)) {\n                const object = this.game.getObjectById(unitId);\n                if (object.isTechno()) {\n                    object.debugLabel = label;\n                }\n            }\n        }\n        else if (this.command.type === DebugCommandType.SetGlobalDebugText) {\n            const { text } = this.command.params;\n            this.game.debugText.value = text;\n        }\n        else {\n            console.warn(`Debug command ${this.command.type} not implemented`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/action/DropPlayerAction.ts",
    "content": "import { Action } from './Action';\nimport { ActionType } from './ActionType';\nimport { PlayerDroppedEvent } from '../event/PlayerDroppedEvent';\nimport { Game } from '../Game';\nexport class DropPlayerAction extends Action {\n    private game: Game;\n    private localPlayerName: string;\n    constructor(game: Game, localPlayerName: string) {\n        super(ActionType.DropPlayer);\n        this.game = game;\n        this.localPlayerName = localPlayerName;\n    }\n    unserialize(_data: Uint8Array): void { }\n    serialize(): Uint8Array {\n        return new Uint8Array();\n    }\n    process(): void {\n        if (this.localPlayerName !== this.player.name) {\n            const player = this.player;\n            if (!player.defeated) {\n                const redistributedAssets = this.game.redistributeAllPlayerAssets(player);\n                this.game.removeAllPlayerAssets(player);\n                player.dropped = true;\n                this.game.events.dispatch(new PlayerDroppedEvent(player, redistributedAssets));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/action/NoAction.ts",
    "content": "import { Action } from './Action';\nimport { ActionType } from './ActionType';\nexport class NoAction extends Action {\n    constructor() {\n        super(ActionType.NoAction);\n    }\n    unserialize(_data: Uint8Array): void { }\n    serialize(): Uint8Array {\n        return new Uint8Array();\n    }\n    process(): void { }\n}\n"
  },
  {
    "path": "src/game/action/ObserveGameAction.ts",
    "content": "import { Action } from './Action';\nimport { ActionType } from './ActionType';\nimport { PlayerResignedEvent } from '../event/PlayerResignedEvent';\nimport { PlayerDefeatedEvent } from '../event/PlayerDefeatedEvent';\nimport { RadarOnOffEvent } from '../event/RadarOnOffEvent';\nimport { Game } from '../Game';\nexport class ObserveGameAction extends Action {\n    private game: Game;\n    constructor(game: Game) {\n        super(ActionType.ObserveGame);\n        this.game = game;\n    }\n    unserialize(_data: Uint8Array): void { }\n    serialize(): Uint8Array {\n        return new Uint8Array();\n    }\n    process(): void {\n        const player = this.player;\n        this.game.removeAllPlayerAssets(player);\n        if (!player.isCombatant() || player.defeated || player.isObserver) {\n            return;\n        }\n        player.resigned = true;\n        player.defeated = true;\n        player.isObserver = true;\n        this.game.events.dispatch(new PlayerResignedEvent(player));\n        this.game.events.dispatch(new PlayerDefeatedEvent(player));\n        this.game.mapShroudTrait.getPlayerShroud(player)?.revealAll();\n        const wasRadarDisabled = player.radarTrait.isDisabled();\n        player.radarTrait.setDisabled(false);\n        if (wasRadarDisabled) {\n            this.game.events.dispatch(new RadarOnOffEvent(player, true));\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/action/OrderActionContext.ts",
    "content": "import { UnitSelectionLite } from '../gameobject/selection/UnitSelectionLite';\nexport class OrderActionContext {\n    private unitSelectionByPlayer: Map<number, UnitSelectionLite>;\n    constructor() {\n        this.unitSelectionByPlayer = new Map();\n    }\n    getOrCreateSelection(playerId: number): UnitSelectionLite {\n        let selection = this.unitSelectionByPlayer.get(playerId);\n        if (!selection) {\n            selection = new UnitSelectionLite(playerId);\n            this.unitSelectionByPlayer.set(playerId, selection);\n        }\n        return selection;\n    }\n}\n"
  },
  {
    "path": "src/game/action/OrderUnitsAction.ts",
    "content": "import { DataStream } from \"@/data/DataStream\";\nimport { Action } from \"@/game/action/Action\";\nimport { OrderType } from \"@/game/order/OrderType\";\nimport { orderPriorities } from \"@/game/order/orderPriorities\";\nimport { MoveOrder } from \"@/game/order/MoveOrder\";\nimport { MovePositionHelper } from \"@/game/gameobject/unit/MovePositionHelper\";\nimport { DeployOrder } from \"@/game/order/DeployOrder\";\nimport { CheerEvent } from \"@/game/event/CheerEvent\";\nimport { DeployNotAllowedEvent } from \"@/game/event/DeployNotAllowedEvent\";\nimport { isNotNullOrUndefined } from \"@/util/typeGuard\";\nimport { ScatterPositionHelper } from \"@/game/gameobject/unit/ScatterPositionHelper\";\nimport { ActionType } from \"@/game/action/ActionType\";\nexport const ORDER_UNIT_LIMIT = 128;\nexport class OrderUnitsAction extends Action {\n    private game: any;\n    private map: any;\n    private orderActionContext: any;\n    private orderFactory: any;\n    public queue: boolean = false;\n    private isInvalid: boolean = false;\n    public orderType!: number;\n    public target: any;\n    constructor(game: any, map: any, orderActionContext: any, orderFactory: any) {\n        super(ActionType.OrderUnits);\n        this.game = game;\n        this.map = map;\n        this.orderActionContext = orderActionContext;\n        this.orderFactory = orderFactory;\n        this.queue = false;\n        this.isInvalid = false;\n    }\n    unserialize(data: Uint8Array): void {\n        let stream = new DataStream(data);\n        this.orderType = stream.readUint8();\n        const version = stream.readUint8();\n        if (version !== 0) {\n            const rx = stream.readUint16();\n            const ry = stream.readUint16();\n            this.queue = version > 2 && Boolean(stream.readUint8());\n            let targetObject: any;\n            if (version > 3) {\n                const objectId = stream.readUint32();\n                if (!this.game.getWorld().hasObjectId(objectId)) {\n                    this.isInvalid = true;\n                    return;\n                }\n                targetObject = this.game.getObjectById(objectId);\n            }\n            else {\n                targetObject = undefined;\n            }\n            const tile = this.map.tiles.getByMapCoords(rx, ry);\n            if (tile) {\n                this.target = this.game.createTarget(targetObject, tile);\n            }\n            else {\n                this.isInvalid = true;\n            }\n        }\n    }\n    serialize(): Uint8Array {\n        let stream = new DataStream(11);\n        stream.dynamicSize = false;\n        stream.writeUint8(this.orderType);\n        let extraDataSize = 0;\n        stream.writeUint8(extraDataSize);\n        if (this.target) {\n            stream.writeUint16(this.target.tile.rx);\n            stream.writeUint16(this.target.tile.ry);\n            extraDataSize += 2;\n            const objectId = (this.target.obj || this.target.getBridge())?.id;\n            if (this.queue || objectId !== undefined) {\n                stream.writeUint8(Number(this.queue));\n                extraDataSize += 1;\n            }\n            if (objectId !== undefined) {\n                stream.writeUint32(objectId);\n                extraDataSize += 1;\n            }\n        }\n        const currentPosition = stream.position;\n        if (extraDataSize > 0) {\n            stream.seek(1);\n            stream.writeUint8(extraDataSize);\n        }\n        return new Uint8Array(stream.buffer, stream.byteOffset, currentPosition);\n    }\n    print(): string {\n        if (this.isInvalid) {\n            return \"\";\n        }\n        let result = OrderType[this.orderType] + \" order \";\n        if (this.target) {\n            const objName = (this.target.obj || this.target.getBridge())?.name || \"<none>\";\n            result += `[obj: ${objName}, tile: ${this.target.tile.rx},${this.target.tile.ry}]`;\n            if (this.queue) {\n                result += \"(queue)\";\n            }\n        }\n        return result;\n    }\n    process(): void {\n        if (this.isInvalid) {\n            return;\n        }\n        const player = this.player;\n        const shroud = this.game.mapShroudTrait.getPlayerShroud(player);\n        if (!shroud) {\n            return;\n        }\n        const targetObject = this.target?.obj;\n        if (targetObject) {\n            const tiles = this.game.map.tileOccupation.calculateTilesForGameObject(targetObject.tile, targetObject);\n            if (!tiles.find((tile: any) => !shroud.isShrouded(tile, targetObject.tileElevation))) {\n                return;\n            }\n        }\n        const validatedOrders = this.validateOrders(player).slice(0, ORDER_UNIT_LIMIT);\n        const processedOrders: any[] = [];\n        const moveOrders: any[] = [];\n        const scatterOrders: any[] = [];\n        const deployOrders: any[] = [];\n        const cheerOrders: any[] = [];\n        validatedOrders.forEach((order: any) => {\n            if (order instanceof MoveOrder) {\n                moveOrders.push(order);\n            }\n            else if (order.orderType === OrderType.Scatter) {\n                scatterOrders.push(order);\n            }\n            else if (order.orderType === OrderType.DeploySelected) {\n                deployOrders.push(order);\n            }\n            else if (order.orderType === OrderType.Cheer) {\n                cheerOrders.push(order);\n            }\n            else {\n                processedOrders.push(order);\n            }\n        });\n        if (moveOrders.length && this.target) {\n            const isEnemyBuildingBlock = moveOrders[0].isEnemyBuildingBlock();\n            const isFollowMove = moveOrders[0].isFollowMove();\n            if (isEnemyBuildingBlock || isFollowMove) {\n                moveOrders.forEach((order: any) => processedOrders.push(order));\n            }\n            else {\n                const bridge = this.target.getBridge();\n                const forceMove = moveOrders[0].forceMove;\n                const units = moveOrders.map((order: any) => order.sourceObject);\n                const positions = new MovePositionHelper(this.map).findPositions(units, this.target.tile, bridge, forceMove);\n                moveOrders.forEach((order: any) => {\n                    const position = positions.get(order.sourceObject);\n                    const bridgeOnTile = !bridge || bridge.isHighBridge()\n                        ? this.map.tileOccupation.getBridgeOnTile(position)\n                        : bridge;\n                    const target = this.game.createTarget(bridgeOnTile, position);\n                    order.target = target;\n                    processedOrders.push(order);\n                });\n            }\n        }\n        if (scatterOrders.length) {\n            const scatterUnits = scatterOrders\n                .map((order: any) => order.sourceObject)\n                .filter((unit: any) => unit.isInfantry() || unit.isVehicle());\n            const scatterPositions = new ScatterPositionHelper(this.game).findPositions(scatterUnits);\n            scatterOrders.forEach((order: any) => {\n                const position = scatterPositions.get(order.sourceObject);\n                if (position) {\n                    const target = this.game.createTarget(position.onBridge, position.tile);\n                    order.target = target;\n                    processedOrders.push(order);\n                }\n            });\n        }\n        if (deployOrders.length) {\n            const deployableOrders: any[] = [];\n            deployOrders.forEach((order: any) => {\n                const unit = order.sourceObject;\n                if ((unit.isInfantry() || unit.isVehicle()) && unit.deployerTrait) {\n                    deployableOrders.push(order);\n                }\n                else {\n                    processedOrders.push(order);\n                }\n            });\n            const undeployedOrders = deployableOrders.filter((order: any) => !order.sourceObject.deployerTrait.isDeployed());\n            if (undeployedOrders.length) {\n                undeployedOrders.forEach((order: any) => processedOrders.push(order));\n            }\n            else {\n                deployableOrders.forEach((order: any) => processedOrders.push(order));\n            }\n        }\n        if (cheerOrders.length) {\n            if (!player.cheerCooldownTicks) {\n                player.cheerCooldownTicks = this.game.rules.general.maximumCheerRate;\n                processedOrders.push(...cheerOrders);\n                this.game.events.dispatch(new CheerEvent(player));\n            }\n        }\n        processedOrders.forEach((order: any) => order.sourceObject.unitOrderTrait.addOrder(order, this.queue));\n        this.updateWaypointPaths(processedOrders);\n    }\n    private validateOrders(player: any): any[] {\n        const selection = this.orderActionContext.getOrCreateSelection(player);\n        const selectedUnits = selection.getSelectedUnits();\n        const baseOrder = this.orderFactory.create(this.orderType, selection);\n        baseOrder.target = this.target;\n        const validOrders: any[] = [];\n        for (const unit of selectedUnits) {\n            if (unit.owner !== player ||\n                unit.rules.spawned ||\n                unit.isDestroyed ||\n                unit.isCrashing ||\n                unit.isDisposed ||\n                unit.warpedOutTrait.isActive()) {\n                continue;\n            }\n            baseOrder.sourceObject = unit;\n            if (baseOrder instanceof DeployOrder && baseOrder.isValid() && !baseOrder.isAllowed()) {\n                this.game.events.dispatch(new DeployNotAllowedEvent(unit));\n            }\n            if (baseOrder.singleSelectionRequired && selectedUnits.length > 1) {\n                continue;\n            }\n            if (baseOrder.isValid() && baseOrder.isAllowed()) {\n                const order = this.orderFactory.create(this.orderType, selection);\n                order.set(unit, this.target);\n                validOrders.push(order);\n            }\n            else {\n                let orderFound = false;\n                for (const priorityOrderType of orderPriorities) {\n                    const order = this.orderFactory.create(priorityOrderType, selection);\n                    order.set(unit, this.target);\n                    if (!(order.singleSelectionRequired && selectedUnits.length > 1) &&\n                        order.targetOptional === !this.target &&\n                        order.isValid() &&\n                        order.isAllowed()) {\n                        validOrders.push(order);\n                        orderFound = true;\n                        break;\n                    }\n                }\n                if (!orderFound && this.target && this.orderType !== OrderType.Deploy) {\n                    const moveOrder = this.orderFactory.create(OrderType.Move, selection);\n                    moveOrder.set(unit, this.target);\n                    if (moveOrder.isValid() && moveOrder.isAllowed()) {\n                        validOrders.push(moveOrder);\n                    }\n                }\n            }\n        }\n        return validOrders;\n    }\n    private updateWaypointPaths(orders: any[]): void {\n        if (!this.queue || !this.target) {\n            return;\n        }\n        const units = orders.map((order: any) => order.sourceObject);\n        const waypointPaths = [\n            ...new Set(units\n                .map((unit: any) => unit.unitOrderTrait.waypointPath)\n                .filter(isNotNullOrUndefined))\n        ];\n        if (waypointPaths.length <= 1) {\n            const waypoint = {\n                orderType: this.orderType,\n                target: this.target,\n                terminal: orders.some((order: any) => order.terminal),\n                next: undefined\n            };\n            if (waypointPaths.length === 0) {\n                const waypointPath = { units: units, waypoints: [waypoint] };\n                units.forEach((unit: any) => {\n                    unit.unitOrderTrait.waypointPath = waypointPath;\n                });\n            }\n            else {\n                const existingPath = waypointPaths[0];\n                existingPath.waypoints[existingPath.waypoints.length - 1].next = waypoint;\n                existingPath.waypoints.push(waypoint);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/action/PingLocationAction.ts",
    "content": "import { Action } from './Action';\nimport { DataStream } from '@/data/DataStream';\nimport { ActionType } from './ActionType';\nimport { PingLocationEvent } from '../event/PingLocationEvent';\nimport { RadarEvent } from '../event/RadarEvent';\nimport { RadarEventType } from '../rules/general/RadarRules';\nimport { Game } from '../Game';\nexport class PingLocationAction extends Action {\n    private game: Game;\n    private tile: {\n        x: number;\n        y: number;\n    };\n    constructor(game: Game) {\n        super(ActionType.PingLocation);\n        this.game = game;\n    }\n    unserialize(data: Uint8Array): void {\n        const stream = new DataStream(data);\n        this.tile = {\n            x: stream.readUint16(),\n            y: stream.readUint16()\n        };\n    }\n    serialize(): Uint8Array {\n        const stream = new DataStream(4);\n        stream.writeUint16(this.tile.x);\n        stream.writeUint16(this.tile.y);\n        return stream.toUint8Array();\n    }\n    print(): string {\n        return `Ping location at tile (${this.tile.x}, ${this.tile.y})`;\n    }\n    process(): void {\n        const player = this.player;\n        const tile = this.game.map.tiles.getByMapCoords(this.tile.x, this.tile.y);\n        if (tile) {\n            this.game.events.dispatch(new PingLocationEvent(tile, player));\n            const allies = [player, ...this.game.alliances.getAllies(player)];\n            for (const ally of allies) {\n                this.game.events.dispatch(new RadarEvent(ally, RadarEventType.GenericNonCombat, tile));\n            }\n        }\n        else {\n            console.warn(`Tile ${this.tile.x},${this.tile.y} doesn't exist`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/action/PlaceBuildingAction.ts",
    "content": "import { Action } from './Action';\nimport { DataStream } from '@/data/DataStream';\nimport { BuildingPlaceEvent } from '../event/BuildingPlaceEvent';\nimport { ProductionQueue, QueueStatus } from '../player/production/ProductionQueue';\nimport { TechnoRules, FactoryType } from '../rules/TechnoRules';\nimport { FactoryTrait, FactoryStatus } from '../gameobject/trait/FactoryTrait';\nimport { ActionType } from './ActionType';\nimport { BuildingFailedPlaceEvent } from '../event/BuildingFailedPlaceEvent';\nimport { NotifyPlaceBuilding } from '../trait/interface/NotifyPlaceBuilding';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { Game } from '../Game';\nimport { Tile } from '../map/Tile';\nimport { Player } from '@/game/Player';\nimport { GameObject } from '../gameobject/GameObject';\nexport class PlaceBuildingAction extends Action {\n    private game: Game;\n    private buildingRules: TechnoRules;\n    private tile: {\n        x: number;\n        y: number;\n    };\n    constructor(game: Game) {\n        super(ActionType.PlaceBuilding);\n        this.game = game;\n    }\n    unserialize(data: Uint8Array): void {\n        const stream = new DataStream(data);\n        this.buildingRules = this.game.rules.getTechnoByInternalId(stream.readUint32(), ObjectType.Building);\n        this.tile = {\n            x: stream.readUint16(),\n            y: stream.readUint16()\n        };\n    }\n    serialize(): Uint8Array {\n        const stream = new DataStream(8);\n        stream.writeUint32(this.buildingRules.index);\n        stream.writeUint16(this.tile.x);\n        stream.writeUint16(this.tile.y);\n        return stream.toUint8Array();\n    }\n    print(): string {\n        return `Place building ${this.buildingRules.name} at tile (${this.tile.x}, ${this.tile.y})`;\n    }\n    process(): void {\n        const tile = this.game.map.tiles.getByMapCoords(this.tile.x, this.tile.y);\n        if (tile) {\n            const player = this.player;\n            const building = this.tryPlaceBuilding(player, tile);\n            if (building) {\n                this.game.traits\n                    .filter(NotifyPlaceBuilding)\n                    .forEach(trait => {\n                    trait[NotifyPlaceBuilding.onPlace](building, this.game);\n                });\n                this.game.events.dispatch(new BuildingPlaceEvent(building));\n            }\n            else {\n                this.game.events.dispatch(new BuildingFailedPlaceEvent(this.buildingRules.name, player, tile));\n            }\n        }\n        else {\n            console.warn(`Tile ${this.tile.x},${this.tile.y} doesn't exist`);\n        }\n    }\n    private tryPlaceBuilding(player: Player, tile: Tile): GameObject | undefined {\n        const buildingRules = this.buildingRules;\n        if (player.production) {\n            const queue = player.production.getQueueForObject(buildingRules);\n            if (queue.status === QueueStatus.Ready && queue.getFirst().rules === buildingRules) {\n                const worker = this.game.getConstructionWorker(player);\n                if (player.production.isAvailableForProduction(buildingRules as any) &&\n                    worker.canPlaceAt(buildingRules.name, tile, { normalizedTile: true })) {\n                    const placed = worker.placeAt(buildingRules.name, tile, true);\n                    player.addUnitsBuilt(buildingRules as any, 1);\n                    queue.shift(buildingRules as any, 1);\n                    const factory = player.production.getPrimaryFactory(FactoryType.BuildingType);\n                    if (factory) {\n                        factory.factoryTrait.status = FactoryStatus.Delivering;\n                    }\n                    return placed[0] as any;\n                }\n            }\n        }\n        return undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/action/ResignGameAction.ts",
    "content": "import { Action } from './Action';\nimport { PlayerResignedEvent } from '../event/PlayerResignedEvent';\nimport { ActionType } from './ActionType';\nimport { Game } from '../Game';\nexport class ResignGameAction extends Action {\n    private game: Game;\n    private localPlayerName: string;\n    constructor(game: Game, localPlayerName: string) {\n        super(ActionType.ResignGame);\n        this.game = game;\n        this.localPlayerName = localPlayerName;\n    }\n    unserialize(_data: Uint8Array): void { }\n    serialize(): Uint8Array {\n        return new Uint8Array();\n    }\n    process(): void {\n        if (this.localPlayerName !== this.player.name) {\n            const player = this.player;\n            const redistributedAssets = this.game.redistributeAllPlayerAssets(player);\n            this.game.removeAllPlayerAssets(player);\n            if (player.isCombatant()) {\n                player.resigned = true;\n                this.game.events.dispatch(new PlayerResignedEvent(player, redistributedAssets));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/action/SelectUnitsAction.ts",
    "content": "import { Action } from './Action';\nimport { ActionType } from './ActionType';\nimport { DataStream } from '@/data/DataStream';\nimport { OrderUnitsAction, ORDER_UNIT_LIMIT } from './OrderUnitsAction';\nimport { OrderActionContext } from './OrderActionContext';\nimport { GameObject } from '../gameobject/GameObject';\nexport class SelectUnitsAction extends Action {\n    private _unitIds: number[] = [];\n    private orderActionContext: OrderActionContext;\n    constructor(game: any, orderActionContext: OrderActionContext) {\n        super(ActionType.SelectUnits);\n        this.orderActionContext = orderActionContext;\n    }\n    get unitIds(): number[] {\n        return this._unitIds;\n    }\n    set unitIds(value: number[]) {\n        this._unitIds = value.slice(0, ORDER_UNIT_LIMIT);\n    }\n    unserialize(data: Uint8Array): void {\n        const stream = new DataStream(data);\n        this.unitIds = new Array(data.byteLength / 4);\n        for (let i = 0; i < data.byteLength / 4; i++) {\n            this.unitIds[i] = stream.readUint32();\n        }\n    }\n    serialize(): Uint8Array {\n        const stream = new DataStream(4 * this.unitIds.length);\n        stream.dynamicSize = false;\n        for (const id of this.unitIds) {\n            stream.writeUint32(id);\n        }\n        return stream.toUint8Array();\n    }\n    print(): string {\n        return `Select unit(s) [${this.unitIds.join(',')}]`;\n    }\n    process(): void {\n        const player = this.player;\n        const units: GameObject[] = [];\n        for (const id of this.unitIds) {\n            const unit = player.getOwnedObjectById(id);\n            if (unit) {\n                units.push(unit);\n            }\n        }\n        this.orderActionContext.getOrCreateSelection(player).update(units);\n    }\n}\n"
  },
  {
    "path": "src/game/action/SellObjectAction.ts",
    "content": "import { Action } from './Action';\nimport { DataStream } from '@/data/DataStream';\nimport { Building, BuildStatus } from '../gameobject/Building';\nimport { ActionType } from './ActionType';\nimport { DockableTrait } from '../gameobject/trait/DockableTrait';\nimport { Game } from '../Game';\nexport class SellObjectAction extends Action {\n    private game: Game;\n    private objectId: number;\n    constructor(game: Game) {\n        super(ActionType.SellObject);\n        this.game = game;\n    }\n    unserialize(data: Uint8Array): void {\n        this.objectId = new DataStream(data).readUint32();\n    }\n    serialize(): Uint8Array {\n        const stream = new DataStream(4);\n        stream.dynamicSize = false;\n        stream.writeUint32(this.objectId);\n        return stream.toUint8Array();\n    }\n    print(): string {\n        return `Sell object ${this.objectId}`;\n    }\n    process(): void {\n        const player = this.player;\n        if (this.game.getWorld().hasObjectId(this.objectId)) {\n            const object = this.game.getObjectById(this.objectId);\n            if (object.isTechno() &&\n                player === object.owner &&\n                object.isSpawned) {\n                const canSell = object.isBuilding()\n                    ? object.buildStatus === BuildStatus.Ready && !object.warpedOutTrait.isActive()\n                    : object.traits.find(DockableTrait)?.dock?.rules.unitSell;\n                if (canSell) {\n                    this.game.sellTrait.sell(object);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/action/ToggleAllianceAction.ts",
    "content": "import { Action } from './Action';\nimport { Alliances, AllianceStatus } from '../Alliances';\nimport { AllianceChangeEvent, AllianceEventType } from '../event/AllianceChangeEvent';\nimport { NotifyAllianceChange } from '../trait/interface/NotifyAllianceChange';\nimport { ActionType } from './ActionType';\nimport { Game } from '../Game';\nimport { Player } from '@/game/Player';\nexport class ToggleAllianceAction extends Action {\n    private game: Game;\n    private toPlayer: Player;\n    private toggle: boolean;\n    constructor(game: Game) {\n        super(ActionType.ToggleAlliance);\n        this.game = game;\n    }\n    unserialize(data: Uint8Array): void {\n        this.toPlayer = this.game.getPlayer(data[0]);\n        this.toggle = Boolean(data[1]);\n    }\n    serialize(): Uint8Array {\n        return new Uint8Array([\n            this.game.getPlayerNumber(this.toPlayer),\n            this.toggle ? 1 : 0\n        ]);\n    }\n    print(): string {\n        return `Toggle alliance ${this.toggle ? \"on\" : \"off\"} with ${this.toPlayer.name}`;\n    }\n    process(): void {\n        const mpSettings = this.game.rules.mpDialogSettings;\n        if (!mpSettings.alliesAllowed || !mpSettings.allyChangeAllowed) {\n            return;\n        }\n        const player = this.player;\n        const targetPlayer = this.toPlayer;\n        const toggle = this.toggle;\n        const alliances = this.game.alliances;\n        if (player.defeated || !alliances.canRequestAlliance(targetPlayer)) {\n            return;\n        }\n        const alliance = alliances.findByPlayers(player, targetPlayer);\n        if (alliance) {\n            if (alliance.status === AllianceStatus.Formed) {\n                if (!toggle) {\n                    alliances.breakAlliance(player, targetPlayer);\n                    this.game.onAllianceChange(alliance, player, false);\n                }\n            }\n            else if (alliance.status === AllianceStatus.Requested) {\n                if (alliance.players.first === targetPlayer) {\n                    if (toggle && alliances.canFormAlliance(player, targetPlayer)) {\n                        alliances.acceptRequest(targetPlayer, player);\n                        this.game.onAllianceChange(alliance, player, true);\n                        const remainingCombatants = this.game.getCombatants()\n                            .filter(p => p !== player && !alliances.areAllied(player, p));\n                        if (remainingCombatants.length === 1) {\n                            const remainingAlliance = alliances.findByPlayers(remainingCombatants[0], player);\n                            if (remainingAlliance) {\n                                alliances.cancelRequest(remainingAlliance.players.first, remainingAlliance.players.second);\n                            }\n                        }\n                        const targetRemainingCombatants = this.game.getCombatants()\n                            .filter(p => p !== targetPlayer && !alliances.areAllied(targetPlayer, p));\n                        if (targetRemainingCombatants.length === 1) {\n                            const targetRemainingAlliance = alliances.findByPlayers(targetRemainingCombatants[0], targetPlayer);\n                            if (targetRemainingAlliance) {\n                                alliances.cancelRequest(targetRemainingAlliance.players.first, targetRemainingAlliance.players.second);\n                            }\n                        }\n                    }\n                }\n                else if (!toggle) {\n                    alliances.cancelRequest(player, targetPlayer);\n                    this.game.events.dispatch(new AllianceChangeEvent(alliance, AllianceEventType.Broken, player));\n                    this.game.traits\n                        .filter(NotifyAllianceChange)\n                        .forEach(trait => {\n                        trait[NotifyAllianceChange.onChange](alliance, false, this.game);\n                    });\n                }\n            }\n        }\n        else if (toggle && alliances.canFormAlliance(player, targetPlayer)) {\n            const newAlliance = alliances.request(player, targetPlayer);\n            if (newAlliance) {\n                this.game.events.dispatch(new AllianceChangeEvent(newAlliance, AllianceEventType.Requested, player));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/action/ToggleRepairAction.ts",
    "content": "import { Action } from './Action';\nimport { DataStream } from '@/data/DataStream';\nimport { AutoRepairTrait } from '../gameobject/trait/AutoRepairTrait';\nimport { BuildingRepairStartEvent } from '../event/BuildingRepairStartEvent';\nimport { ActionType } from './ActionType';\nimport { Game } from '../Game';\nexport class ToggleRepairAction extends Action {\n    private game: Game;\n    private buildingId: number;\n    constructor(game: Game) {\n        super(ActionType.ToggleRepair);\n        this.game = game;\n    }\n    unserialize(data: Uint8Array): void {\n        this.buildingId = new DataStream(data).readUint32();\n    }\n    serialize(): Uint8Array {\n        const stream = new DataStream(4);\n        stream.dynamicSize = false;\n        stream.writeUint32(this.buildingId);\n        return stream.toUint8Array();\n    }\n    print(): string {\n        return `Toggle repair ${this.buildingId}`;\n    }\n    process(): void {\n        const player = this.player;\n        if (this.game.getWorld().hasObjectId(this.buildingId)) {\n            const building = this.game.getObjectById(this.buildingId);\n            if (building.isBuilding() &&\n                player === building.owner &&\n                !building.isDestroyed &&\n                building.rules.repairable &&\n                building.rules.clickRepairable &&\n                building.healthTrait.health !== 100) {\n                const repairTrait = building.traits.get(AutoRepairTrait);\n                repairTrait.setDisabled(!repairTrait.isDisabled());\n                if (!repairTrait.isDisabled()) {\n                    this.game.events.dispatch(new BuildingRepairStartEvent(building));\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/action/UpdateQueueAction.ts",
    "content": "import { Action } from './Action';\nimport { DataStream } from '@/data/DataStream';\nimport { QueueType, QueueStatus } from '../player/production/ProductionQueue';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { ActionType } from './ActionType';\nimport { Game } from '../Game';\nexport enum UpdateType {\n    Add = 0,\n    Cancel = 1,\n    Pause = 2,\n    Resume = 3,\n    AddNext = 4\n}\nexport class UpdateQueueAction extends Action {\n    private game: Game;\n    private queueType: number;\n    private updateType: UpdateType;\n    private item?: any;\n    private quantity?: number;\n    constructor(game: Game) {\n        super(ActionType.UpdateQueue);\n        this.game = game;\n    }\n    unserialize(data: Uint8Array): void {\n        const stream = new DataStream(data);\n        this.queueType = stream.readUint8();\n        this.updateType = stream.readUint8();\n        if (this.updateType === UpdateType.Add ||\n            this.updateType === UpdateType.Cancel ||\n            this.updateType === UpdateType.AddNext) {\n            const index = stream.readUint32();\n            const type = stream.readUint8();\n            this.item = this.game.rules.getTechnoByInternalId(index, type);\n            this.quantity = stream.readUint16();\n        }\n    }\n    serialize(): Uint8Array {\n        const stream = new DataStream(9);\n        stream.dynamicSize = false;\n        stream.writeUint8(this.queueType);\n        stream.writeUint8(this.updateType);\n        if (this.updateType === UpdateType.Add ||\n            this.updateType === UpdateType.Cancel ||\n            this.updateType === UpdateType.AddNext) {\n            if (this.quantity === undefined) {\n                throw new Error(\"Missing quantity\");\n            }\n            if (this.quantity > 65535) {\n                throw new Error(\"Maximum quantity exceeded\");\n            }\n            stream.writeUint32(this.item.index);\n            stream.writeUint8(this.item.type);\n            stream.writeUint16(this.quantity);\n        }\n        return new Uint8Array(stream.buffer, stream.byteOffset, stream.position);\n    }\n    print(): string {\n        switch (this.updateType) {\n            case UpdateType.Resume:\n                return `Resume queue ${QueueType[this.queueType]}`;\n            case UpdateType.Add:\n                return `Add to queue ${this.item.name} x ${this.quantity}`;\n            case UpdateType.AddNext:\n                return `Add next in queue ${this.item.name} x ${this.quantity}`;\n            case UpdateType.Pause:\n                return `Put queue ${QueueType[this.queueType]} on hold.`;\n            case UpdateType.Cancel:\n                return `Cancel ${this.item.name} x ${this.quantity}`;\n            default:\n                return `Unhandled queue update type ${this.updateType}`;\n        }\n    }\n    process(): void {\n        const player = this.player;\n        const item = this.item;\n        const queue = player.production.getQueue(this.queueType);\n        if (this.updateType === UpdateType.Resume) {\n            if (queue.status === QueueStatus.OnHold) {\n                queue.status = QueueStatus.Active;\n            }\n        }\n        else if (this.updateType === UpdateType.Add || this.updateType === UpdateType.AddNext) {\n            const existingItems = queue.find(item);\n            if (queue.status === QueueStatus.Active ||\n                queue.status === QueueStatus.Idle ||\n                (queue.status === QueueStatus.OnHold && existingItems[0] !== queue.getFirst()) ||\n                (queue.status === QueueStatus.Ready && item.type !== ObjectType.Building)) {\n                let buildLimit: number;\n                const queuedQuantity = existingItems.reduce((sum, item) => sum + item.quantity, 0);\n                if (Number.isFinite(item.buildLimit)) {\n                    const builtCount = item.buildLimit >= 0\n                        ? player.getOwnedObjectsByType(item.type, true)\n                            .filter(obj => obj.name === item.name).length\n                        : player.getLimitedUnitsBuilt(item.name);\n                    buildLimit = Math.max(0, Math.abs(item.buildLimit) - (builtCount + queuedQuantity));\n                }\n                else {\n                    buildLimit = Number.POSITIVE_INFINITY;\n                }\n                if (buildLimit && player.production.isAvailableForProduction(item)) {\n                    const maxQuantity = Math.min(queue.maxSize - queue.currentSize, queue.maxItemQuantity - queuedQuantity, buildLimit);\n                    const quantity = Math.min(this.quantity!, maxQuantity);\n                    if (quantity > 0) {\n                        if (this.updateType === UpdateType.AddNext) {\n                            queue.insertAfterFirst(item, quantity, item.cost);\n                        }\n                        else {\n                            queue.push(item, quantity, item.cost);\n                        }\n                    }\n                }\n            }\n        }\n        else if (this.updateType === UpdateType.Cancel) {\n            if ([QueueStatus.Ready, QueueStatus.OnHold, QueueStatus.Active].includes(queue.status)) {\n                const existingItems = queue.find(item);\n                if (existingItems.length) {\n                    const totalQuantity = existingItems.reduce((sum, item) => sum + item.quantity, 0);\n                    const cancelQuantity = Math.min(totalQuantity, this.quantity!);\n                    if (cancelQuantity > 0) {\n                        queue.pop(item, cancelQuantity);\n                        if (cancelQuantity === totalQuantity) {\n                            player.credits += existingItems[0].creditsSpent;\n                        }\n                    }\n                }\n            }\n        }\n        else if (this.updateType === UpdateType.Pause) {\n            if (queue.status === QueueStatus.Active) {\n                queue.status = QueueStatus.OnHold;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/ActivateSuperWeaponActionFactory.ts",
    "content": "import { ActivateSuperWeaponAction } from '../ActivateSuperWeaponAction';\nimport { Game } from '@/game/Game';\nexport class ActivateSuperWeaponActionFactory {\n    private game: Game;\n    constructor(game: Game) {\n        this.game = game;\n    }\n    create(): ActivateSuperWeaponAction {\n        return new ActivateSuperWeaponAction(this.game);\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/DebugActionFactory.ts",
    "content": "import { DebugAction } from '../DebugAction';\nimport { Game } from '@/game/Game';\nexport class DebugActionFactory {\n    private game: Game;\n    constructor(game: Game) {\n        this.game = game;\n    }\n    create(): DebugAction {\n        return new DebugAction(this.game);\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/DropPlayerActionFactory.ts",
    "content": "import { DropPlayerAction } from '../DropPlayerAction';\nimport { Game } from '@/game/Game';\nexport class DropPlayerActionFactory {\n    private game: Game;\n    private localPlayerName: string;\n    constructor(game: Game, localPlayerName: string) {\n        this.game = game;\n        this.localPlayerName = localPlayerName;\n    }\n    create(): DropPlayerAction {\n        return new DropPlayerAction(this.game, this.localPlayerName);\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/NoActionFactory.ts",
    "content": "import { NoAction } from '../NoAction';\nexport class NoActionFactory {\n    create(): NoAction {\n        return new NoAction();\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/ObserveGameActionFactory.ts",
    "content": "import { ObserveGameAction } from '../ObserveGameAction';\nimport { Game } from '@/game/Game';\nexport class ObserveGameActionFactory {\n    private game: Game;\n    constructor(game: Game) {\n        this.game = game;\n    }\n    create(): ObserveGameAction {\n        return new ObserveGameAction(this.game);\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/OrderUnitsActionFactory.ts",
    "content": "import { OrderUnitsAction } from '../OrderUnitsAction';\nimport { OrderFactory } from '@/game/order/OrderFactory';\nimport { Game } from '@/game/Game';\nimport { OrderActionContext } from '@/game/action/OrderActionContext';\nexport class OrderUnitsActionFactory {\n    private game: Game;\n    private map: Map<any, any>;\n    private orderActionContext: OrderActionContext;\n    constructor(game: Game, map: Map<any, any>, orderActionContext: OrderActionContext) {\n        this.game = game;\n        this.map = map;\n        this.orderActionContext = orderActionContext;\n    }\n    create(): OrderUnitsAction {\n        return new OrderUnitsAction(this.game, this.map, this.orderActionContext, new OrderFactory(this.game, this.map));\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/PingLocationActionFactory.ts",
    "content": "import { PingLocationAction } from '../PingLocationAction';\nimport { Game } from '@/game/Game';\nexport class PingLocationActionFactory {\n    private game: Game;\n    constructor(game: Game) {\n        this.game = game;\n    }\n    create(): PingLocationAction {\n        return new PingLocationAction(this.game);\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/PlaceBuildingActionFactory.ts",
    "content": "import { PlaceBuildingAction } from '../PlaceBuildingAction';\nimport { Game } from '@/game/Game';\nexport class PlaceBuildingActionFactory {\n    private game: Game;\n    constructor(game: Game) {\n        this.game = game;\n    }\n    create(): PlaceBuildingAction {\n        return new PlaceBuildingAction(this.game);\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/ResignGameActionFactory.ts",
    "content": "import { ResignGameAction } from '../ResignGameAction';\nimport { Game } from '@/game/Game';\nexport class ResignGameActionFactory {\n    private game: Game;\n    private localPlayerName: string;\n    constructor(game: Game, localPlayerName: string) {\n        this.game = game;\n        this.localPlayerName = localPlayerName;\n    }\n    create(): ResignGameAction {\n        return new ResignGameAction(this.game, this.localPlayerName);\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/SelectUnitsActionFactory.ts",
    "content": "import { SelectUnitsAction } from '../SelectUnitsAction';\nimport { Game } from '@/game/Game';\nimport { OrderActionContext } from '@/game/action/OrderActionContext';\nexport class SelectUnitsActionFactory {\n    private game: Game;\n    private orderActionContext: OrderActionContext;\n    constructor(game: Game, orderActionContext: OrderActionContext) {\n        this.game = game;\n        this.orderActionContext = orderActionContext;\n    }\n    create(): SelectUnitsAction {\n        return new SelectUnitsAction(this.game, this.orderActionContext);\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/SellObjectActionFactory.ts",
    "content": "import { SellObjectAction } from '../SellObjectAction';\nimport { Game } from '../../Game';\nexport class SellObjectActionFactory {\n    private game: Game;\n    constructor(game: Game) {\n        this.game = game;\n    }\n    create(): SellObjectAction {\n        return new SellObjectAction(this.game);\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/ToggleAllianceFactory.ts",
    "content": "import { ToggleAllianceAction } from '../ToggleAllianceAction';\nimport { Game } from '../../Game';\nexport class ToggleAllianceActionFactory {\n    private game: Game;\n    constructor(game: Game) {\n        this.game = game;\n    }\n    create(): ToggleAllianceAction {\n        return new ToggleAllianceAction(this.game);\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/ToggleRepairActionFactory.ts",
    "content": "import { ToggleRepairAction } from '../ToggleRepairAction';\nimport { Game } from '@/game/Game';\nexport class ToggleRepairActionFactory {\n    private game: Game;\n    constructor(game: Game) {\n        this.game = game;\n    }\n    create(): ToggleRepairAction {\n        return new ToggleRepairAction(this.game);\n    }\n}\n"
  },
  {
    "path": "src/game/action/factories/UpdateQueueActionFactory.ts",
    "content": "import { UpdateQueueAction } from '../UpdateQueueAction';\nimport { Game } from '../../Game';\nexport class UpdateQueueActionFactory {\n    private game: Game;\n    constructor(game: Game) {\n        this.game = game;\n    }\n    create(): UpdateQueueAction {\n        return new UpdateQueueAction(this.game);\n    }\n}\n"
  },
  {
    "path": "src/game/ai/Ai.ts",
    "content": "export class Ai {\n    private ini: any;\n    constructor(ini: any) {\n        this.ini = ini;\n    }\n    getIni(): any {\n        return this.ini;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/BotRegistry.ts",
    "content": "import { ThirdPartyBotMeta } from './ThirdPartyBotInterface';\nimport { BotSandbox } from './BotSandbox';\n\ninterface PersistedBot {\n    id: string;\n    displayName: string;\n    version: string;\n    author: string;\n    description?: string;\n    source: string;\n    sourceFile: string;\n}\n\n/**\n * Registry for third-party bots.\n * Manages registration, listing, and instantiation of third-party bots.\n */\nexport class BotRegistry {\n    private static instance: BotRegistry;\n    private bots: Map<string, ThirdPartyBotMeta> = new Map();\n    private botSources: Map<string, { source: string; sourceFile: string }> = new Map();\n\n    private constructor() {}\n\n    static getInstance(): BotRegistry {\n        if (!BotRegistry.instance) {\n            BotRegistry.instance = new BotRegistry();\n        }\n        return BotRegistry.instance;\n    }\n\n    /**\n     * Register a third-party bot.\n     */\n    register(meta: ThirdPartyBotMeta): void {\n        if (this.bots.has(meta.id)) {\n            console.warn(`[BotRegistry] Bot \"${meta.id}\" is already registered, overwriting.`);\n        }\n        this.bots.set(meta.id, meta);\n        console.info(`[BotRegistry] Registered bot: ${meta.displayName} v${meta.version} by ${meta.author}`);\n    }\n\n    /**\n     * Register a bot and store its source for persistence.\n     */\n    registerWithSource(meta: ThirdPartyBotMeta, source: string, sourceFile: string): void {\n        this.register(meta);\n        this.botSources.set(meta.id, { source, sourceFile });\n    }\n\n    /**\n     * Unregister a third-party bot by ID.\n     */\n    unregister(botId: string): boolean {\n        const meta = this.bots.get(botId);\n        if (meta?.builtIn) {\n            console.warn(`[BotRegistry] Cannot unregister built-in bot \"${botId}\".`);\n            return false;\n        }\n        this.botSources.delete(botId);\n        return this.bots.delete(botId);\n    }\n\n    /**\n     * Get a registered bot by ID.\n     */\n    get(botId: string): ThirdPartyBotMeta | undefined {\n        return this.bots.get(botId);\n    }\n\n    /**\n     * Get all registered bots.\n     */\n    getAll(): ThirdPartyBotMeta[] {\n        return [...this.bots.values()];\n    }\n\n    /**\n     * Get all user-uploaded (non-built-in) bots.\n     */\n    getUploadedBots(): ThirdPartyBotMeta[] {\n        return [...this.bots.values()].filter(b => !b.builtIn);\n    }\n\n    /**\n     * Check if a bot with the given ID is registered.\n     */\n    has(botId: string): boolean {\n        return this.bots.has(botId);\n    }\n\n    /**\n     * Get the count of registered bots.\n     */\n    get size(): number {\n        return this.bots.size;\n    }\n\n    /**\n     * Serialize uploaded bots for localStorage persistence.\n     */\n    serializeUploadedBots(): string {\n        const bots: PersistedBot[] = [];\n        for (const meta of this.getUploadedBots()) {\n            const sourceInfo = this.botSources.get(meta.id);\n            if (sourceInfo) {\n                bots.push({\n                    id: meta.id,\n                    displayName: meta.displayName,\n                    version: meta.version,\n                    author: meta.author,\n                    description: meta.description,\n                    source: sourceInfo.source,\n                    sourceFile: sourceInfo.sourceFile,\n                });\n            }\n        }\n        return JSON.stringify(bots);\n    }\n\n    /**\n     * Load persisted bots from serialized data.\n     */\n    loadPersistedBots(data: string): void {\n        try {\n            const bots: PersistedBot[] = JSON.parse(data);\n            for (const bot of bots) {\n                if (this.bots.has(bot.id)) {\n                    continue;\n                }\n                const meta = BotSandbox.loadBotFromSource(bot.source, bot.sourceFile);\n                if (meta) {\n                    this.botSources.set(meta.id, { source: bot.source, sourceFile: bot.sourceFile });\n                }\n            }\n        } catch (e) {\n            console.error('[BotRegistry] Failed to load persisted bots:', e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/BotSandbox.ts",
    "content": "import { ThirdPartyBotInterface, ThirdPartyBotMeta } from './ThirdPartyBotInterface';\nimport { BotRegistry } from './BotRegistry';\n\n/**\n * Allowed file extensions for bot scripts inside the zip.\n */\nconst ALLOWED_EXTENSIONS = ['.ts', '.json', '.txt', '.md', '.yml'];\n\n/**\n * Maximum file size for a single bot script file (512 KB).\n */\nconst MAX_FILE_SIZE = 512 * 1024;\n\n/**\n * Maximum total size for all files in a bot zip (10 MB).\n */\nconst MAX_TOTAL_SIZE = 10 * 1024 * 1024;\n\n/**\n * Maximum number of files in a bot zip.\n */\nconst MAX_FILE_COUNT = 200;\n\n/**\n * Forbidden patterns in bot code that could indicate malicious behavior.\n */\nconst FORBIDDEN_PATTERNS = [\n    /\\beval\\s*\\(/,\n    /\\bFunction\\s*\\(/,\n    /\\bnew\\s+Function\\b/,\n    /\\bimportScripts\\s*\\(/,\n    /\\bfetch\\s*\\(/,\n    /\\bXMLHttpRequest\\b/,\n    /\\bWebSocket\\b/,\n    /\\blocalStorage\\b/,\n    /\\bsessionStorage\\b/,\n    /\\bindexedDB\\b/,\n    /\\bdocument\\s*\\.\\s*(cookie|write|createElement)/,\n    /\\bwindow\\s*\\.\\s*(open|location|navigator)/,\n    /\\b__proto__\\b/,\n    /\\bconstructor\\s*\\[\\s*['\"]constructor['\"]\\s*\\]/,\n    /\\bProcess\\b/,\n    /\\brequire\\s*\\(/,\n    /\\bimport\\s*\\(/,\n    /\\bchild_process\\b/,\n    /\\bfs\\s*\\.\\s*(read|write|unlink|mkdir|rmdir)/,\n];\n\n/**\n * Validates and loads uploaded bot zip files with security restrictions.\n */\nexport class BotSandbox {\n    /**\n     * Strips TypeScript-specific syntax from source code so it can be run as\n     * plain JavaScript via new Function().  This is intentionally lightweight —\n     * it handles the patterns that appear in the example bot and most hand-written\n     * bots without pulling in a full TS compiler.\n     *\n     * Handled:\n     *   - interface / type alias declarations (via line-by-line brace-balanced scanner)\n     *   - `const enum` → plain `const` object literal so references still resolve\n     *   - Inline type annotations: `: Type`, `as Type`\n     *   - `readonly` and access modifiers\n     *   - `import type` / `export type` statements\n     *   - Non-null assertions (`!`)\n     */\n    static stripTypes(source: string): string {\n        // 1. Remove interface and object-form type blocks using brace-depth scanner\n        //    (handles any nesting depth, unlike regex)\n        source = BotSandbox.removeTypescriptBlocks(source);\n\n        // 2. Convert `const enum Foo { A = 0, B = 1 }` to `const Foo = { A: 0, B: 1 };`\n        //    so references like Foo.A still resolve at runtime.\n        source = source.replace(/\\bconst\\s+enum\\s+(\\w+)\\s*\\{([^}]*)\\}/g, (_m, name: string, body: string) => {\n            let autoVal = 0;\n            const members = body.split(',')\n                .map((m: string) => m.trim().replace(/\\/\\/[^\\n]*/g, '').trim())\n                .filter(Boolean)\n                .map((m: string) => {\n                    const eq = m.indexOf('=');\n                    if (eq !== -1) {\n                        const key = m.slice(0, eq).trim();\n                        const val = parseInt(m.slice(eq + 1).trim(), 10);\n                        autoVal = val + 1;\n                        return `${key}: ${val}`;\n                    }\n                    return `${m.trim()}: ${autoVal++}`;\n                });\n            return `const ${name} = { ${members.join(', ')} };`;\n        });\n\n        // 3. Remove `import type ...` / `export type ...` lines\n        source = source.replace(/^\\s*(?:import|export)\\s+type\\s+[^\\n]+\\n?/gm, '');\n        // Remove regular import statements (no module system in sandbox)\n        source = source.replace(/^\\s*import\\s+[^\\n]+from\\s+['\"][^'\"]+['\"]\\s*;?\\s*\\n?/gm, '');\n        // Remove `declare` statements (type-only, emit no code)\n        source = source.replace(/^\\s*declare\\s+[^\\n]+\\n?/gm, '');\n\n        // 4. `foo as any` / `foo as Bar` casts\n        source = source.replace(/\\bas\\s+\\w[\\w<>, [\\]|&]*/g, '');\n\n        // 5. Generic type parameters on function declarations: `function foo<T>(`\n        source = source.replace(/(\\bfunction\\s+\\w+)\\s*<[^>]*>/g, '$1');\n\n        // 6. Return type annotations on function declarations/expressions:\n        //    `function foo(params): ReturnType {`\n        //    Anchored to `function` keyword to avoid false-matching ternary `) : expr`.\n        source = source.replace(\n            /(\\bfunction\\s*\\w*\\s*\\([^)]*\\))\\s*:\\s*[\\w<>[\\]|&., ]+(?=\\s*\\{)/g,\n            '$1',\n        );\n        // Arrow function return types: `(params): ReturnType =>`\n        source = source.replace(\n            /(\\([^)]*\\))\\s*:\\s*[\\w<>[\\]|&., ]+(?=\\s*=>)/g,\n            '$1',\n        );\n\n        // 7. Strip type annotations from function/method parameter lists only.\n        //    Each parameter is split by comma and handled individually so that\n        //    object-literal key:value pairs are never corrupted.\n        source = BotSandbox.stripFunctionParamTypes(source);\n\n        // 8. Variable type annotations: `const x: Type =`\n        source = source.replace(/((?:const|let|var)\\s+\\w+)\\s*:\\s*[\\w<>[\\]|&., ]+\\s*(?==)/g, '$1 ');\n\n        // 9. Access modifiers and readonly\n        source = source.replace(/\\b(?:public|private|protected|readonly)\\s+/g, '');\n\n        // 10. Non-null assertions: `foo!.bar` → `foo.bar`, `foo!` → `foo`\n        source = source.replace(/(\\w)!/g, '$1');\n\n        // 11. Leftover bare generic casts: `<SomeType>value`\n        source = source.replace(/<\\w[\\w<>, [\\]]*>\\s*(?=[\\w(])/g, '');\n\n        return source;\n    }\n\n    /**\n     * Removes TypeScript `interface Foo { ... }` and `type Foo = { ... }` blocks\n     * using a line-by-line brace-depth scanner so any nesting depth is handled.\n     */\n    private static removeTypescriptBlocks(source: string): string {\n        const lines = source.split('\\n');\n        const result: string[] = [];\n        let depth = 0;\n        let inBlock = false;\n\n        for (const line of lines) {\n            if (!inBlock) {\n                if (/^\\s*(?:export\\s+)?(?:interface|type)\\s+\\w+/.test(line) && line.includes('{')) {\n                    inBlock = true;\n                    depth = 0;\n                    for (const c of line) {\n                        if (c === '{') depth++;\n                        else if (c === '}') depth--;\n                    }\n                    if (depth <= 0) inBlock = false;\n                    continue;\n                }\n                result.push(line);\n            } else {\n                for (const c of line) {\n                    if (c === '{') depth++;\n                    else if (c === '}') depth--;\n                }\n                if (depth <= 0) inBlock = false;\n            }\n        }\n\n        return result.join('\\n');\n    }\n\n    /**\n     * Strips type annotations from function / method parameter lists only.\n     * Each parameter is split by comma and handled individually so that\n     * object-literal `key: value` pairs are never corrupted.\n     */\n    private static stripFunctionParamTypes(source: string): string {\n        // Strip `: TypeAnnotation` from a single parameter token.\n        // Handles optional `?` marker and rest `...` spread.\n        const stripParam = (p: string): string =>\n            p.replace(/^(\\s*(?:\\.{3})?\\w+)\\s*\\??\\s*:\\s*[^,)]+/, '$1');\n\n        const stripParams = (paramStr: string): string => {\n            if (!paramStr.includes(':')) return paramStr;\n            return paramStr.split(',').map(stripParam).join(',');\n        };\n\n        // function declarations / expressions: `function name(params)`\n        source = source.replace(\n            /(\\bfunction\\s*\\w*\\s*)\\(([^)]*)\\)/g,\n            (_m: string, pre: string, params: string) => pre + '(' + stripParams(params) + ')',\n        );\n\n        // Arrow function params: (params) =>\n        source = source.replace(\n            /\\(([^)]*)\\)\\s*(?==\\s*>)/g,\n            (_m: string, params: string) => '(' + stripParams(params) + ') ',\n        );\n\n        return source;\n    }\n\n    /**\n     * Validates a bot script source code for forbidden patterns.\n     * @returns Array of security violation messages, empty if safe.\n     */\n    static validateSource(source: string): string[] {\n        const violations: string[] = [];\n        for (const pattern of FORBIDDEN_PATTERNS) {\n            if (pattern.test(source)) {\n                violations.push(`Forbidden pattern detected: ${pattern.source}`);\n            }\n        }\n        return violations;\n    }\n\n    /**\n     * Validates a file entry from a zip archive.\n     */\n    static validateFileEntry(fileName: string, fileSize: number): string[] {\n        const violations: string[] = [];\n\n        // Check path traversal\n        if (fileName.includes('..') || fileName.startsWith('/') || fileName.startsWith('\\\\')) {\n            violations.push(`Path traversal detected in file name: ${fileName}`);\n        }\n\n        // Check extension\n        const ext = '.' + fileName.split('.').pop()?.toLowerCase();\n        if (!ALLOWED_EXTENSIONS.includes(ext) && !fileName.endsWith('/')) {\n            violations.push(`Disallowed file extension: ${ext} (file: ${fileName})`);\n        }\n\n        // Check file size\n        if (fileSize > MAX_FILE_SIZE) {\n            violations.push(`File too large: ${fileName} (${fileSize} bytes, max ${MAX_FILE_SIZE})`);\n        }\n\n        return violations;\n    }\n\n    /**\n     * Validates the total content of a bot zip.\n     */\n    static validateZipContent(files: { name: string; size: number }[]): string[] {\n        const violations: string[] = [];\n\n        if (files.length > MAX_FILE_COUNT) {\n            violations.push(`Too many files: ${files.length} (max ${MAX_FILE_COUNT})`);\n        }\n\n        const totalSize = files.reduce((sum, f) => sum + f.size, 0);\n        if (totalSize > MAX_TOTAL_SIZE) {\n            violations.push(`Total size too large: ${totalSize} bytes (max ${MAX_TOTAL_SIZE})`);\n        }\n\n        for (const file of files) {\n            violations.push(...this.validateFileEntry(file.name, file.size));\n        }\n\n        return violations;\n    }\n\n    /**\n     * Loads and registers a bot from its main script source code.\n     * The script must export a bot object conforming to ThirdPartyBotInterface.\n     */\n    static loadBotFromSource(\n        mainScript: string,\n        sourceFileName: string,\n    ): ThirdPartyBotMeta | null {\n        // Validate source\n        const violations = this.validateSource(mainScript);\n        if (violations.length > 0) {\n            console.error('[BotSandbox] Security violations:', violations);\n            return null;\n        }\n\n        try {\n            // Create a restricted scope for the bot\n            const exports: any = {};\n            const module = { exports };\n            const restrictedGlobals = {\n                console: {\n                    log: (...args: any[]) => console.log(`[Bot:${sourceFileName}]`, ...args),\n                    warn: (...args: any[]) => console.warn(`[Bot:${sourceFileName}]`, ...args),\n                    error: (...args: any[]) => console.error(`[Bot:${sourceFileName}]`, ...args),\n                    info: (...args: any[]) => console.info(`[Bot:${sourceFileName}]`, ...args),\n                },\n                Math,\n                Date,\n                JSON,\n                Array,\n                Object,\n                String,\n                Number,\n                Boolean,\n                Map,\n                Set,\n                Promise,\n                parseInt,\n                parseFloat,\n                isNaN,\n                isFinite,\n                undefined,\n                NaN,\n                Infinity,\n            };\n\n            // Execute bot script in restricted scope\n            const wrappedScript = `\n                \"use strict\";\n                return (function(module, exports, ${Object.keys(restrictedGlobals).join(', ')}) {\n                    ${mainScript}\n                    return module.exports;\n                });\n            `;\n\n            const factory = new Function(wrappedScript)();\n            const botExport = factory(\n                module,\n                exports,\n                ...Object.values(restrictedGlobals),\n            );\n\n            // Validate bot export\n            if (!botExport || !botExport.id || !botExport.createBot) {\n                console.error('[BotSandbox] Invalid bot export: must have \"id\" and \"createBot\"');\n                return null;\n            }\n\n            const meta: ThirdPartyBotMeta = {\n                id: String(botExport.id),\n                displayName: String(botExport.displayName || botExport.id),\n                version: String(botExport.version || '1.0.0'),\n                author: String(botExport.author || 'Unknown'),\n                description: botExport.description ? String(botExport.description) : undefined,\n                factory: (name: string, country: string): ThirdPartyBotInterface => {\n                    const bot = botExport.createBot(name, country);\n                    return {\n                        id: meta.id,\n                        displayName: meta.displayName,\n                        version: meta.version,\n                        author: meta.author,\n                        description: meta.description,\n                        onGameStart: (gameApi: any) => bot.onGameStart?.(gameApi),\n                        onGameTick: (gameApi: any) => bot.onGameTick?.(gameApi),\n                        onGameEvent: (event: any, data: any) => bot.onGameEvent?.(event, data),\n                        dispose: () => bot.dispose?.(),\n                    };\n                },\n                builtIn: false,\n                sourceFile: sourceFileName,\n            };\n\n            // Register the bot with source for persistence\n            BotRegistry.getInstance().registerWithSource(meta, mainScript, sourceFileName);\n            return meta;\n        } catch (e) {\n            console.error('[BotSandbox] Failed to load bot:', e);\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/BotUploader.ts",
    "content": "import { BotSandbox } from './BotSandbox';\nimport { ThirdPartyBotMeta } from './ThirdPartyBotInterface';\n\n/**\n * Handles bot zip file upload, extraction, validation, and registration.\n */\nexport class BotUploader {\n    /**\n     * Process an uploaded bot zip file.\n     * Extracts, validates security, and registers the bot.\n     * @returns The registered bot metadata, or null if failed.\n     */\n    static async processUpload(file: File): Promise<{\n        success: boolean;\n        meta?: ThirdPartyBotMeta;\n        errors?: string[];\n    }> {\n        // Validate file type\n        if (!file.name.endsWith('.zip')) {\n            return { success: false, errors: ['Only .zip files are allowed.'] };\n        }\n\n        // Validate file size (max 10MB)\n        if (file.size > 10 * 1024 * 1024) {\n            return { success: false, errors: ['File too large (max 10MB).'] };\n        }\n\n        try {\n            const arrayBuffer = await file.arrayBuffer();\n            const files = await this.extractZip(arrayBuffer);\n\n            console.log(`[BotUploader] Extracted ${files.length} files:`, files.map(f => `${f.name} (${f.content.length}B)`));\n\n            // Validate zip content\n            const contentViolations = BotSandbox.validateZipContent(\n                files.map(f => ({ name: f.name, size: f.content.length }))\n            );\n            if (contentViolations.length > 0) {\n                return { success: false, errors: contentViolations };\n            }\n\n            // Find main entry point (bot.ts or index.ts)\n            const mainFile = files.find(\n                f => f.name === 'bot.ts' || f.name === 'index.ts'\n            ) || files.find(\n                f => f.name.endsWith('/bot.ts') || f.name.endsWith('/index.ts')\n            );\n\n            if (!mainFile) {\n                return {\n                    success: false,\n                    errors: [`No bot.ts or index.ts found in zip root. Files in zip: [${files.map(f => f.name).join(', ')}]`],\n                };\n            }\n\n            const rawSource = new TextDecoder().decode(mainFile.content);\n            const source = BotSandbox.stripTypes(rawSource);\n\n            // Validate source code\n            const sourceViolations = BotSandbox.validateSource(source);\n            if (sourceViolations.length > 0) {\n                return { success: false, errors: sourceViolations };\n            }\n\n            // Load and register the bot\n            const meta = BotSandbox.loadBotFromSource(source, file.name);\n            if (!meta) {\n                return {\n                    success: false,\n                    errors: ['Failed to load bot. Check that your bot exports the required interface.'],\n                };\n            }\n\n            return { success: true, meta };\n        } catch (e) {\n            return {\n                success: false,\n                errors: [`Failed to process zip: ${(e as Error).message}`],\n            };\n        }\n    }\n\n    /**\n     * Simple zip extraction using the browser's built-in capabilities.\n     * Handles basic PK-format zip files.\n     */\n    private static async extractZip(\n        buffer: ArrayBuffer,\n    ): Promise<{ name: string; content: Uint8Array }[]> {\n        const view = new DataView(buffer);\n        const bytes = new Uint8Array(buffer);\n        const files: { name: string; content: Uint8Array }[] = [];\n\n        // ── 1. Locate End-of-Central-Directory record (EOCD) ──\n        // Scan backwards from the end; EOCD signature = 0x06054b50.\n        let eocdOffset = -1;\n        for (let i = bytes.length - 22; i >= 0 && i >= bytes.length - 65558; i--) {\n            if (view.getUint32(i, true) === 0x06054b50) {\n                eocdOffset = i;\n                break;\n            }\n        }\n        if (eocdOffset === -1) {\n            throw new Error('Invalid zip: end-of-central-directory record not found');\n        }\n\n        const cdEntryCount = view.getUint16(eocdOffset + 10, true);\n        const cdOffset = view.getUint32(eocdOffset + 16, true);\n\n        // ── 2. Walk the central directory to collect file metadata ──\n        interface CdEntry {\n            fileName: string;\n            compressionMethod: number;\n            compressedSize: number;\n            uncompressedSize: number;\n            localHeaderOffset: number;\n        }\n        const entries: CdEntry[] = [];\n        let pos = cdOffset;\n\n        for (let i = 0; i < cdEntryCount; i++) {\n            if (pos + 46 > bytes.length) break;\n            const sig = view.getUint32(pos, true);\n            if (sig !== 0x02014b50) break; // not a central directory entry\n\n            const compressionMethod = view.getUint16(pos + 10, true);\n            const compressedSize = view.getUint32(pos + 20, true);\n            const uncompressedSize = view.getUint32(pos + 24, true);\n            const fileNameLength = view.getUint16(pos + 28, true);\n            const extraFieldLength = view.getUint16(pos + 30, true);\n            const commentLength = view.getUint16(pos + 32, true);\n            const localHeaderOffset = view.getUint32(pos + 42, true);\n\n            const fileNameBytes = new Uint8Array(buffer, pos + 46, fileNameLength);\n            const fileName = new TextDecoder().decode(fileNameBytes);\n\n            entries.push({ fileName, compressionMethod, compressedSize, uncompressedSize, localHeaderOffset });\n            pos += 46 + fileNameLength + extraFieldLength + commentLength;\n        }\n\n        // ── 3. Extract each file using its local header + central-dir sizes ──\n        for (const entry of entries) {\n            // Skip directory entries\n            if (entry.fileName.endsWith('/')) continue;\n\n            const lh = entry.localHeaderOffset;\n            if (lh + 30 > bytes.length) continue;\n\n            const lhFileNameLength = view.getUint16(lh + 26, true);\n            const lhExtraFieldLength = view.getUint16(lh + 28, true);\n            const dataOffset = lh + 30 + lhFileNameLength + lhExtraFieldLength;\n\n            if (entry.compressionMethod === 0) {\n                // Stored (no compression)\n                if (dataOffset + entry.uncompressedSize > bytes.length) continue;\n                const content = new Uint8Array(buffer, dataOffset, entry.uncompressedSize);\n                files.push({ name: entry.fileName, content: new Uint8Array(content) });\n            } else if (entry.compressionMethod === 8) {\n                // Deflate\n                if (dataOffset + entry.compressedSize > bytes.length) continue;\n                const compressedData = new Uint8Array(buffer, dataOffset, entry.compressedSize);\n                try {\n                    const decompressed = await this.inflateRaw(compressedData);\n                    files.push({ name: entry.fileName, content: decompressed });\n                } catch {\n                    console.warn(`[BotUploader] Skipping file ${entry.fileName}: decompression failed`);\n                }\n            }\n        }\n\n        return files;\n    }\n\n    /**\n     * Decompress raw deflate data using DecompressionStream API.\n     */\n    private static async inflateRaw(data: Uint8Array): Promise<Uint8Array> {\n        const ds = new DecompressionStream('deflate-raw');\n        const writer = ds.writable.getWriter();\n        writer.write(data as unknown as BufferSource);\n        writer.close();\n\n        const reader = ds.readable.getReader();\n        const chunks: Uint8Array[] = [];\n        let totalLength = 0;\n\n        while (true) {\n            const { done, value } = await reader.read();\n            if (done) break;\n            chunks.push(value);\n            totalLength += value.length;\n        }\n\n        const result = new Uint8Array(totalLength);\n        let offset = 0;\n        for (const chunk of chunks) {\n            result.set(chunk, offset);\n            offset += chunk.length;\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/ThirdPartyBotAdapter.ts",
    "content": "import { Bot } from '../../bot/Bot';\nimport { ThirdPartyBotInterface, ThirdPartyBotMeta } from './ThirdPartyBotInterface';\n\n/**\n * Adapter that wraps a ThirdPartyBotInterface into the game's Bot class.\n *\n * The inner bot's onGameStart / onGameTick receive a context object:\n *   { gameApi, actionsApi, productionApi, logger, playerName, country }\n */\nexport class ThirdPartyBotAdapter extends Bot {\n    private thirdPartyBot: ThirdPartyBotInterface;\n\n    constructor(name: string, country: string, meta: ThirdPartyBotMeta) {\n        super(name, country);\n        this.thirdPartyBot = meta.factory(name, country);\n    }\n\n    private createContext(gameApi: any) {\n        return {\n            gameApi,\n            actionsApi: this.actionsApi,\n            productionApi: this.productionApi,\n            logger: this.logger,\n            playerName: this.name,\n            country: this.country,\n        };\n    }\n\n    override onGameStart(event: any): void {\n        try {\n            this.thirdPartyBot.onGameStart(this.createContext(event));\n        } catch (e) {\n            console.error(`[ThirdPartyBot:${this.thirdPartyBot.id}] Error in onGameStart:`, e);\n        }\n    }\n\n    override onGameTick(event: any): void {\n        try {\n            this.thirdPartyBot.onGameTick(this.createContext(event));\n        } catch (e) {\n            if (event?.getCurrentTick?.() % 150 === 0) {\n                console.error(`[ThirdPartyBot:${this.thirdPartyBot.id}] Error in onGameTick:`, e);\n            }\n        }\n    }\n\n    override onGameEvent(event: any, data: any): void {\n        try {\n            this.thirdPartyBot.onGameEvent(event, data);\n        } catch (e) {\n            console.error(`[ThirdPartyBot:${this.thirdPartyBot.id}] Error in onGameEvent:`, e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/ThirdPartyBotInterface.ts",
    "content": "/**\n * Third-party bot interface definition.\n * All custom AI bots must implement this interface.\n */\nexport interface ThirdPartyBotInterface {\n    /** Unique identifier for the bot */\n    readonly id: string;\n    /** Display name of the bot */\n    readonly displayName: string;\n    /** Bot version */\n    readonly version: string;\n    /** Bot author */\n    readonly author: string;\n    /** Bot description */\n    readonly description?: string;\n\n    /**\n     * Called when the game starts.\n     * Use this to initialize bot state, scan enemies, etc.\n     */\n    onGameStart(gameApi: any): void;\n\n    /**\n     * Called each game tick.\n     * This is where the main bot logic should go.\n     */\n    onGameTick(gameApi: any): void;\n\n    /**\n     * Called when a game event occurs (unit destroyed, ownership change, etc.)\n     */\n    onGameEvent(event: any, data: any): void;\n\n    /**\n     * Called when the bot is disposed/destroyed.\n     * Clean up any resources here.\n     */\n    dispose?(): void;\n}\n\n/**\n * Metadata for a registered third-party bot.\n */\nexport interface ThirdPartyBotMeta {\n    id: string;\n    displayName: string;\n    version: string;\n    author: string;\n    description?: string;\n    /** The factory function that creates a bot instance */\n    factory: (name: string, country: string) => ThirdPartyBotInterface;\n    /** Whether this bot is built-in (not uploaded by user) */\n    builtIn: boolean;\n    /** Source zip file name (for uploaded bots) */\n    sourceFile?: string;\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/BuiltInBotAdapter.ts",
    "content": "import { Bot } from '../../../bot/Bot';\nimport { BuiltInBot } from './bot/bot';\nimport { BotRegistry } from '../BotRegistry';\nimport { Countries } from './bot/logic/common/utils';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { QueueType, QueueStatus } from '@/game/player/production/ProductionQueue';\nimport { OrderType } from '@/game/order/OrderType';\n\n/**\n * BuiltInBotAdapter — wraps the real BuiltInBot.\n * Delegates all lifecycle methods to the underlying BuiltInBot instance.\n *\n * Source: https://github.com/Supalosa/supalosa-chronodivide-bot\n */\nexport class BuiltInBotAdapter extends Bot {\n    private innerBot: BuiltInBot;\n    private failSafePendingBuildingType: string | null = null;\n    private lastFailSafeDeployTick: number = -9999;\n    private failSafeDeployAttempts: number = 0;\n\n    private static readonly ALLIED_COUNTRIES = [\n        'Americans', 'British', 'French', 'Germans', 'Koreans', 'Alliance',\n    ];\n\n    private static readonly FAIL_SAFE_BUILD_ORDER_ALLIED = ['GAPOWR', 'GAREFN', 'GAPILE', 'GAWEAP'];\n    private static readonly FAIL_SAFE_BUILD_ORDER_SOVIET = ['NAPOWR', 'NAREFN', 'NAHAND', 'NAWEAP'];\n\n    constructor(name: string, country: string) {\n        super(name, country);\n        this.innerBot = new BuiltInBot(name, country as Countries);\n    }\n\n    override setGameApi(api: any): void {\n        super.setGameApi(api);\n        this.innerBot.setGameApi(api);\n    }\n\n    override setActionsApi(api: any): void {\n        super.setActionsApi(api);\n        this.innerBot.setActionsApi(api);\n    }\n\n    override setProductionApi(api: any): void {\n        super.setProductionApi(api);\n        this.innerBot.setProductionApi(api);\n    }\n\n    override setLogger(logger: any): void {\n        super.setLogger(logger);\n        this.innerBot.setLogger(logger);\n    }\n\n    override setDebugMode(debug: boolean): Bot {\n        super.setDebugMode(debug);\n        this.innerBot.setDebugMode(debug);\n        return this;\n    }\n\n    override onGameStart(event: any): void {\n        console.log(`[BuiltInBotAdapter] onGameStart called for \"${this.name}\" country=\"${this.country}\"`);\n        try {\n            this.innerBot.onGameStart(event);\n            console.log(`[BuiltInBotAdapter] onGameStart completed for \"${this.name}\"`);\n        } catch (e) {\n            console.error(`[BuiltInBotAdapter] onGameStart FAILED for \"${this.name}\":`, e);\n            throw e;\n        }\n    }\n\n    override onGameTick(event: any): void {\n        try {\n            this.innerBot.onGameTick(event);\n        } catch (e) {\n            this.logger?.error?.('BuiltInBot tick error:', e);\n            console.error(`[BuiltInBotAdapter] tick error for \"${this.name}\":`, e);\n            // Keep the AI alive even if the imported bot throws.\n            this.runFailSafeTick(event);\n            return;\n        }\n        // Non-invasive safety net for \"AI stands still\" scenarios.\n        this.runFailSafeTick(event);\n    }\n\n    override onGameEvent(event: any): void {\n        try {\n            this.innerBot.onGameEvent(event);\n        } catch (e) {\n            this.logger?.error?.('BuiltInBot event error:', e);\n        }\n    }\n\n    private runFailSafeTick(gameApi: any): void {\n        if (!this.productionApi || !this.actionsApi || !gameApi) {\n            if (gameApi?.getCurrentTick?.() % 150 === 0) {\n                console.warn(`[BuiltInBotAdapter] \"${this.name}\" failsafe skipped: productionApi=${!!this.productionApi} actionsApi=${!!this.actionsApi} gameApi=${!!gameApi}`);\n            }\n            return;\n        }\n\n        // Keep fallback low-frequency to reduce interference with normal logic.\n        if (gameApi.getCurrentTick() % 15 !== 0) {\n            return;\n        }\n\n        const conYards = gameApi.getVisibleUnits(this.name, 'self', (r: any) => r.constructionYard);\n        if (conYards.length === 0) {\n            if (gameApi.getCurrentTick() < this.lastFailSafeDeployTick + 30) {\n                return;\n            }\n            const mcvs = gameApi.getVisibleUnits(\n                this.name,\n                'self',\n                (r: any) => !!r.deploysInto && gameApi.getGeneralRules().baseUnit.includes(r.name),\n            );\n            if (mcvs.length > 0) {\n                this.failSafeDeployAttempts++;\n                if (this.failSafeDeployAttempts > 5) {\n                    // Deploy keeps failing — find a clear spot nearby and move there\n                    const mcvData = gameApi.getUnitData(mcvs[0]);\n                    if (mcvData?.tile && mcvData.rules?.deploysInto) {\n                        const cx = mcvData.tile.rx;\n                        const cy = mcvData.tile.ry;\n                        let found = false;\n                        for (let radius = 2; radius <= 10 && !found; radius++) {\n                            for (let dx = -radius; dx <= radius && !found; dx++) {\n                                for (let dy = -radius; dy <= radius && !found; dy++) {\n                                    if (Math.abs(dx) !== radius && Math.abs(dy) !== radius) continue;\n                                    const tx = cx + dx;\n                                    const ty = cy + dy;\n                                    try {\n                                        if (gameApi.canPlaceBuilding(this.name, mcvData.rules.deploysInto, { rx: tx, ry: ty })) {\n                                            this.actionsApi.orderUnits([mcvs[0]], OrderType.Move, tx, ty);\n                                            this.failSafeDeployAttempts = 0;\n                                            found = true;\n                                        }\n                                    } catch (_e) { /* skip */ }\n                                }\n                            }\n                        }\n                        if (!found) {\n                            // No valid spot, scatter and reset\n                            this.actionsApi.orderUnits([mcvs[0]], OrderType.Scatter);\n                            this.failSafeDeployAttempts = 0;\n                        }\n                    }\n                } else {\n                    this.actionsApi.orderUnits([mcvs[0]], OrderType.DeploySelected);\n                }\n                this.lastFailSafeDeployTick = gameApi.getCurrentTick();\n            }\n            return;\n        }\n        // Conyard exists, reset deploy attempts\n        this.failSafeDeployAttempts = 0;\n\n        const queueData = this.productionApi.getQueueData(QueueType.Structures);\n\n        if (queueData.status === QueueStatus.OnHold) {\n            this.actionsApi.resumeProduction(QueueType.Structures);\n        }\n\n        if (queueData.status === QueueStatus.Ready && queueData.items.length > 0) {\n            const readyType = queueData.items[0]?.rules?.name || this.failSafePendingBuildingType;\n            if (readyType) {\n                this.tryPlaceBuildingNearConyard(gameApi, readyType);\n            }\n            return;\n        }\n\n        const queueHasItems = Array.isArray(queueData.items) && queueData.items.length > 0;\n        if (\n            queueHasItems &&\n            queueData.status !== QueueStatus.Idle &&\n            queueData.status !== QueueStatus.OnHold\n        ) {\n            return;\n        }\n\n        const available = this.productionApi\n            .getAvailableObjects(QueueType.Structures)\n            .map((o: any) => o.name);\n        if (available.length === 0) {\n            return;\n        }\n\n        const buildOrder = this.isAlliedCountry(this.country)\n            ? BuiltInBotAdapter.FAIL_SAFE_BUILD_ORDER_ALLIED\n            : BuiltInBotAdapter.FAIL_SAFE_BUILD_ORDER_SOVIET;\n\n        const ownedBuildingNames = new Set(\n            gameApi\n                .getVisibleUnits(this.name, 'self', (r: any) => r.type === ObjectType.Building)\n                .map((id: any) => gameApi.getGameObjectData(id)?.name)\n                .filter((n: any) => !!n),\n        );\n\n        let nextBuild = buildOrder.find((name) => {\n            if (!available.includes(name)) {\n                return false;\n            }\n            // Allow building extra power if needed.\n            if (name.endsWith('POWR')) {\n                return true;\n            }\n            return !ownedBuildingNames.has(name);\n        });\n\n        // If predefined order is unavailable for this ruleset/mod, build any available structure to avoid deadlock.\n        if (!nextBuild) {\n            nextBuild = available[0];\n        }\n\n        if (nextBuild) {\n            try {\n                this.actionsApi.queueForProduction(QueueType.Structures, ObjectType.Building, nextBuild, 1);\n                this.failSafePendingBuildingType = nextBuild;\n            } catch (err) {\n                this.logger?.error?.('BuiltIn fail-safe queueForProduction failed', nextBuild, err);\n            }\n        }\n    }\n\n    private tryPlaceBuildingNearConyard(gameApi: any, buildingType: string): void {\n        const conYards = gameApi.getVisibleUnits(this.name, 'self', (r: any) => r.constructionYard);\n        if (conYards.length === 0) {\n            return;\n        }\n\n        const conYardData = gameApi.getUnitData(conYards[0]);\n        if (!conYardData?.tile) {\n            return;\n        }\n\n        const cx = conYardData.tile.rx;\n        const cy = conYardData.tile.ry;\n\n        for (let radius = 3; radius <= 15; radius++) {\n            for (let dx = -radius; dx <= radius; dx++) {\n                for (let dy = -radius; dy <= radius; dy++) {\n                    if (Math.abs(dx) !== radius && Math.abs(dy) !== radius) {\n                        continue;\n                    }\n                    const tx = cx + dx;\n                    const ty = cy + dy;\n                    try {\n                        if (gameApi.canPlaceBuilding(this.name, buildingType, { rx: tx, ry: ty })) {\n                            this.actionsApi.placeBuilding(buildingType, tx, ty);\n                            this.failSafePendingBuildingType = null;\n                            return;\n                        }\n                    } catch (_e) {\n                        // Keep scanning nearby tiles.\n                    }\n                }\n            }\n        }\n\n        this.logger?.info?.(`BuiltIn fail-safe could not place ${buildingType} near conyard`);\n    }\n\n    private isAlliedCountry(countryName: string): boolean {\n        const c = (countryName || '').toLowerCase();\n        return BuiltInBotAdapter.ALLIED_COUNTRIES.some((name) => name.toLowerCase() === c);\n    }\n}\n\n/**\n * Register BuiltInBot as a built-in third-party bot.\n */\nexport function registerBuiltInBot(): void {\n    BotRegistry.getInstance().register({\n        id: 'builtIn-bot',\n        displayName: 'AI-普通 (BuiltIn)',\n        version: '0.6.1',\n        author: 'BuiltIn',\n        description: 'Normal difficulty AI. Full strategy system with missions, threat analysis, and build prioritization.',\n        factory: (name: string, country: string) => {\n            const bot = new BuiltInBotAdapter(name, country);\n            return {\n                id: 'builtIn-bot',\n                displayName: 'AI-普通 (BuiltIn)',\n                version: '0.6.1',\n                author: 'BuiltIn',\n                description: 'Normal difficulty AI',\n                onGameStart: (gameApi: any) => bot.onGameStart(gameApi),\n                onGameTick: (gameApi: any) => bot.onGameTick(gameApi),\n                onGameEvent: (event: any, _data: any) => bot.onGameEvent(event),\n            };\n        },\n        builtIn: true,\n    });\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/bot.ts",
    "content": "import { ApiEventType, Bot, GameApi, ApiEvent, ObjectType, FactoryType, QueueType, OrderType } from \"../game-api\";\n\nimport { MissionController } from \"./logic/mission/missionController\";\nimport { QueueController } from \"./logic/building/queueController\";\nimport { MatchAwareness, MatchAwarenessImpl } from \"./logic/awareness\";\nimport { Countries, formatTimeDuration } from \"./logic/common/utils\";\nimport { IncrementalGridCache } from \"./logic/map/incrementalGridCache\";\nimport { SupabotContext } from \"./logic/common/context\";\nimport { Strategy } from \"./strategy/strategy\";\nimport { DefaultStrategy } from \"./strategy/defaultStrategy\";\nimport { BaseBuildingMission } from \"./logic/mission/missions/baseBuildingMission\";\n\nconst DEBUG_STATE_UPDATE_INTERVAL_SECONDS = 6;\n\nconst DEBUG_MESSAGES_BUFFER_LENGTH = 20;\n\n// Number of ticks per second at the base speed.\nconst NATURAL_TICK_RATE = 15;\n\nexport class BuiltInBot extends Bot {\n    private tickRatio?: number;\n    private queueController: QueueController;\n    private tickOfLastAttackOrder: number = 0;\n    private lastDeployAttemptTick: number = -9999;\n    private deployAttemptCount: number = 0;\n\n    private missionController: MissionController | null = null;\n    private matchAwareness: MatchAwareness | null = null;\n\n    // Messages to display in visualisation mode only.\n    public _debugMessages: string[] = [];\n    public _globalDebugText: string = \"\";\n    public _debugGridCaches: { grid: IncrementalGridCache<any>; tag: string }[] = [];\n\n    constructor(\n        name: string,\n        country: Countries,\n        private tryAllyWith: string[] = [],\n        private enableLogging = true,\n        private strategy: Strategy = new DefaultStrategy(),\n    ) {\n        super(name, country);\n        this.queueController = new QueueController();\n    }\n\n    override onGameStart(game: GameApi) {\n        const gameRate = game.getTickRate();\n        const botApm = 300;\n        const botRate = botApm / 60;\n        this.tickRatio = Math.ceil(gameRate / botRate);\n\n        const myPlayer = game.getPlayerData(this.name);\n\n        if (!myPlayer.country) {\n            throw new Error(`Player ${this.name} has no country`);\n        }\n        this.missionController = new MissionController((message, sayInGame) => this.logBotStatus(message, sayInGame));\n\n        // TODO: Strategy should have an onGameStart call which sets up the initial missions.\n        this.missionController.addMission(\n            new BaseBuildingMission(QueueType.Structures, (message, sayInGame) =>\n                this.logBotStatus(message, sayInGame),\n            ),\n        );\n        this.missionController.addMission(\n            new BaseBuildingMission(QueueType.Armory, (message, sayInGame) => this.logBotStatus(message, sayInGame)),\n        );\n\n        this.matchAwareness = new MatchAwarenessImpl(\n            game,\n            myPlayer,\n            null,\n            myPlayer.startLocation,\n            (message, sayInGame) => this.logBotStatus(message, sayInGame),\n        );\n\n        this._debugGridCaches = [\n            { grid: this.matchAwareness.getSectorCache(), tag: \"sector-cache\" },\n            { grid: this.matchAwareness.getBuildSpaceCache()._cache, tag: \"build-cache\" },\n        ];\n\n        this.matchAwareness.onGameStart(game, myPlayer);\n\n        this.tryAllyWith\n            .filter((playerName) => playerName !== this.name)\n            .forEach((playerName) => this.actionsApi.toggleAlliance(playerName, true));\n    }\n\n    override onGameTick(game: GameApi) {\n        if (!this.matchAwareness || !this.missionController || !this.strategy) {\n            if (game.getCurrentTick() % 150 === 0) {\n                console.warn(`[BuiltInBot] \"${this.name}\" tick skipped: awareness=${!!this.matchAwareness} missions=${!!this.missionController} strategy=${!!this.strategy}`);\n            }\n            return;\n        }\n\n        // Periodic heartbeat log\n        if (game.getCurrentTick() % 300 === 0) {\n            const myPlayer = game.getPlayerData(this.name);\n            const conYards = game.getVisibleUnits(this.name, 'self', (r) => r.constructionYard);\n            const allUnits = game.getVisibleUnits(this.name, 'self');\n            console.log(`[BuiltInBot] \"${this.name}\" tick=${game.getCurrentTick()} credits=${myPlayer.credits} units=${allUnits.length} conyards=${conYards.length}`);\n        }\n\n        let threatCache = this.matchAwareness.getThreatCache();\n\n        if ((game.getCurrentTick() / NATURAL_TICK_RATE) % DEBUG_STATE_UPDATE_INTERVAL_SECONDS === 0) {\n            this.updateDebugState(game);\n        }\n\n        if (game.getCurrentTick() % this.tickRatio! === 0) {\n            this.tryInitialMcvDeploy(game);\n\n            try {\n                this.matchAwareness.onAiUpdate(this.context);\n                threatCache = this.matchAwareness.getThreatCache();\n            } catch (err) {\n                this.logger?.error?.(\"BuiltIn awareness update failed\", err);\n            }\n\n            const fullContext: SupabotContext = {\n                ...this.context,\n                matchAwareness: this.matchAwareness,\n            };\n\n            // hacky resign condition\n            const armyUnits = game.getVisibleUnits(this.name, \"self\", (r) => r.isSelectableCombatant);\n            const mcvUnits = game.getVisibleUnits(\n                this.name,\n                \"self\",\n                (r) => !!r.deploysInto && game.getGeneralRules().baseUnit.includes(r.name),\n            );\n            const productionBuildings = game.getVisibleUnits(\n                this.name,\n                \"self\",\n                (r) => r.type == ObjectType.Building && r.factory != FactoryType.None,\n            );\n            if (armyUnits.length == 0 && productionBuildings.length == 0 && mcvUnits.length == 0) {\n                this.logBotStatus(`No army or production left, quitting.`);\n                this.context.player.actions.quitGame();\n            }\n\n            // Mission/strategy logic every 3 ticks.\n            if (this.context.game.getCurrentTick() % 3 === 0) {\n                this.missionController.onAiUpdate(fullContext);\n                this.strategy = this.strategy.onAiUpdate(fullContext, this.missionController, (message, sayInGame) =>\n                    this.logBotStatus(message, sayInGame),\n                );\n            }\n\n            const unitTypeRequests = this.missionController.getRequestedUnitTypes();\n\n            // Queue-controller logic.\n            this.queueController.onAiUpdate(fullContext, threatCache, unitTypeRequests, (message) =>\n                this.logBotStatus(message),\n            );\n        }\n    }\n\n    private tryInitialMcvDeploy(game: GameApi): void {\n        const hasConyard = game.getVisibleUnits(this.name, \"self\", (r) => r.constructionYard).length > 0;\n        if (hasConyard) {\n            this.deployAttemptCount = 0;\n            return;\n        }\n\n        if (game.getCurrentTick() < this.lastDeployAttemptTick + 30) {\n            return;\n        }\n\n        const mcvUnits = game.getVisibleUnits(\n            this.name,\n            \"self\",\n            (r) => !!r.deploysInto && game.getGeneralRules().baseUnit.includes(r.name),\n        );\n\n        if (mcvUnits.length === 0) {\n            return;\n        }\n\n        this.deployAttemptCount++;\n\n        if (this.deployAttemptCount > 5) {\n            // Deploy keeps failing — current position is blocked, find a clear spot\n            const mcvData = game.getUnitData(mcvUnits[0]);\n            if (mcvData?.tile && mcvData.rules?.deploysInto) {\n                const cx = mcvData.tile.rx;\n                const cy = mcvData.tile.ry;\n                for (let radius = 2; radius <= 10; radius++) {\n                    for (let dx = -radius; dx <= radius; dx++) {\n                        for (let dy = -radius; dy <= radius; dy++) {\n                            if (Math.abs(dx) !== radius && Math.abs(dy) !== radius) continue;\n                            try {\n                                if (game.canPlaceBuilding(this.name, mcvData.rules.deploysInto, { rx: cx + dx, ry: cy + dy })) {\n                                    this.actionsApi.orderUnits([mcvUnits[0]], OrderType.Move, cx + dx, cy + dy);\n                                    this.deployAttemptCount = 0;\n                                    this.lastDeployAttemptTick = game.getCurrentTick();\n                                    return;\n                                }\n                            } catch (_e) { /* skip */ }\n                        }\n                    }\n                }\n            }\n            // Nothing found, scatter\n            this.actionsApi.orderUnits([mcvUnits[0]], OrderType.Scatter);\n            this.deployAttemptCount = 0;\n        } else {\n            this.actionsApi.orderUnits([mcvUnits[0]], OrderType.DeploySelected);\n        }\n        this.lastDeployAttemptTick = game.getCurrentTick();\n    }\n\n    private getHumanTimestamp(game: GameApi) {\n        return formatTimeDuration(game.getCurrentTick() / NATURAL_TICK_RATE);\n    }\n\n    private logBotStatus(message: string, sayInGame: boolean = false) {\n        if (!this.enableLogging) {\n            return;\n        }\n        this.logger.info(message);\n        const timestamp = this.getHumanTimestamp(this.gameApi);\n        if (sayInGame) {\n            this.actionsApi.sayAll(`${timestamp}: ${message}`);\n        }\n        this.pushDebugMessage(`${timestamp}: ${message}`);\n    }\n\n    private updateDebugState(game: GameApi) {\n        if (!this.getDebugMode() || !this.missionController) {\n            return;\n        }\n        // Update the global debug text.\n        const myPlayer = game.getPlayerData(this.name);\n        const harvesters = game.getVisibleUnits(this.name, \"self\", (r) => r.harvester).length;\n\n        let globalDebugText = `Cash: ${myPlayer.credits} | Harvesters: ${harvesters}\\n`;\n        globalDebugText += this.queueController.getGlobalDebugText(this.gameApi, this.productionApi);\n        globalDebugText += this.missionController.getGlobalDebugText(this.gameApi);\n        globalDebugText += this.matchAwareness?.getGlobalDebugText();\n\n        this.missionController.updateDebugText(this.actionsApi);\n\n        // Tag enemy units with IDs\n        game.getVisibleUnits(this.name, \"enemy\").forEach((unitId) => {\n            this.actionsApi.setUnitDebugText(unitId, unitId.toString());\n        });\n\n        this.actionsApi.setGlobalDebugText(globalDebugText);\n        this._globalDebugText = globalDebugText;\n    }\n\n    override onGameEvent(ev: ApiEvent) {\n        switch (ev.type) {\n            case ApiEventType.ObjectDestroy: {\n                // Add to the stalemate detection.\n                if (ev.attackerInfo?.playerName == this.name) {\n                    this.tickOfLastAttackOrder += (this.gameApi.getCurrentTick() - this.tickOfLastAttackOrder) / 2;\n                }\n                break;\n            }\n            default:\n                break;\n        }\n    }\n\n    protected pushDebugMessage(message: string) {\n        if (this._debugMessages.length + 1 > DEBUG_MESSAGES_BUFFER_LENGTH) {\n            this._debugMessages.shift();\n        }\n        this._debugMessages.push(message);\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/awareness.ts",
    "content": "import { BotContext, GameApi, GameObjectData, MapApi, PathNode, PlayerData, Size, SpeedType, Vector2 } from \"../../game-api\";\nimport { calculateConnectedSectorIds, SectorCache } from \"./map/sector\";\nimport { GlobalThreat } from \"./threat/threat\";\nimport { calculateGlobalThreat } from \"./threat/threatCalculator\";\nimport { calculateAreaVisibility, getPointTowardsOtherPoint } from \"./map/map\";\nimport { Circle, Quadtree } from \"@timohausmann/quadtree-ts\";\nimport { ScoutingManager } from \"./common/scout\";\nimport { getDiagonalMapBounds, IncrementalGridCache } from \"./map/incrementalGridCache\";\nimport { calculateDiffuseSectorThreat, calculateMoney, calculateSectorThreat } from \"./threat/sectorThreat\";\nimport { BuildSpaceCache } from \"./map/buildSpaceCache\";\nimport { getSectorId } from \"./map/sectorUtils\";\n\nexport type UnitPositionQuery = { x: number; y: number; unitId: number };\n\n/**\n * The bot's understanding of the current state of the game.\n */\nexport interface MatchAwareness {\n    /**\n     * Returns the threat cache for the AI.\n     */\n    getThreatCache(): GlobalThreat | null;\n\n    /**\n     * Returns the sector visibility cache.\n     */\n    getSectorCache(): SectorCache;\n\n    getBuildSpaceCache(): BuildSpaceCache;\n\n    /**\n     * Returns the enemy unit IDs in a certain radius of a point.\n     * Warning: this may return non-combatant hostiles, such as neutral units.\n     */\n    getHostilesNearPoint2d(point: Vector2, radius: number): UnitPositionQuery[];\n\n    /**\n     * Returns the enemy unit IDs in a certain radius of a point.\n     * Warning: this may return non-combatant hostiles, such as neutral units.\n     */\n    getHostilesNearPoint(x: number, y: number, radius: number): UnitPositionQuery[];\n\n    /**\n     * Returns the main rally point for the AI, which updates every few ticks.\n     */\n    getMainRallyPoint(): Vector2;\n\n    onGameStart(gameApi: GameApi, playerData: PlayerData): void;\n\n    /**\n     * Update the internal state of the Ai.\n     */\n    onAiUpdate(context: BotContext): void;\n\n    /**\n     * True if the AI should initiate an attack.\n     */\n    shouldAttack(): boolean;\n\n    getScoutingManager(): ScoutingManager;\n\n    getNextExpansionCandidates(): Vector2[];\n\n    getGlobalDebugText(): string | undefined;\n}\n\nconst SECTORS_TO_UPDATE_PER_CYCLE = 12;\n\nconst RALLY_POINT_UPDATE_INTERVAL_TICKS = 90;\n\nconst THREAT_UPDATE_INTERVAL_TICKS = 30;\n\nconst EXPANSION_UPDATE_INTERVAL_TICKS = 240;\n\nconst EXPANSION_MIN_MONEY = 4000;\nconst EXPANSION_MIN_DISTANCE_TO_BUILDABLE = 20;\nconst EXPANSION_MIN_CLEAR_SPACE_TILES = 9; // minimum \"clear space\" required to expand somewhere (should be large enough to fit conyard and refinery)\n\ntype QTUnit = Circle<number>;\n\nconst rebuildQuadtree = (quadtree: Quadtree<QTUnit>, units: GameObjectData[]) => {\n    quadtree.clear();\n    units.forEach((unit) => {\n        quadtree.insert(new Circle<number>({ x: unit.tile.rx, y: unit.tile.ry, r: 1, data: unit.id }));\n    });\n};\n\nexport class MatchAwarenessImpl implements MatchAwareness {\n    private _shouldAttack: boolean = false;\n\n    private hostileQuadTree: Quadtree<QTUnit>;\n    private scoutingManager: ScoutingManager;\n    private sectorCache: SectorCache;\n    private buildSpaceCache: BuildSpaceCache;\n\n    private expansionCandidates: Vector2[] = [];\n\n    constructor(\n        gameApi: GameApi,\n        playerData: PlayerData,\n        private threatCache: GlobalThreat | null,\n        private mainRallyPoint: Vector2,\n        private logger: (message: string, sayInGame?: boolean) => void,\n    ) {\n        const mapSize = gameApi.mapApi.getRealMapSize();\n        const diagonalBounds = getDiagonalMapBounds(gameApi.mapApi);\n        this.hostileQuadTree = new Quadtree(mapSize);\n        this.scoutingManager = new ScoutingManager(logger);\n        this.sectorCache = new SectorCache(\n            mapSize, \n            diagonalBounds,\n            (x: number, y: number) => ({\n                id: getSectorId(x, y),\n                sectorVisibilityRatio: null,\n                threatLevel: null,\n                diffuseThreatLevel: null,\n                totalMoney: null,\n                connectedSectorsDirty: true,\n                connectedSectorIds: [],\n            }),\n            (startX, startY, size, currentValue, neighbours) => {\n                const sp = new Vector2(startX, startY);\n                const ep = new Vector2(sp.x + size, sp.y + size);\n                const visibility = calculateAreaVisibility(gameApi.mapApi, playerData, sp, ep);\n                const threatLevel = calculateSectorThreat(startX, startY, size, gameApi, playerData);\n                const diffuseThreatLevel = calculateDiffuseSectorThreat(currentValue, neighbours);\n                const totalMoney = calculateMoney(startX, startY, size, gameApi.mapApi);\n                const connectedSectorIds = currentValue.connectedSectorsDirty ? calculateConnectedSectorIds(gameApi.mapApi, startX, startY, neighbours) : currentValue.connectedSectorIds;\n                return {\n                    ...currentValue,\n                    sectorVisibilityRatio: visibility.validTiles > 0 ?\n                        visibility.visibleTiles / visibility.validTiles :\n                        null, \n                    threatLevel,\n                    diffuseThreatLevel,\n                    totalMoney,\n                    connectedSectorsDirty: false,\n                    connectedSectorIds\n                }\n            }\n        );\n        this.buildSpaceCache = new BuildSpaceCache(mapSize, gameApi, diagonalBounds);\n    }\n\n    getHostilesNearPoint2d(point: Vector2, radius: number): UnitPositionQuery[] {\n        return this.getHostilesNearPoint(point.x, point.y, radius);\n    }\n\n    getHostilesNearPoint(searchX: number, searchY: number, radius: number): UnitPositionQuery[] {\n        const intersections = this.hostileQuadTree.retrieve(new Circle({ x: searchX, y: searchY, r: radius }));\n        return intersections\n            .map(({ x, y, data: unitId }) => ({ x, y, unitId: unitId! }))\n            .filter(({ x, y }) => new Vector2(x, y).distanceTo(new Vector2(searchX, searchY)) <= radius)\n            .filter(({ unitId }) => !!unitId);\n    }\n\n    getThreatCache(): GlobalThreat | null {\n        return this.threatCache;\n    }\n    getSectorCache(): SectorCache {\n        return this.sectorCache;\n    }\n    getMainRallyPoint(): Vector2 {\n        return this.mainRallyPoint;\n    }\n    getScoutingManager(): ScoutingManager {\n        return this.scoutingManager;\n    }\n    getNextExpansionCandidates(): Vector2[] {\n        return this.expansionCandidates;\n    }\n    getBuildSpaceCache(): BuildSpaceCache {\n        return this.buildSpaceCache;\n    }\n\n    shouldAttack(): boolean {\n        return this._shouldAttack;\n    }\n\n    private checkShouldAttack(threatCache: GlobalThreat, threatFactor: number) {\n        let scaledGroundPower = threatCache.totalAvailableAntiGroundFirepower * 1.1;\n        let scaledGroundThreat =\n            (threatFactor * threatCache.totalOffensiveLandThreat + threatCache.totalDefensiveThreat) * 1.1;\n\n        let scaledAirPower = threatCache.totalAvailableAirPower * 1.1;\n        let scaledAirThreat =\n            (threatFactor * threatCache.totalOffensiveAntiAirThreat + threatCache.totalDefensiveThreat) * 1.1;\n\n        return scaledGroundPower > scaledGroundThreat || scaledAirPower > scaledAirThreat;\n    }\n\n    public onGameStart(gameApi: GameApi, playerData: PlayerData) {\n        this.scoutingManager.onGameStart(gameApi, playerData, this.sectorCache);\n    }\n\n    onAiUpdate({game, player}: BotContext): void {\n        const sectorCache = this.sectorCache;\n        const playerData = game.getPlayerData(player.name);\n\n        sectorCache.updateSectors(game.getCurrentTick(), SECTORS_TO_UPDATE_PER_CYCLE);\n        this.buildSpaceCache.update(game.getCurrentTick());\n\n        this.scoutingManager.onAiUpdate(game, playerData, sectorCache);\n\n        let updateRatio = sectorCache?.getSectorUpdateRatio(game.getCurrentTick() - game.getTickRate() * 60);\n        if (updateRatio && updateRatio < 1.0) {\n            this.logger(`${updateRatio * 100.0}% of sectors updated in last 60 seconds.`);\n        }\n\n        // Build the quadtree, if this is too slow we should consider doing this periodically.\n        const hostileUnitIds = game.getVisibleUnits(playerData.name, \"enemy\");\n        try {\n            const hostileUnits = hostileUnitIds\n                .map((id) => game.getGameObjectData(id))\n                .filter(\n                    (gameObjectData: GameObjectData | undefined): gameObjectData is GameObjectData =>\n                        gameObjectData !== undefined,\n                );\n\n            rebuildQuadtree(this.hostileQuadTree, hostileUnits);\n        } catch (err) {\n            // Hack. Will be fixed soon.\n            console.error(`caught error`, hostileUnitIds);\n        }\n\n        if (game.getCurrentTick() % THREAT_UPDATE_INTERVAL_TICKS == 0) {\n            let visibility = sectorCache?.getOverallVisibility();\n            if (visibility) {\n                this.logger(`${Math.round(visibility * 1000.0) / 10}% of tiles visible. Calculating threat.`);\n                // Update the global threat cache\n                this.threatCache = calculateGlobalThreat(game, playerData, visibility);\n\n                // As the game approaches 2 hours, be more willing to attack. (15 ticks per second)\n                const gameLengthFactor = Math.max(0, 1.0 - game.getCurrentTick() / (15 * 7200.0));\n                this.logger(`Game length multiplier: ${gameLengthFactor}`);\n\n                if (!this._shouldAttack) {\n                    // If not attacking, make it harder to switch to attack mode by multiplying the opponent's threat.\n                    this._shouldAttack = this.checkShouldAttack(this.threatCache, 1.25 * gameLengthFactor);\n                    if (this._shouldAttack) {\n                        this.logger(`Globally switched to attack mode.`);\n                    }\n                } else {\n                    // If currently attacking, make it harder to switch to defence mode my dampening the opponent's threat.\n                    this._shouldAttack = this.checkShouldAttack(this.threatCache, 0.75 * gameLengthFactor);\n                    if (!this._shouldAttack) {\n                        this.logger(`Globally switched to defence mode.`);\n                    }\n                }\n            }\n        }\n\n        // Update rally point every few ticks.\n        if (game.getCurrentTick() % RALLY_POINT_UPDATE_INTERVAL_TICKS === 0) {\n            const enemyPlayers = game\n                .getPlayers()\n                .filter((p) => p !== playerData.name && !game.areAlliedPlayers(playerData.name, p));\n            const enemy = game.getPlayerData(enemyPlayers[0]);\n            this.mainRallyPoint = getPointTowardsOtherPoint(\n                game,\n                playerData.startLocation,\n                enemy.startLocation,\n                10,\n                10,\n                0,\n            );\n        }\n\n        // Decide to expand or not\n        if (this.buildSpaceCache.isFinished() && game.getCurrentTick() % EXPANSION_UPDATE_INTERVAL_TICKS === 0) {\n            // don't expand somewhere near where we can already build\n            const ownBuildingVectors = game\n                .getVisibleUnits(playerData.name, \"self\", (r) => r.baseNormal)\n                .map((id) => game.getGameObjectData(id)).filter((o): o is GameObjectData => !!o)\n                .map((r) => new Vector2(r.tile.rx, r.tile.ry));\n            const rawCandidates = this.buildSpaceCache.findSpace(EXPANSION_MIN_CLEAR_SPACE_TILES);\n            this.expansionCandidates = rawCandidates.filter((candidate) => {\n                const cell = this.sectorCache.getCell(candidate.x, candidate.y);\n                if (!cell) {\n                    return false;\n                }\n                if (cell.value.totalMoney && cell.value.totalMoney < EXPANSION_MIN_MONEY) {\n                    return false;\n                }\n                if (ownBuildingVectors.some((ref) => ref.distanceTo(candidate) < EXPANSION_MIN_DISTANCE_TO_BUILDABLE)) {\n                    return false;\n                }\n                if (ownBuildingVectors.some((ref) => ref.distanceTo(candidate) < EXPANSION_MIN_DISTANCE_TO_BUILDABLE)) {\n                    return false;\n                }\n                const tile = game.mapApi.getTile(candidate.x, candidate.y);\n                if (!tile) {\n                    return false;\n                }\n                return true;\n            });\n        }\n    }\n\n    public getGlobalDebugText(): string | undefined {\n        if (!this.threatCache) {\n            return undefined;\n        }\n        return (\n            `Threat LAND: Them ${Math.round(this.threatCache.totalOffensiveLandThreat)}, us: ${Math.round(\n                this.threatCache.totalAvailableAntiGroundFirepower,\n            )}.\\n` +\n            `Threat DEFENSIVE: Them ${Math.round(this.threatCache.totalDefensiveThreat)}, us: ${Math.round(\n                this.threatCache.totalDefensivePower,\n            )}.\\n` +\n            `Threat AIR: Them ${Math.round(this.threatCache.totalOffensiveAirThreat)}, us: ${Math.round(\n                this.threatCache.totalAvailableAntiAirFirepower,\n            )}.`\n        );\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/antiAirStaticDefence.ts",
    "content": "import { GameApi, PlayerData, TechnoRules, Vector2 } from \"../../../game-api\";\nimport { getPointTowardsOtherPoint } from \"../map/map\";\nimport { GlobalThreat } from \"../threat/threat\";\nimport { AiBuildingRules, getDefaultPlacementLocation, numBuildingsOwnedOfType } from \"./buildingRules\";\n\nexport class AntiAirStaticDefence implements AiBuildingRules {\n    constructor(\n        private basePriority: number,\n        private baseAmount: number,\n        private airStrength: number,\n    ) {}\n\n    getPlacementLocation(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n    ): { rx: number; ry: number } | undefined {\n        // Prefer front towards enemy.\n        let startLocation = playerData.startLocation;\n        let players = game.getPlayers();\n        let enemyFacingLocationCandidates: Vector2[] = [];\n        for (let i = 0; i < players.length; ++i) {\n            let playerName = players[i];\n            if (playerName == playerData.name) {\n                continue;\n            }\n            let enemyPlayer = game.getPlayerData(playerName);\n            enemyFacingLocationCandidates.push(\n                getPointTowardsOtherPoint(game, startLocation, enemyPlayer.startLocation, 4, 16, 1.5),\n            );\n        }\n        let selectedLocation =\n            enemyFacingLocationCandidates[Math.floor(game.generateRandom() * enemyFacingLocationCandidates.length)];\n        return getDefaultPlacementLocation(game, playerData, selectedLocation, technoRules, false, 0);\n    }\n\n    getPriority(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number {\n        if (threatCache) {\n            let denominator = threatCache.totalAvailableAntiAirFirepower + this.airStrength;\n            if (threatCache.totalOffensiveAirThreat > denominator * 1.1) {\n                return this.basePriority * (threatCache.totalOffensiveAirThreat / Math.max(1, denominator));\n            } else {\n                return 0;\n            }\n        }\n        const strengthPerCost = (this.airStrength / technoRules.cost) * 1000;\n        const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);\n        return this.basePriority * (1.0 - numOwned / this.baseAmount) * strengthPerCost;\n    }\n\n    getMaxCount(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number | null {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/antiGroundStaticDefence.ts",
    "content": "import { GameApi, PlayerData, TechnoRules, Vector2 } from \"../../../game-api\";\nimport { getPointTowardsOtherPoint } from \"../map/map\";\nimport { GlobalThreat } from \"../threat/threat\";\nimport { AiBuildingRules, getDefaultPlacementLocation, numBuildingsOwnedOfType } from \"./buildingRules\";\nimport { getStaticDefencePlacement } from \"./common\";\n\nexport class AntiGroundStaticDefence implements AiBuildingRules {\n    constructor(\n        private basePriority: number,\n        private baseAmount: number,\n        private groundStrength: number,\n        private limit: number,\n    ) {}\n\n    getPlacementLocation(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n    ): { rx: number; ry: number } | undefined {\n        return getStaticDefencePlacement(game, playerData, technoRules);\n    }\n\n    getPriority(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number {\n        const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);\n        if (numOwned >= this.limit) {\n            return 0;\n        }\n        // If the enemy's ground power is increasing we should try to keep up.\n        if (threatCache) {\n            let denominator =\n                threatCache.totalAvailableAntiGroundFirepower + threatCache.totalDefensivePower + this.groundStrength;\n            if (threatCache.totalOffensiveLandThreat > denominator * 1.1) {\n                return this.basePriority * (threatCache.totalOffensiveLandThreat / Math.max(1, denominator));\n            } else {\n                return 0;\n            }\n        }\n        const strengthPerCost = (this.groundStrength / technoRules.cost) * 1000;\n        return this.basePriority * (1.0 - numOwned / this.baseAmount) * strengthPerCost;\n    }\n\n    getMaxCount(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number | null {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/artilleryUnit.ts",
    "content": "import { GameApi, GameMath, PlayerData, TechnoRules } from \"../../../game-api\";\nimport { GlobalThreat } from \"../threat/threat\";\nimport { AiBuildingRules, numBuildingsOwnedOfType } from \"./buildingRules\";\n\nexport class ArtilleryUnit implements AiBuildingRules {\n    constructor(\n        private basePriority: number,\n        private artilleryPower: number,\n        private antiGroundPower: number,\n        private baseAmount: number,\n    ) {}\n\n    getPlacementLocation(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n    ): { rx: number; ry: number } | undefined {\n        return undefined;\n    }\n\n    getPriority(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number {\n        // Units aren't built automatically, but are instead requested by missions.\n        return 0;\n    }\n\n    getMaxCount(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number | null {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/basicAirUnit.ts",
    "content": "import { GameApi, GameMath, PlayerData, TechnoRules } from \"../../../game-api\";\nimport { GlobalThreat } from \"../threat/threat\";\nimport { AiBuildingRules, getDefaultPlacementLocation, numBuildingsOwnedOfType } from \"./buildingRules\";\n\nexport class BasicAirUnit implements AiBuildingRules {\n    constructor(\n        private basePriority: number,\n        private baseAmount: number,\n        private antiGroundPower: number = 1, // boolean for now, but will eventually be used in weighting.\n        private antiAirPower: number = 0,\n    ) {}\n\n    getPlacementLocation(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n    ): { rx: number; ry: number } | undefined {\n        return undefined;\n    }\n\n    getPriority(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number {\n        // Units aren't built automatically, but are instead requested by missions.\n        return 0;\n    }\n\n    getMaxCount(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number | null {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/basicBuilding.ts",
    "content": "import { GameApi, PlayerData, TechnoRules, Tile, Vector2 } from \"../../../game-api\";\nimport { AiBuildingRules, getDefaultPlacementLocation, numBuildingsOwnedOfType } from \"./buildingRules\";\nimport { GlobalThreat } from \"../threat/threat\";\n\nexport class BasicBuilding implements AiBuildingRules {\n    constructor(\n        protected basePriority: number,\n        protected maxNeeded: number,\n        protected onlyBuildWhenFloatingCreditsAmount?: number,\n    ) {}\n\n    getPlacementLocation(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n    ): { rx: number; ry: number } | undefined {\n        // Prefer spawning close to conyard\n        const conyardVectors = game\n            .getVisibleUnits(playerData.name, \"self\", (r) => r.constructionYard)\n            .map((r) => game.getGameObjectData(r)?.tile)\n            .filter((t): t is Tile => !!t)\n            .map((t) => new Vector2(t.rx, t.ry));\n\n        if (conyardVectors.length === 0) {\n            return undefined;\n        }\n        return getDefaultPlacementLocation(game, playerData, conyardVectors[0], technoRules, false, 2);\n    }\n\n    getPriority(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number {\n        const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);\n        const calcMaxCount = this.getMaxCount(game, playerData, technoRules, threatCache);\n        const max = calcMaxCount ?? this.maxNeeded;\n        if (numOwned >= max) {\n            return -100;\n        }\n\n        const priority = this.basePriority * (1.0 - numOwned / max);\n\n        if (this.onlyBuildWhenFloatingCreditsAmount && playerData.credits < this.onlyBuildWhenFloatingCreditsAmount) {\n            return priority * (playerData.credits / this.onlyBuildWhenFloatingCreditsAmount);\n        }\n\n        return priority;\n    }\n\n    getMaxCount(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number | null {\n        return this.maxNeeded;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/basicGroundUnit.ts",
    "content": "import { GameApi, GameMath, PlayerData, TechnoRules } from \"../../../game-api\";\nimport { GlobalThreat } from \"../threat/threat\";\nimport { AiBuildingRules, getDefaultPlacementLocation, numBuildingsOwnedOfType } from \"./buildingRules\";\n\nexport class BasicGroundUnit implements AiBuildingRules {\n    constructor(\n        protected basePriority: number,\n        protected baseAmount: number,\n        protected antiGroundPower: number = 1, // boolean for now, but will eventually be used in weighting.\n        protected antiAirPower: number = 0,\n    ) {}\n\n    getPlacementLocation(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n    ): { rx: number; ry: number } | undefined {\n        return undefined;\n    }\n\n    getPriority(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number {\n        // Units aren't built automatically, but are instead requested by missions.\n        return 0;\n    }\n\n    getMaxCount(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number | null {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/buildingRules.ts",
    "content": "import {\n    BuildingPlacementData,\n    GameApi,\n    GameMath,\n    LandType,\n    ObjectType,\n    PlayerData,\n    Rectangle,\n    Size,\n    TechnoRules,\n    Tile,\n    Vector2,\n} from \"../../../game-api\";\nimport { GlobalThreat } from \"../threat/threat\";\nimport { AntiGroundStaticDefence } from \"./antiGroundStaticDefence\";\nimport { ArtilleryUnit } from \"./artilleryUnit\";\nimport { BasicAirUnit } from \"./basicAirUnit\";\nimport { BasicBuilding } from \"./basicBuilding\";\nimport { BasicGroundUnit } from \"./basicGroundUnit\";\nimport { PowerPlant } from \"./powerPlant\";\nimport { ResourceCollectionBuilding } from \"./resourceCollectionBuilding\";\nimport { Harvester } from \"./harvester\";\nimport { uniqBy } from \"../common/utils\";\nimport { AntiAirStaticDefence } from \"./antiAirStaticDefence\";\nimport { computeAdjacentRect, getAdjacentTiles } from \"../common/tileUtils\";\n\nexport interface AiBuildingRules {\n    getPriority(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number;\n\n    getPlacementLocation(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n    ): { rx: number; ry: number } | undefined;\n\n    getMaxCount(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number | null;\n}\n\nexport function numBuildingsOwnedOfType(game: GameApi, playerData: PlayerData, technoRules: TechnoRules): number {\n    return game.getVisibleUnits(playerData.name, \"self\", (r) => r == technoRules).length;\n}\n\nexport function numBuildingsOwnedOfName(game: GameApi, playerData: PlayerData, name: string): number {\n    return game.getVisibleUnits(playerData.name, \"self\", (r) => r.name === name).length;\n}\n\nexport function getAdjacencyTiles(\n    game: GameApi,\n    playerData: PlayerData,\n    technoRules: TechnoRules,\n    onWater: boolean,\n    minimumSpace: number,\n): Tile[] {\n    const placementRules = game.getBuildingPlacementData(technoRules.name);\n    const { width: newBuildingWidth, height: newBuildingHeight } = placementRules.foundation;\n    const tiles = [];\n    const buildings = game.getVisibleUnits(playerData.name, \"self\", (r: TechnoRules) => r.type === ObjectType.Building);\n    const removedTiles = new Set<string>();\n    for (let buildingId of buildings) {\n        const building = game.getUnitData(buildingId);\n        if (!building?.rules?.baseNormal) {\n            // This building is not considered for adjacency checks.\n            continue;\n        }\n        const { foundation, tile } = building;\n        const buildingBase = new Vector2(tile.rx, tile.ry);\n        const buildingSize = {\n            width: foundation?.width,\n            height: foundation?.height,\n        };\n        const range = computeAdjacentRect(buildingBase, buildingSize, technoRules.adjacent, placementRules.foundation);\n        const adjacentTiles = getAdjacentTiles(game, range, onWater);\n        if (adjacentTiles.length === 0) {\n            continue;\n        }\n        tiles.push(...adjacentTiles);\n\n        // Prevent placing the new building on tiles that would cause it to overlap with this building.\n        const modifiedBase = new Vector2(\n            buildingBase.x - (newBuildingWidth - 1),\n            buildingBase.y - (newBuildingHeight - 1),\n        );\n        const modifiedSize = {\n            width: buildingSize.width + (newBuildingWidth - 1),\n            height: buildingSize.height + (newBuildingHeight - 1),\n        };\n        const blockedRect = computeAdjacentRect(modifiedBase, modifiedSize, minimumSpace);\n        const buildingTiles = adjacentTiles.filter((tile) => {\n            return (\n                tile.rx >= blockedRect.x &&\n                tile.rx < blockedRect.x + blockedRect.width &&\n                tile.ry >= blockedRect.y &&\n                tile.ry < blockedRect.y + blockedRect.height\n            );\n        });\n        buildingTiles.forEach((buildingTile) => removedTiles.add(buildingTile.id));\n    }\n    // Remove duplicate tiles.\n    const withDuplicatesRemoved = uniqBy(tiles, (tile) => tile.id);\n    // Remove tiles containing buildings and potentially area around them removed as well.\n    return withDuplicatesRemoved.filter((tile) => !removedTiles.has(tile.id));\n}\n\nfunction getTileDistances(startPoint: Vector2, tiles: Tile[]) {\n    return tiles\n        .map((tile) => ({\n            tile,\n            distance: distance(tile.rx, tile.ry, startPoint.x, startPoint.y),\n        }))\n        .sort((a, b) => {\n            return a.distance - b.distance;\n        });\n}\n\nfunction distance(x1: number, y1: number, x2: number, y2: number) {\n    var dx = x1 - x2;\n    var dy = y1 - y2;\n    let tmp = dx * dx + dy * dy;\n    if (0 === tmp) {\n        return 0;\n    }\n    return GameMath.sqrt(tmp);\n}\n\nexport function getDefaultPlacementLocation(\n    game: GameApi,\n    playerData: PlayerData,\n    idealPoint: Vector2,\n    technoRules: TechnoRules,\n    onWater: boolean = false,\n    minSpace: number = 2,\n): { rx: number; ry: number } | undefined {\n    // Closest possible location near `startPoint`.\n    const size: BuildingPlacementData = game.getBuildingPlacementData(technoRules.name) as any;\n    if (!size) {\n        return undefined;\n    }\n    const tiles = getAdjacencyTiles(game, playerData, technoRules, onWater, minSpace);\n\n    // Score tiles: prefer close to ideal point but penalize crowding near many buildings.\n    // This encourages a more spread-out base layout with room for unit movement.\n    const buildings = game.getVisibleUnits(playerData.name, \"self\", (r: TechnoRules) => r.type === ObjectType.Building) as any;\n    const buildingPositions: Vector2[] = [];\n    for (const bid of buildings) {\n        const bd = game.getGameObjectData(bid);\n        if (bd?.tile) buildingPositions.push(new Vector2(bd.tile.rx, bd.tile.ry));\n    }\n\n    const scored = tiles.map((tile) => {\n        const distToIdeal = distance(tile.rx, tile.ry, idealPoint.x, idealPoint.y);\n        // Count nearby buildings within 3 tiles — more neighbors = higher crowding penalty\n        let crowding = 0;\n        for (const bp of buildingPositions) {\n            const d = distance(tile.rx, tile.ry, bp.x, bp.y);\n            if (d < 4) crowding += (4 - d);\n        }\n        // Combined score: distance matters most, but crowding adds a penalty\n        const score = distToIdeal + crowding * 0.8;\n        return { tile, score };\n    }).sort((a, b) => a.score - b.score);\n\n    for (const entry of scored) {\n        if (entry.tile && game.canPlaceBuilding(playerData.name, technoRules.name, entry.tile)) {\n            return entry.tile;\n        }\n    }\n    return undefined;\n}\n\n// Priority 0 = don't build.\nexport type TechnoRulesWithPriority = { unit: TechnoRules; priority: number };\n\nexport const DEFAULT_BUILDING_PRIORITY = 0;\n\nexport const BUILDING_NAME_TO_RULES = new Map<string, AiBuildingRules>([\n    // Allied\n    [\"GAPOWR\", new PowerPlant()],\n    [\"GAREFN\", new ResourceCollectionBuilding(10, 3)], // Refinery\n    [\"GAWEAP\", new BasicBuilding(15, 3)], // War Factory\n    [\"GAPILE\", new BasicBuilding(12, 1)], // Barracks\n    [\"CMIN\", new Harvester(15, 4, 2)], // Chrono Miner\n    [\"GADEPT\", new BasicBuilding(1, 1, 10000)], // Repair Depot\n    [\"GAAIRC\", new BasicBuilding(10, 1, 500)], // Airforce Command\n    [\"AMRADR\", new BasicBuilding(10, 1, 500)], // Airforce Command (USA)\n\n    [\"GATECH\", new BasicBuilding(20, 1, 4000)], // Allied Battle Lab\n    [\"GAYARD\", new BasicBuilding(0, 0, 0)], // Naval Yard, disabled\n\n    [\"GAPILL\", new AntiGroundStaticDefence(2, 1, 7.5, 5)], // Pillbox\n    [\"ATESLA\", new AntiGroundStaticDefence(2, 1, 10, 3)], // Prism Cannon\n    [\"NASAM\", new AntiAirStaticDefence(1, 1, 7.5)], // Patriot Missile\n    [\"GAWALL\", new AntiGroundStaticDefence(0, 0, 0, 0)], // Walls\n\n    [\"E1\", new BasicGroundUnit(2, 2, 0.2, 0)], // GI\n    [\"ENGINEER\", new BasicGroundUnit(1, 0, 0)], // Engineer\n    [\"MTNK\", new BasicGroundUnit(10, 3, 2, 0)], // Grizzly Tank\n    [\"MGTK\", new BasicGroundUnit(10, 1, 2.5, 0)], // Mirage Tank\n    [\"FV\", new BasicGroundUnit(5, 2, 0.5, 1)], // IFV\n    [\"JUMPJET\", new BasicAirUnit(10, 1, 1, 1)], // Rocketeer\n    [\"ORCA\", new BasicAirUnit(7, 1, 2, 0)], // Rocketeer\n    [\"SREF\", new ArtilleryUnit(10, 5, 3, 3)], // Prism Tank\n    [\"CLEG\", new BasicGroundUnit(0, 0)], // Chrono Legionnaire (Disabled - we don't handle the warped out phase properly and it tends to bug both bots out)\n    [\"SHAD\", new BasicGroundUnit(0, 0)], // Nighthawk (Disabled)\n\n    // Soviet\n    [\"NAPOWR\", new PowerPlant()],\n    [\"NAREFN\", new ResourceCollectionBuilding(10, 3)], // Refinery\n    [\"NAWEAP\", new BasicBuilding(15, 3)], // War Factory\n    [\"NAHAND\", new BasicBuilding(12, 1)], // Barracks\n    [\"HARV\", new Harvester(15, 4, 2)], // War Miner\n    [\"NADEPT\", new BasicBuilding(1, 1, 10000)], // Repair Depot\n    [\"NARADR\", new BasicBuilding(10, 1, 500)], // Radar\n    [\"NANRCT\", new PowerPlant()], // Nuclear Reactor\n    [\"NAYARD\", new BasicBuilding(0, 0, 0)], // Naval Yard, disabled\n\n    [\"NATECH\", new BasicBuilding(20, 1, 4000)], // Soviet Battle Lab\n\n    [\"NALASR\", new AntiGroundStaticDefence(2, 1, 7.5, 5)], // Sentry Gun\n    [\"NAFLAK\", new AntiAirStaticDefence(1, 1, 7.5)], // Flak Cannon\n    [\"TESLA\", new AntiGroundStaticDefence(2, 1, 10, 3)], // Tesla Coil\n    [\"NAWALL\", new AntiGroundStaticDefence(0, 0, 0, 0)], // Walls\n\n    [\"E2\", new BasicGroundUnit(2, 2, 0.2, 0)], // Conscript\n    [\"SENGINEER\", new BasicGroundUnit(1, 0, 0)], // Soviet Engineer\n    [\"FLAKT\", new BasicGroundUnit(2, 2, 0.1, 0.3)], // Flak Trooper\n    [\"YURI\", new BasicGroundUnit(1, 1, 1, 0)], // Yuri\n    [\"DOG\", new BasicGroundUnit(1, 1, 0, 0)], // Soviet Attack Dog\n    [\"HTNK\", new BasicGroundUnit(10, 3, 3, 0)], // Rhino Tank\n    [\"APOC\", new BasicGroundUnit(6, 1, 5, 0)], // Apocalypse Tank\n    [\"HTK\", new BasicGroundUnit(5, 2, 0.33, 1.5)], // Flak Track\n    [\"ZEP\", new BasicAirUnit(5, 1, 5, 1)], // Kirov\n    [\"V3\", new ArtilleryUnit(9, 10, 0, 3)], // V3 Rocket Launcher\n]);\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/common.ts",
    "content": "import { GameApi, PlayerData, TechnoRules, Vector2 } from \"../../../game-api\";\nimport { getPointTowardsOtherPoint } from \"../map/map\";\nimport { getDefaultPlacementLocation } from \"./buildingRules\";\n\nexport const getStaticDefencePlacement = (game: GameApi, playerData: PlayerData, technoRules: TechnoRules) => {\n    // Prefer front towards enemy.\n    const { startLocation, name: currentName } = playerData;\n    const allNames = game.getPlayers();\n    // Create a list of positions that point roughly towards hostile player start locatoins.\n    const candidates = allNames\n        .filter((otherName) => otherName !== currentName && !game.areAlliedPlayers(otherName, currentName))\n        .map((otherName) => {\n            const enemyPlayer = game.getPlayerData(otherName);\n            return getPointTowardsOtherPoint(game, startLocation, enemyPlayer.startLocation, 4, 16, 1.5);\n        });\n    if (candidates.length === 0) {\n        return undefined;\n    }\n    const selectedLocation = candidates[Math.floor(game.generateRandom() * candidates.length)];\n    return getDefaultPlacementLocation(game, playerData, selectedLocation, technoRules, false, 2);\n};\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/harvester.ts",
    "content": "import { GameApi, PlayerData, TechnoRules } from \"../../../game-api\";\nimport { GlobalThreat } from \"../threat/threat\";\nimport { BasicGroundUnit } from \"./basicGroundUnit\";\n\nconst IDEAL_HARVESTERS_PER_REFINERY = 2;\nconst MAX_HARVESTERS_PER_REFINERY = 4;\n\n// because refineries also scales based on harvesters, we need a cap\nconst MAX_HARVESTERS_TOTAL = 10;\n\nexport class Harvester extends BasicGroundUnit {\n    constructor(\n        basePriority: number,\n        baseAmount: number,\n        private minNeeded: number,\n    ) {\n        super(basePriority, baseAmount, 0, 0);\n    }\n\n    // Priority goes up when we have fewer than this many refineries.\n    getPriority(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number {\n        const refineries = game.getVisibleUnits(playerData.name, \"self\", (r) => r.refinery).length;\n        const harvesters = game.getVisibleUnits(playerData.name, \"self\", (r) => r.harvester).length;\n\n        const boost = harvesters < this.minNeeded ? 3 : harvesters > refineries * MAX_HARVESTERS_PER_REFINERY ? 0 : 1;\n\n        return this.basePriority * (refineries / Math.max(harvesters / IDEAL_HARVESTERS_PER_REFINERY, 1)) * boost;\n    }\n\n    getMaxCount(game: GameApi, playerData: PlayerData, technoRules: TechnoRules, threatCache: GlobalThreat | null): number | null {\n        return MAX_HARVESTERS_TOTAL;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/powerPlant.ts",
    "content": "import { GameApi, PlayerData, TechnoRules } from \"../../../game-api\";\nimport { AiBuildingRules, getDefaultPlacementLocation } from \"./buildingRules\";\nimport { GlobalThreat } from \"../threat/threat\";\n\nexport class PowerPlant implements AiBuildingRules {\n    getPlacementLocation(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules\n    ): { rx: number; ry: number } | undefined {\n        return getDefaultPlacementLocation(game, playerData, playerData.startLocation, technoRules, false, 2);\n    }\n\n    getPriority(game: GameApi, playerData: PlayerData, technoRules: TechnoRules): number {\n        if (playerData.power.total < playerData.power.drain) {\n            return 100;\n        } else if (playerData.power.total < playerData.power.drain + technoRules.power / 2) {\n            return 20;\n        } else {\n            return 0;\n        }\n    }\n\n    getMaxCount(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null\n    ): number | null {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/queueController.ts",
    "content": "import {\n    ActionsApi,\n    GameApi,\n    PlayerData,\n    ProductionApi,\n    QueueStatus,\n    QueueType,\n    TechnoRules,\n    Vector2,\n} from \"../../../game-api\";\nimport { GlobalThreat } from \"../threat/threat\";\nimport { TechnoRulesWithPriority, getDefaultPlacementLocation } from \"./buildingRules\";\nimport { SupabotContext } from \"../common/context\";\nimport { UnitRequest } from \"../mission/missionController\";\n\nexport const QUEUES = [\n    QueueType.Structures,\n    QueueType.Armory,\n    QueueType.Infantry,\n    QueueType.Vehicles,\n    QueueType.Aircrafts,\n    QueueType.Ships,\n];\n\nfunction isBuildingQueue(queueType: QueueType): boolean {\n    return queueType === QueueType.Structures || queueType === QueueType.Armory;\n}\n\nexport const queueTypeToName = (queue: QueueType) => {\n    switch (queue) {\n        case QueueType.Structures:\n            return \"Structures\";\n        case QueueType.Armory:\n            return \"Armory\";\n        case QueueType.Infantry:\n            return \"Infantry\";\n        case QueueType.Vehicles:\n            return \"Vehicles\";\n        case QueueType.Aircrafts:\n            return \"Aircrafts\";\n        case QueueType.Ships:\n            return \"Ships\";\n        default:\n            return \"Unknown\";\n    }\n};\n\ntype QueueState = {\n    queue: QueueType;\n    /** sorted in ascending order (last item is the topItem) */\n    items: TechnoRulesWithPriority[];\n    topItem: TechnoRulesWithPriority | undefined;\n};\n\nconst REPAIR_CHECK_INTERVAL = 30;\nconst PLACEMENT_FAILURE_RETRY_THRESHOLD = 3;\nconst PLACEMENT_FAILURE_CANCEL_THRESHOLD = 10;\n\nexport class QueueController {\n    private queueStates: QueueState[] = [];\n    private lastRepairCheckAt = 0;\n    private placementFailures: Map<string, number> = new Map();\n\n    constructor() {}\n\n    public onAiUpdate(\n        context: SupabotContext,\n        threatCache: GlobalThreat | null,\n        unitTypeRequests: Map<string, UnitRequest>,\n        logger: (message: string) => void,\n    ) {\n        const { game, player } = context;\n        const { production: productionApi, actions: actionsApi } = player;\n        const playerData = game.getPlayerData(player.name);\n        this.queueStates = QUEUES.map((queueType) => {\n            const options = productionApi.getAvailableObjects(queueType);\n            const items = QueueController.getPrioritiesForBuildingOptions(options, unitTypeRequests);\n            const topItem = items.length > 0 ? items[items.length - 1] : undefined;\n            return {\n                queue: queueType,\n                items,\n                // only if the top item has a  priority above zero\n                topItem: topItem && topItem.priority > 0 ? topItem : undefined,\n            };\n        });\n        const totalWeightAcrossQueues = this.queueStates\n            .map((decision) => decision.topItem?.priority!)\n            .reduce((pV, cV) => pV + cV, 0);\n        const totalCostAcrossQueues = this.queueStates\n            .map((decision) => decision.topItem?.unit.cost!)\n            .reduce((pV, cV) => pV + cV, 0);\n\n        this.queueStates.forEach((decision) => {\n            this.updateBuildQueue(\n                game,\n                productionApi,\n                actionsApi,\n                playerData,\n                threatCache,\n                unitTypeRequests,\n                decision.queue,\n                decision.topItem,\n                totalWeightAcrossQueues,\n                totalCostAcrossQueues,\n                logger,\n            );\n        });\n\n        // Repair is simple - just repair everything that's damaged.\n        if (playerData.credits > 0 && game.getCurrentTick() > this.lastRepairCheckAt + REPAIR_CHECK_INTERVAL) {\n            game.getVisibleUnits(playerData.name, \"self\", (r) => r.repairable).forEach((unitId) => {\n                const unit = game.getUnitData(unitId);\n                if (!unit || !unit.hitPoints || !unit.maxHitPoints || unit.hasWrenchRepair) {\n                    return;\n                }\n                if (unit.hitPoints < unit.maxHitPoints) {\n                    actionsApi.toggleRepairWrench(unitId);\n                }\n            });\n            this.lastRepairCheckAt = game.getCurrentTick();\n        }\n    }\n\n    private updateBuildQueue(\n        game: GameApi,\n        productionApi: ProductionApi,\n        actionsApi: ActionsApi,\n        playerData: PlayerData,\n        threatCache: GlobalThreat | null,\n        unitTypeRequests: Map<string, UnitRequest>,\n        queueType: QueueType,\n        decision: TechnoRulesWithPriority | undefined,\n        totalWeightAcrossQueues: number,\n        totalCostAcrossQueues: number,\n        logger: (message: string) => void,\n    ): void {\n        const myCredits = playerData.credits;\n\n        const queueData = productionApi.getQueueData(queueType);\n        if (queueData.status == QueueStatus.Idle) {\n            // Start building the decided item.\n            if (decision !== undefined) {\n                logger(`Decision (${queueTypeToName(queueType)}): ${decision.unit.name}`);\n                actionsApi.queueForProduction(queueType, decision.unit.name, decision.unit.type, 1);\n            }\n        } else if (queueData.status == QueueStatus.Ready && queueData.items.length > 0) {\n            if (isBuildingQueue(queueType)) {\n                const readyUnit = queueData.items[0].rules;\n                const currentRequest = unitTypeRequests.get(readyUnit.name);\n                if (!currentRequest) {\n                    // No one is requesting this anymore, cancel\n                    logger(`Cancelling ready ${readyUnit.name} because no one is requesting anymore`);\n                    actionsApi.unqueueFromProduction(queueType, readyUnit.name, readyUnit.type, 1);\n                    this.placementFailures.delete(readyUnit.name);\n                    return;\n                }\n                if (!currentRequest.specificLocation) {\n                    // No one is requesting this anymore, cancel\n                    logger(`Cancelling ready ${readyUnit.name} because location is unspecified`);\n                    actionsApi.unqueueFromProduction(queueType, readyUnit.name, readyUnit.type, 1);\n                    this.placementFailures.delete(readyUnit.name);\n                    return;\n                }\n\n                const failures = this.placementFailures.get(readyUnit.name) ?? 0;\n\n                // If too many failures, cancel the building to unblock the queue\n                if (failures >= PLACEMENT_FAILURE_CANCEL_THRESHOLD) {\n                    logger(`Cancelling ready ${readyUnit.name} after ${failures} placement failures`);\n                    actionsApi.unqueueFromProduction(queueType, readyUnit.name, readyUnit.type, 1);\n                    this.placementFailures.delete(readyUnit.name);\n                    return;\n                }\n\n                let placeX = currentRequest.specificLocation.x;\n                let placeY = currentRequest.specificLocation.y;\n\n                // Check if the suggested location is valid\n                const canPlace = game.canPlaceBuilding(playerData.name, readyUnit.name, { rx: placeX, ry: placeY });\n\n                if (!canPlace) {\n                    this.placementFailures.set(readyUnit.name, failures + 1);\n\n                    // After threshold, try to find an alternative placement location\n                    if (failures >= PLACEMENT_FAILURE_RETRY_THRESHOLD) {\n                        const conYards = game.getVisibleUnits(playerData.name, \"self\", (r: TechnoRules) => r.constructionYard);\n                        if (conYards.length > 0) {\n                            const conYardData = game.getUnitData(conYards[0]);\n                            if (conYardData?.tile) {\n                                const altLocation = getDefaultPlacementLocation(\n                                    game,\n                                    playerData,\n                                    new Vector2(conYardData.tile.rx, conYardData.tile.ry),\n                                    readyUnit,\n                                );\n                                if (altLocation) {\n                                    logger(`Retrying ${readyUnit.name} at alternative location (${altLocation.rx},${altLocation.ry}) after ${failures} failures`);\n                                    actionsApi.placeBuilding(readyUnit.name, altLocation.rx, altLocation.ry);\n                                    this.placementFailures.delete(readyUnit.name);\n                                    return;\n                                }\n                            }\n                        }\n                        logger(`Cannot find alternative location for ${readyUnit.name} (failure #${failures + 1})`);\n                    }\n                    return;\n                }\n\n                // Location is valid, place the building\n                actionsApi.placeBuilding(readyUnit.name, placeX, placeY);\n                this.placementFailures.delete(readyUnit.name);\n            }\n        } else if (queueData.status == QueueStatus.Active && queueData.items.length > 0 && decision != null) {\n            // Consider cancelling if something else is significantly higher priority than what is currently being produced.\n\n            const currentProduction = queueData.items[0].rules;\n            if (decision.unit != currentProduction) {\n                // Changing our mind.\n                const currentRequest = unitTypeRequests.get(currentProduction.name);\n                const currentItemPriority = currentRequest ? currentRequest.priority : 0;\n                const newItemPriority = decision.priority;\n                if (newItemPriority > currentItemPriority * 2) {\n                    logger(\n                        `Dequeueing queue ${queueTypeToName(queueData.type)} unit ${currentProduction.name} because ${\n                            decision.unit.name\n                        } has 2x higher priority.`,\n                    );\n                    actionsApi.unqueueFromProduction(queueData.type, currentProduction.name, currentProduction.type, 1);\n                }\n            } else {\n                // Not changing our mind, but maybe other queues are more important for now.\n                if (totalCostAcrossQueues > myCredits && decision.priority < totalWeightAcrossQueues * 0.25) {\n                    logger(\n                        `Pausing queue ${queueTypeToName(queueData.type)} because weight is low (${\n                            decision.priority\n                        }/${totalWeightAcrossQueues})`,\n                    );\n                    actionsApi.pauseProduction(queueData.type);\n                }\n            }\n        } else if (queueData.status == QueueStatus.OnHold) {\n            // Consider resuming queue if priority is high relative to other queues.\n            if (myCredits >= totalCostAcrossQueues) {\n                logger(`Resuming queue ${queueTypeToName(queueData.type)} because credits are high`);\n                actionsApi.resumeProduction(queueData.type);\n            } else if (decision && decision.priority >= totalWeightAcrossQueues * 0.25) {\n                logger(\n                    `Resuming queue ${queueTypeToName(queueData.type)} because weight is high (${\n                        decision.priority\n                    }/${totalWeightAcrossQueues})`,\n                );\n                actionsApi.resumeProduction(queueData.type);\n            }\n        }\n    }\n\n    private static getPrioritiesForBuildingOptions(\n        options: TechnoRules[],\n        unitTypeRequests: Map<string, UnitRequest>,\n    ): TechnoRulesWithPriority[] {\n        let priorityQueue: TechnoRulesWithPriority[] = [];\n        options.forEach((option) => {\n            const priority = unitTypeRequests.get(option.name)?.priority ?? 0;\n            if (priority > 0) {\n                priorityQueue.push({ unit: option, priority });\n            }\n        });\n\n        priorityQueue = priorityQueue.sort((a, b) => a.priority - b.priority);\n        return priorityQueue;\n    }\n\n    public getGlobalDebugText(gameApi: GameApi, productionApi: ProductionApi) {\n        const productionState = QUEUES.reduce((prev, queueType) => {\n            if (productionApi.getQueueData(queueType).size === 0) {\n                return prev;\n            }\n            const paused = productionApi.getQueueData(queueType).status === QueueStatus.OnHold;\n            return (\n                prev +\n                \" [\" +\n                queueTypeToName(queueType) +\n                (paused ? \" PAUSED\" : \"\") +\n                \": \" +\n                productionApi\n                    .getQueueData(queueType)\n                    .items.map((item) => item.rules.name + (item.quantity > 1 ? \"x\" + item.quantity : \"\")) +\n                \"]\"\n            );\n        }, \"\");\n\n        const queueStates = this.queueStates\n            .filter((queueState) => queueState.items.length > 0)\n            .map((queueState) => {\n                const queueString = queueState.items\n                    .map((item) => item.unit.name + \"(\" + Math.round(item.priority * 10) / 10 + \")\")\n                    .join(\", \");\n                return `${queueTypeToName(queueState.queue)} Prios: ${queueString}\\n`;\n            })\n            .join(\"\");\n\n        return `Production: ${productionState}\\n${queueStates}`;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/building/resourceCollectionBuilding.ts",
    "content": "import { Box2, GameApi, GameMath, PlayerData, TechnoRules, Tile, Vector2 } from \"../../../game-api\";\nimport { GlobalThreat } from \"../threat/threat\";\nimport { BasicBuilding } from \"./basicBuilding\";\nimport { getDefaultPlacementLocation } from \"./buildingRules\";\nimport { getCachedTechnoRules } from \"../common/rulesCache\";\n\nconst NO_REFINERY_DISTANCE = 10;\nconst REFINERY_HARD_LIMIT = 6;\n\nexport class ResourceCollectionBuilding extends BasicBuilding {\n    constructor(basePriority: number, maxNeeded: number, onlyBuildWhenFloatingCreditsAmount?: number) {\n        super(basePriority, maxNeeded, onlyBuildWhenFloatingCreditsAmount);\n    }\n\n    getPlacementLocation(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n    ): { rx: number; ry: number } | undefined {\n        // Prefer spawning close to ore.\n        const conyardVectors = game\n            .getVisibleUnits(playerData.name, \"self\", (r) => r.constructionYard)\n            .map((r) => game.getGameObjectData(r)?.tile)\n            .filter((t): t is Tile => !!t)\n            .map((t) => new Vector2(t.rx, t.ry));\n\n        if (conyardVectors.length === 0) {\n            return undefined;\n        }\n\n        var closeOre: Tile | undefined;\n        var closeOreDist: number | undefined;\n        let selectedLocation: Vector2 = conyardVectors[0];\n\n        for (const conyard of conyardVectors) {\n            let allTileResourceData = game.mapApi.getAllTilesResourceData();\n            for (let i = 0; i < allTileResourceData.length; ++i) {\n                let tileResourceData = allTileResourceData[i];\n                if (tileResourceData.spawnsOre) {\n                    let dist = GameMath.sqrt(\n                        (conyard.x - tileResourceData.tile.rx) ** 2 + (conyard.y - tileResourceData.tile.ry) ** 2,\n                    );\n                    if (closeOreDist == undefined || dist < closeOreDist) {\n                        closeOreDist = dist;\n                        closeOre = tileResourceData.tile;\n                    }\n                }\n            }\n        }\n        if (closeOre) {\n            selectedLocation = new Vector2(closeOre.rx, closeOre.ry);\n        }\n        return getDefaultPlacementLocation(game, playerData, selectedLocation, technoRules, false, 2);\n    }\n\n    // Don't build/start selling these if we don't have any harvesters\n    getMaxCount(\n        game: GameApi,\n        playerData: PlayerData,\n        technoRules: TechnoRules,\n        threatCache: GlobalThreat | null,\n    ): number | null {\n        const harvesters = game.getVisibleUnits(playerData.name, \"self\", (r) => r.harvester).length;\n        // if there is no refinery within distance of a conyard, that conyard wants an expansion\n        const conyardBoxes = game\n            .getVisibleUnits(playerData.name, \"self\", (r) => r.constructionYard)\n            .map((r) => game.getGameObjectData(r)?.tile)\n            .filter((t): t is Tile => !!t)\n            .map((t) => new Vector2(t.rx, t.ry))\n            .map((v) => new Box2(v.clone().subScalar(NO_REFINERY_DISTANCE), v.clone().addScalar(NO_REFINERY_DISTANCE)));\n        const conyardsWithRefineries = conyardBoxes\n            .map((b) => game.getUnitsInArea(b))\n            .filter((unitIds) => unitIds.some((unitId) => getCachedTechnoRules(game, unitId)?.refinery));\n        const conyardsWithoutRefineries = conyardBoxes.length - conyardsWithRefineries.length;\n\n        return Math.max(1, Math.min(REFINERY_HARD_LIMIT, 2 * harvesters * (conyardsWithoutRefineries + 1)));\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/common/context.ts",
    "content": "import { BotContext } from \"../../../game-api\";\nimport { MatchAwareness } from \"../awareness\";\nimport { ActionBatcher } from \"../mission/actionBatcher\";\n\nexport interface SupabotContext extends BotContext {\n    readonly matchAwareness: MatchAwareness;\n}\n\nexport interface MissionContext extends SupabotContext {\n    readonly actionBatcher: ActionBatcher;\n}"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/common/rulesCache.ts",
    "content": "import { GameApi, TechnoRules } from \"../../../game-api\";\n\n// checking technorules directly reduces the amount of calls to getUnitData(), which is a relatively expensive function.\n// A null value indicates an object that does not have TechnoRules.\nconst technoRulesCache: { [rulesName: string]: TechnoRules | null } = {};\n\nexport const getCachedTechnoRules = (gameApi: GameApi, unitId: any): TechnoRules | null => {\n    const gameObject = gameApi.getGameObjectData(unitId);\n    if (!gameObject) {\n        return null;\n    }\n    const { rulesApi } = gameApi;\n    const { name } = gameObject;\n\n    if (technoRulesCache[name]) {\n        // object is present in cache, either with TechnoRules or null (indicating that it does not have TechnoRules)\n        return technoRulesCache[name];\n    }\n\n    const aircraftRules = rulesApi.aircraftRules.get(name);\n    if (aircraftRules) {\n        technoRulesCache[name] = aircraftRules;\n        return aircraftRules;\n    }\n\n    const buildingRules = rulesApi.buildingRules.get(name);\n    if (buildingRules) {\n        technoRulesCache[name] = buildingRules;\n        return buildingRules;\n    }\n\n    const infantryRules = rulesApi.infantryRules.get(name);\n    if (infantryRules) {\n        technoRulesCache[name] = infantryRules;\n        return infantryRules;\n    }\n\n    const vehicleRules = rulesApi.vehicleRules.get(name);\n    if (vehicleRules) {\n        technoRulesCache[name] = vehicleRules;\n        return vehicleRules;\n    }\n\n    technoRulesCache[name] = null;\n    return null;\n};\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/common/scout.ts",
    "content": "import { GameApi, PlayerData, Vector2 } from \"../../../game-api\";\nimport { SectorCache } from \"../map/sector\";\nimport { DebugLogger } from \"./utils\";\nimport { PriorityQueue } from \"@datastructures-js/priority-queue\";\n\nexport const getUnseenStartingLocations = (gameApi: GameApi, playerData: PlayerData) => {\n    const unseenStartingLocations = gameApi.mapApi.getStartingLocations().filter((startingLocation) => {\n        if (startingLocation == playerData.startLocation) {\n            return false;\n        }\n        let tile = gameApi.mapApi.getTile(startingLocation.x, startingLocation.y);\n        return tile ? !gameApi.mapApi.isVisibleTile(tile, playerData.name) : false;\n    });\n    return unseenStartingLocations;\n};\n\nexport class PrioritisedScoutTarget {\n    constructor(\n        public priority: number,\n        public target: Vector2,\n        public permanent: boolean = false,\n    ) {}\n\n    toString() {\n        const vector2 = this.target;\n        return `${vector2?.x},${vector2?.y}`;\n    }\n}\n\nconst ENEMY_SPAWN_POINT_PRIORITY = 900;\n\n// Distance around the starting area (in tiles) to scout first.\nconst NEARBY_SECTOR_STARTING_RADIUS = 16;\nconst NEARBY_SECTOR_BASE_PRIORITY = 900;\n\n// Amount of ticks per 'radius' to expand for scouting.\nconst SCOUTING_RADIUS_EXPANSION_TICKS = 120;\n// Don't actually queue the scouting until the radius increased by this much\nconst MIN_SCOUT_RADIUS_INCREASE = 16;\n// Don't queue scouting for sectors with enough visibility.\nconst SCOUTING_MAX_VISIBILITY_RATIO = 0.8;\n\nexport class ScoutingManager {\n    private scoutingQueue: PriorityQueue<PrioritisedScoutTarget>;\n\n    private queuedRadius = NEARBY_SECTOR_STARTING_RADIUS;\n\n    constructor(private logger: DebugLogger) {\n        // Order by descending priority.\n        this.scoutingQueue = new PriorityQueue(\n            (a: PrioritisedScoutTarget, b: PrioritisedScoutTarget) => b.priority - a.priority,\n        );\n    }\n\n    addRadiusToScout(\n        gameApi: GameApi,\n        centerPoint: Vector2,\n        sectorCache: SectorCache,\n        radius: number,\n        startingPriority: number,\n    ) {\n        const { x: startX, y: startY } = centerPoint;\n        sectorCache.forEachInRadius(startX,\n            startY, radius,\n            (x, y, sector, distance) => {\n                if (!sector) {\n                    return;\n                }\n                // Make it scout closer sectors first.\n                if (gameApi.mapApi.getTile(x, y)) {\n                    // Sector with high visility ratios are deprioritised.\n                    const ratio = sector.value.sectorVisibilityRatio ?? 0;\n                    // Do not scout sectors that are visible enough.\n                    if (ratio >= SCOUTING_MAX_VISIBILITY_RATIO) {\n                        return;\n                    }\n\n                    // Sectors closer to the starting sector are prioritised.\n                    const priority = (startingPriority - distance) * (1 - ratio);\n                    if (priority > 0) {\n                        this.scoutingQueue.enqueue(new PrioritisedScoutTarget(priority, new Vector2(x, y)));\n                    }\n                }\n            }\n        );\n    }\n\n    onGameStart(gameApi: GameApi, playerData: PlayerData, sectorCache: SectorCache) {\n        // Queue hostile starting locations with high priority and as permanent scouting candidates.\n        gameApi.mapApi\n            .getStartingLocations()\n            .filter((startingLocation) => {\n                if (startingLocation == playerData.startLocation) {\n                    return false;\n                }\n                let tile = gameApi.mapApi.getTile(startingLocation.x, startingLocation.y);\n                return tile ? !gameApi.mapApi.isVisibleTile(tile, playerData.name) : false;\n            })\n            .map((tile) => new PrioritisedScoutTarget(ENEMY_SPAWN_POINT_PRIORITY, tile, true))\n            .forEach((target) => {\n                this.logger(`Adding ${target} to initial scouting queue`);\n                this.scoutingQueue.enqueue(target);\n            });\n\n        // Queue sectors near the spawn point.\n        this.addRadiusToScout(\n            gameApi,\n            playerData.startLocation,\n            sectorCache,\n            NEARBY_SECTOR_STARTING_RADIUS,\n            NEARBY_SECTOR_BASE_PRIORITY,\n        );\n    }\n\n    onAiUpdate(gameApi: GameApi, playerData: PlayerData, sectorCache: SectorCache) {\n        const currentHead = this.scoutingQueue.front();\n        if (!currentHead) {\n            return;\n        }\n        const headTarget = currentHead.target;\n        if (!headTarget) {\n            this.scoutingQueue.dequeue();\n            return;\n        }\n        const { x, y } = headTarget;\n        const tile = gameApi.mapApi.getTile(x, y);\n        if (tile && gameApi.mapApi.isVisibleTile(tile, playerData.name)) {\n            this.logger(`head point is visible, dequeueing`);\n            this.scoutingQueue.dequeue();\n        }\n\n        const requiredRadius = Math.floor(gameApi.getCurrentTick() / SCOUTING_RADIUS_EXPANSION_TICKS);\n        if (requiredRadius > this.queuedRadius + MIN_SCOUT_RADIUS_INCREASE) {\n            this.logger(`expanding scouting radius from ${this.queuedRadius} to ${requiredRadius}`);\n            this.addRadiusToScout(\n                gameApi,\n                playerData.startLocation,\n                sectorCache,\n                requiredRadius,\n                NEARBY_SECTOR_BASE_PRIORITY,\n            );\n            this.queuedRadius = requiredRadius;\n        }\n    }\n\n    getNewScoutTarget() {\n        return this.scoutingQueue.dequeue();\n    }\n\n    hasScoutTargets() {\n        return !this.scoutingQueue.isEmpty();\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/common/tileUtils.ts",
    "content": "import {\n    GameApi,\n    GameObjectData,\n    LandType,\n    Rectangle,\n    Size,\n    SpeedType,\n    TerrainType,\n    Tile,\n    Vector2,\n} from \"../../../game-api\";\nimport { getAdjacencyTiles } from \"../building/buildingRules\";\n\nconst FLAT_RAMP_TYPE = 0;\n\n/**\n * Return true if the given tile could be built on (not including other things being there already). This is purely based on static map information.\n */\nfunction tileIsBuildable(tile: Tile) {\n    return (\n        tile.rampType === FLAT_RAMP_TYPE &&\n        (tile.terrainType === TerrainType.Clear ||\n            tile.terrainType === TerrainType.Pavement ||\n            tile.terrainType === TerrainType.Default ||\n            tile.terrainType === TerrainType.Shore ||\n            tile.terrainType === TerrainType.Rock1 ||\n            tile.terrainType === TerrainType.Rock2 ||\n            tile.terrainType === TerrainType.Rough ||\n            tile.terrainType === TerrainType.Railroad ||\n            tile.terrainType === TerrainType.Dirt)\n    );\n}\n\n/**\n * As above, but consider if there is something on the tile.\n * @param tile\n */\nfunction tileIsOccupied(tile: Tile, gameApi: GameApi) {\n    if (tile.landType === LandType.Tiberium) {\n        return true;\n    }\n    // Proxy for \"can I build something or is there something there\"\n    return !gameApi.map.isPassableTile(tile, SpeedType.Track, false, false);\n}\n\nexport function canBuildOnTile(tile: Tile, gameApi: GameApi) {\n    return tileIsBuildable(tile) && !tileIsOccupied(tile, gameApi);\n}\n\n/**\n * Computes a rect 'centered' around a structure of a certain size with an additional radius (`adjacent`).\n * The radius is optionally expanded by the size of the new building.\n *\n * This is essentially the candidate placement around a given structure.\n *\n * @param point Top-left location of the inner rect.\n * @param t Size of the inner rect.\n * @param adjacent Amount to expand the building's inner rect by (so buildings must be adjacent by this many tiles)\n * @param newBuildingSize? Size of the new building\n * @returns\n */\nexport function computeAdjacentRect(point: Vector2, t: Size, adjacent: number, newBuildingSize?: Size): Rectangle {\n    return {\n        x: point.x - adjacent - (newBuildingSize?.width || 0),\n        y: point.y - adjacent - (newBuildingSize?.height || 0),\n        width: t.width + 2 * adjacent + (newBuildingSize?.width || 0),\n        height: t.height + 2 * adjacent + (newBuildingSize?.height || 0),\n    };\n}\n\nexport function getAdjacentTiles(game: GameApi, range: Rectangle, onWater: boolean) {\n    // use the bulk API to get all tiles from the baseTile to the (baseTile + range)\n    const adjacentTiles = game.mapApi\n        .getTilesInRect(range)\n        .filter((tile) => !onWater || tile.landType === LandType.Water);\n    return adjacentTiles;\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/common/utils.ts",
    "content": "import { GameObjectData, ObjectType, PathNode, TechnoRules, Tile, UnitData, Vector2 } from \"../../../game-api\";\n\nexport enum Countries {\n    USA = \"Americans\",\n    KOREA = \"Alliance\",\n    FRANCE = \"French\",\n    GERMANY = \"Germans\",\n    GREAT_BRITAIN = \"British\",\n    LIBYA = \"Africans\",\n    IRAQ = \"Arabs\",\n    CUBA = \"Confederation\",\n    RUSSIA = \"Russians\",\n}\n\nexport type DebugLogger = (message: string, sayInGame?: boolean) => void;\n\nexport const isOwnedByNeutral = (unitData: UnitData | undefined) => unitData?.owner === \"@@NEUTRAL@@\";\n\n// Return if the given unit would have .isSelectableCombatant = true.\n// Usable on GameObjectData (which is faster to get than TechnoRules)\nexport const isSelectableCombatant = (rules: GameObjectData | undefined) =>\n    !!(rules?.rules as any)?.isSelectableCombatant;\n\n// Thanks use-strict!\nexport function formatTimeDuration(timeSeconds: number, skipZeroHours = false) {\n    let h = Math.floor(timeSeconds / 3600);\n    timeSeconds -= h * 3600;\n    let m = Math.floor(timeSeconds / 60);\n    timeSeconds -= m * 60;\n    let s = Math.floor(timeSeconds);\n\n    return [...(h || !skipZeroHours ? [h] : []), pad(m, \"00\"), pad(s, \"00\")].join(\":\");\n}\n\nexport function pad(n: any, format = \"0000\") {\n    let str = \"\" + n;\n    return format.substring(0, format.length - str.length) + str;\n}\n\n// So we don't need lodash\nexport function minBy<T>(array: T[], predicate: (arg: T) => number | null): T | null {\n    if (array.length === 0) {\n        return null;\n    }\n    let minIdx = 0;\n    let minVal = predicate(array[0]);\n    for (let i = 1; i < array.length; ++i) {\n        const newVal = predicate(array[i]);\n        if (minVal === null || (newVal !== null && newVal < minVal)) {\n            minIdx = i;\n            minVal = newVal;\n        }\n    }\n    return array[minIdx];\n}\n\nexport function maxBy<T>(array: T[], predicate: (arg: T) => number | null): T | null {\n    if (array.length === 0) {\n        return null;\n    }\n    let maxIdx = 0;\n    let maxVal = predicate(array[0]);\n    for (let i = 1; i < array.length; ++i) {\n        const newVal = predicate(array[i]);\n        if (maxVal === null || (newVal !== null && newVal > maxVal)) {\n            maxIdx = i;\n            maxVal = newVal;\n        }\n    }\n    return array[maxIdx];\n}\n\nexport function uniqBy<T>(array: T[], predicate: (arg: T) => string | number): T[] {\n    return Object.values(\n        array.reduce(\n            (prev, newVal) => {\n                const val = predicate(newVal);\n                if (!prev[val]) {\n                    prev[val] = newVal;\n                }\n                return prev;\n            },\n            {} as Record<string, T>,\n        ),\n    );\n}\n\nexport function countBy<T>(array: T[], predicate: (arg: T) => string | undefined): { [key: string]: number } {\n    return array.reduce(\n        (prev, newVal) => {\n            const val = predicate(newVal);\n            if (val === undefined) {\n                return prev;\n            }\n            if (!prev[val]) {\n                prev[val] = 0;\n            }\n            prev[val] = prev[val] + 1;\n            return prev;\n        },\n        {} as Record<string, number>,\n    );\n}\n\nexport function groupBy<K extends string, V>(array: V[], predicate: (arg: V) => K): { [key in K]: V[] } {\n    return array.reduce(\n        (prev, newVal) => {\n            const val = predicate(newVal);\n            if (val === undefined) {\n                return prev;\n            }\n            if (!prev.hasOwnProperty(val)) {\n                prev[val] = [];\n            }\n            prev[val].push(newVal);\n            return prev;\n        },\n        {} as Record<K, V[]>,\n    );\n}\n\nexport function toPathNode(tile: Tile, onBridge: boolean): PathNode {\n    return { tile, onBridge } as any;\n}\n\nexport function toVector2(tile: Tile): Vector2 {\n    return new Vector2(tile.rx, tile.ry);\n}\n\ntype TechnoRulesGameObject = Omit<GameObjectData, \"rules\"> & {\n    rules: TechnoRules;\n};\n\nexport function isTechnoRulesObject(obj: GameObjectData | undefined): obj is TechnoRulesGameObject {\n    return (\n        !!obj &&\n        (obj.rules.type === ObjectType.Building ||\n            obj.rules.type === ObjectType.Aircraft ||\n            obj.rules.type === ObjectType.Vehicle ||\n            obj.rules.type === ObjectType.Infantry)\n    );\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/map/buildSpaceCache.ts",
    "content": "import { GameApi, LandType, Size, SpeedType, TechnoRules, TerrainType, Tile, Vector2 } from \"../../../game-api\";\nimport {\n    BasicIncrementalGridCache,\n    DiagonalMapBounds,\n    getDiagonalMapBounds,\n    IncrementalGridCache,\n    SequentialScanStrategy,\n    StagedScanStrategy,\n    toHeatmapColor,\n} from \"./incrementalGridCache\";\nimport { canBuildOnTile } from \"../common/tileUtils\";\n\ntype BuildSpaceData = {\n    // number from raw map data\n    rawValue: number;\n    // number given actual objects on the field\n    liveValue: number;\n};\n\n// distance transform to find flat, buildable areas.\n// ref: https://github.com/Supalosa/supabot/blob/1ce77f3c3e210da738bf231bc6a94aa8bdf68cef/supabot-core/src/main/java/com/supalosa/bot/analysis/Analysis.java#L252\nexport class BuildSpaceCache {\n    private scanStrategy: StagedScanStrategy;\n    private distanceTransformCache: BasicIncrementalGridCache<BuildSpaceData, number>;\n\n    constructor(mapSize: Size, gameApi: GameApi, diagonalMapBounds: DiagonalMapBounds) {\n        this.scanStrategy = new StagedScanStrategy([\n            // The DT algorithm runs in 3 passes. The last pass needs to run in reverse.\n            new SequentialScanStrategy(1, diagonalMapBounds),\n            new SequentialScanStrategy(1, diagonalMapBounds),\n            new SequentialScanStrategy(1, diagonalMapBounds).setReverse(),\n        ]).setRepeating();\n        this.distanceTransformCache = new BasicIncrementalGridCache<BuildSpaceData, number>(\n            mapSize.width,\n            mapSize.height,\n            () => ({\n                rawValue: Number.MAX_VALUE,\n                liveValue: Number.MAX_VALUE,\n            }),\n            (x, y, currentValue, stageIndex) => {\n                const passIndex = stageIndex % 3;\n                if (passIndex === 0) {\n                    // First DT pass: set unbuildable tiles as distance 0\n                    const tile = gameApi.mapApi.getTile(x, y);\n                    if (!tile) {\n                        return {\n                            rawValue: 0,\n                            liveValue: 0,\n                        };\n                    }\n                    const initialValue = !canBuildOnTile(tile, gameApi) ? 0 : currentValue.rawValue;\n                    return {\n                        rawValue: initialValue,\n                        liveValue: initialValue,\n                    };\n                }\n\n                if (passIndex === 1) {\n                    // Second DT pass: all cells (except edges) update from top left\n                    if (x === 0 || y === 0) {\n                        return currentValue;\n                    }\n                    const left = this.distanceTransformCache.getCell(x - 1, y)!;\n                    const top = this.distanceTransformCache.getCell(x, y - 1)!;\n                    const nextValue = Math.min(\n                        currentValue.rawValue,\n                        Math.min(left.value.rawValue + 1, top.value.rawValue + 1),\n                    );\n                    return {\n                        rawValue: nextValue,\n                        // not necessary to set, but liveValue is the value visualised during debug\n                        liveValue: nextValue,\n                    };\n                }\n                // Last DT pass: all cells update from bottom right\n                if (x === mapSize.width - 1 || y === mapSize.height - 1) {\n                    return currentValue;\n                }\n                const right = this.distanceTransformCache.getCell(x + 1, y)!;\n                const bottom = this.distanceTransformCache.getCell(x, y + 1)!;\n                const rawValue = Math.min(\n                    currentValue.rawValue,\n                    Math.min(right.value.rawValue + 1, bottom.value.rawValue + 1),\n                );\n                return {\n                    rawValue,\n                    // not necessary to set, but liveValue is the value visualised during debug\n                    liveValue: rawValue,\n                };\n            },\n            this.scanStrategy,\n            (v) => toHeatmapColor(Math.min(15, v.liveValue ?? v.rawValue), 0, 15),\n        );\n    }\n\n    public update(gameTick: number) {\n        this.distanceTransformCache.updateCells(this.isFinished() ? 128 : 256, gameTick);\n    }\n\n    // visible for debugging\n    public get _cache(): IncrementalGridCache<BuildSpaceData> {\n        return this.distanceTransformCache;\n    }\n\n    public isFinished() {\n        return this.scanStrategy.isFinished();\n    }\n\n    public findSpace(tiles: number): Vector2[] {\n        if (!this.isFinished()) {\n            return [];\n        }\n        type Candidate = {\n            pos: Vector2;\n            value: number;\n        };\n        const candidates: Candidate[] = [];\n        this.distanceTransformCache.forEach((x, y, cell) => {\n            if (cell.lastUpdatedTick === null) {\n                return;\n            }\n            // we know it has a value if the scan is 'finished'\n            const liveValue = cell.value.liveValue!;\n            if (liveValue >= tiles) {\n                // if there's a candidate within `tiles` distance, use the higher of the two\n                const vec = new Vector2(x, y);\n                const otherCandidateIdx = candidates.findIndex((c) => c.pos.distanceTo(vec) < tiles);\n                if (otherCandidateIdx >= 0) {\n                    if (candidates[otherCandidateIdx].value < liveValue) {\n                        candidates[otherCandidateIdx] = { pos: vec, value: liveValue };\n                    }\n                } else {\n                    candidates.push({ pos: vec, value: liveValue });\n                }\n            }\n        });\n        return candidates.map(({ pos }) => pos);\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/map/incrementalGridCache.ts",
    "content": "import { GameMath, MapApi, Size } from \"../../../game-api\";\n\nexport type IncrementalGridCell<T> = {\n    lastUpdatedTick: number | null;\n    value: T;\n}\n\nexport function toHeatmapColor(value: number | null | undefined, minScale: number = 0, maxScale: number = 1) {\n    if (value === undefined || value === null) {\n        return 0;\n    }\n    const ratio = 2 * (value - minScale) / (maxScale - minScale)\n    const b = Math.max(0, 255 * (1 - ratio))\n    const r = Math.max(0, 255 * (ratio - 1))\n    const g = 255 - b - r\n    return toRGBNum(r, g, b)\n}\n\nexport function toRGBNum(red: number, green: number, blue: number) {\n    return red << 16 | green << 8 | blue;\n}\n\nexport function fromRGBNum(num: number) {\n    return [num >> 16 & 0xFF, num >> 8 & 0xFF, num & 0xFF];\n}\n\nexport interface IncrementalGridCache<T> {\n    getSize(): Size;\n    getCell(x: number, y: number): IncrementalGridCell<T> | null;\n    forEach(fn: (x: number, y: number, cell: IncrementalGridCell<T>) => void): void;\n    forEachInRadius(startX: number, startY: number, radius: number, fn: (x: number, y: number, cell: IncrementalGridCell<T>, dist: number) => void): void;\n\n    // For debug purposes, how large each cell is in game tiles.\n    _renderScale(): number;\n    _getCellDebug(x: number, y: number): IncrementalGridCell<T> & { color: number } | null;\n}\n\n/**\n * A class that allows spatial information to be updated lazily as needed, meaning some (or many) grid locations may be stale.\n * \n * Because the game maps are rotated by 45 degrees, we only scan for valid tiles.\n * \n * In game terms, a grid may be a cell for high-resolution information, or multiple cells for low resolution information (e.g. scouting sectors).\n * \n * @param T value type of each cell\n * @param V argument type passed from the scan strategy to the updater (e.g. the number of passes done so far)\n */\nexport class BasicIncrementalGridCache<T, V> implements IncrementalGridCache<T> {\n    // cells, stored in column-major order\n    private cells: IncrementalGridCell<T>[][] = [];\n\n    constructor(\n        private width: number,\n        private height: number,\n        initCellFn: (x: number, y: number) => T,\n        private updateCellFn: (x: number, y: number, currentValue: T, scanStrategyArg: V) => T,\n        private scanStrategy: IncrementalGridCacheUpdateStrategy<V>,\n        private valueToDebugColor: (value: T) => number) {\n        for (let x = 0; x < width; ++x) {\n            this.cells[x] = new Array(height);\n            for (let y = 0; y < height; ++y) {\n                this.cells[x][y] = {\n                    lastUpdatedTick: null,\n                    value: initCellFn(x, y)\n                };\n            }\n        }\n    }\n\n    public getSize(): Size {\n        return { width: this.width, height: this.height }\n    }\n\n    public getCell(x: number, y: number): IncrementalGridCell<T> | null {\n        if (x < 0 || x >= this.width || y < 0 || y >= this.height) {\n            return null;\n        }\n        return this.cells[x][y];\n    }\n\n    public _getCellDebug(x: number, y: number): IncrementalGridCell<T> & { color: number } | null {\n        if (x < 0 || x >= this.width || y < 0 || y >= this.height) {\n            return null;\n        }\n        const cell = this.cells[x][y];\n        return {\n            ...cell,\n            color: this.valueToDebugColor(cell.value)\n        };\n    }\n\n    /**\n     * Using the IncrementalGridCacheUpdateStrategy provided at construction time, update a certain number of cells with new values.\n     * \n     * @param numCellsToUpdate Number of cells to update\n     */\n    public updateCells(numCellsToUpdate: number, gameTick: number) {\n        for (let i = 0; i < numCellsToUpdate; ++i) {\n            const nextCell = this.scanStrategy.getNextCellToUpdate(this.width, this.height);\n            if (!nextCell) {\n                break;\n            }\n            const { x, y, arg } = nextCell;\n            const newValue = this.updateCellFn(x, y, this.cells[x][y].value, arg);\n            this.cells[x][y] = {\n                lastUpdatedTick: gameTick,\n                value: newValue,\n            };\n        }\n    }\n\n    /**\n     * Using a clone of the ScanStrategy, iterates over all cells in the grid and calls the provided callback function on each one.\n     */\n    public forEach(fn: (x: number, y: number, cell: IncrementalGridCell<T>) => void) {\n        const scanStrategy = this.scanStrategy.clone();\n        let next: { x: number, y: number } | null = null;\n        while ((next = scanStrategy.getNextCellToUpdate(this.width, this.height)) !== null) {\n            const { x, y } = next;\n            fn(x, y, this.cells[x][y]);\n        }\n    }\n\n\n    public forEachInRadius(startX: number, startY: number, dist: number, fn: (x: number, y: number, cell: IncrementalGridCell<T>, dist: number) => void) {\n        this.scanStrategy.getNeighbours(startX, startY, this.width, this.height, dist).forEach(({ x, y, dist }) => fn(x, y, this.getCell(x, y)!, dist));\n    }\n\n    public _renderScale() {\n        return 1;\n    }\n}\n\nexport interface IncrementalGridCacheUpdateStrategy<V> {\n    getNextCellToUpdate(width: number, height: number): { x: number; y: number, arg: V } | null;\n    getNeighbours(x: number, y: number, width: number, height: number, dist: number): { x: number, y: number, dist: number }[];\n\n    clone(): IncrementalGridCacheUpdateStrategy<V>;\n    /**\n     * True if this strategy keeps running over and over.\n     */\n    isRepeatable(): boolean;\n    /**\n     * True if consumers can trust all the relevant cells have been populated at least once.\n     */\n    isFinished(): boolean;\n}\n\nexport type DiagonalMapBounds = {\n    // All starts are inclusive. All ends are exclusive.\n    xStarts: number[];\n    xEnds: number[];\n    yStart: number;\n    yEnd: number;\n}\n\nexport function getDiagonalMapBounds(mapApi: MapApi): DiagonalMapBounds {\n    const { width, height } = mapApi.getRealMapSize();\n    const xStarts = new Array<number>(height).fill(width);\n    const xEnds = new Array<number>(height).fill(0);\n    const allTiles = mapApi.getTilesInRect({ x: 0, y: 0, width, height });\n    let yStart = height;\n    let yEnd = 0;\n    for (const tile of allTiles) {\n        if (tile.rx < xStarts[tile.ry]) {\n            xStarts[tile.ry] = tile.rx;\n        }\n        if (tile.rx >= xEnds[tile.ry]) {\n            xEnds[tile.ry] = tile.rx + 1;\n        }\n        if (tile.ry < yStart) {\n            yStart = tile.ry;\n        }\n        if (tile.ry >= yEnd) {\n            yEnd = tile.ry + 1;\n        }\n    }\n    return { xStarts, xEnds, yStart, yEnd };\n}\n\n// Dumb scan strategy: top-left to bottom-right (or reverse).\nexport class SequentialScanStrategy implements IncrementalGridCacheUpdateStrategy<number> {\n    private lastUpdatedSectorX: number | undefined;\n    private lastUpdatedSectorY: number | undefined;\n\n    private passCount: number;\n\n    /**\n     * \n     * @param maxPasses null if infinite, otherwise step through a certain number of times\n     * @param diagonalMapBounds optional diagonal bounds to prevent scanning over blank tiles\n     * You should provide this, otherwise, when scanning from 0,0 to width,height, you end up scanning about 50% of unnecessary tiles.\n     */\n    constructor(private maxPasses: number | null = null, private diagonalMapBounds: DiagonalMapBounds | null = null, private reverse: boolean = false) {\n        this.passCount = 0;\n    };\n\n    public setReverse(): this {\n        this.reverse = true;\n        return this;\n    }\n\n    private getStartY(height: number) {\n        if (this.reverse) {\n            return (this.diagonalMapBounds?.yEnd ?? height) - 1;\n        }\n        return this.diagonalMapBounds?.yStart ?? 0;\n    }\n\n    private getEndY(height: number) {\n        if (this.reverse) {\n            return (this.diagonalMapBounds?.yStart ?? 0) - 1;\n        }\n        return this.diagonalMapBounds?.yEnd ?? height;\n    }\n\n    private getStartX(y: number, width: number) {\n        if (this.reverse) {\n            if (this.diagonalMapBounds) {\n                return this.diagonalMapBounds.xEnds[y] - 1;\n            }\n            return width - 1;\n        }\n        if (this.diagonalMapBounds) {\n            return this.diagonalMapBounds.xStarts[y];\n        }\n        return 0;\n    }\n\n    private getEndX(y: number, width: number) {\n        if (this.reverse) {\n            if (this.diagonalMapBounds) {\n                return this.diagonalMapBounds.xStarts[y] - 1;\n            }\n            return width - 1;\n        }\n        if (this.diagonalMapBounds) {\n            return this.diagonalMapBounds.xEnds[y];\n        }\n        return width;\n    }\n\n    getNextCellToUpdate(width: number, height: number) {\n        // First scan, or the last scan reached the end\n        if (this.lastUpdatedSectorX === undefined || this.lastUpdatedSectorY === undefined) {\n            this.lastUpdatedSectorY = this.getStartY(height);\n            this.lastUpdatedSectorX = this.getStartX(this.lastUpdatedSectorY, width);\n            return { x: this.lastUpdatedSectorX, y: this.lastUpdatedSectorY, arg: this.passCount };\n        }\n\n        const endX = this.getEndX(this.lastUpdatedSectorY, width);\n        const endY = this.getEndY(height);\n\n        if (this.reverse) {\n            if (this.lastUpdatedSectorX - 1 > endX) {\n                return { x: --this.lastUpdatedSectorX, y: this.lastUpdatedSectorY, arg: this.passCount };\n            }\n\n            if (this.lastUpdatedSectorY - 1 > endY) {\n                this.lastUpdatedSectorX = this.getStartX(this.lastUpdatedSectorY - 1, width);\n                return { x: this.lastUpdatedSectorX, y: --this.lastUpdatedSectorY, arg: this.passCount };\n            }\n        } else {\n            if (this.lastUpdatedSectorX + 1 < endX) {\n                return { x: ++this.lastUpdatedSectorX, y: this.lastUpdatedSectorY, arg: this.passCount };\n            }\n\n            if (this.lastUpdatedSectorY + 1 < endY) {\n                this.lastUpdatedSectorX = this.getStartX(this.lastUpdatedSectorY + 1, width);\n                return { x: this.lastUpdatedSectorX, y: ++this.lastUpdatedSectorY, arg: this.passCount };\n            }\n        }\n\n        ++this.passCount;\n        if (this.maxPasses === null || this.passCount < this.maxPasses) {\n            this.lastUpdatedSectorX = undefined;\n            this.lastUpdatedSectorY = undefined;\n        }\n        return null;\n    }\n\n    getNeighbours(baseX: number, baseY: number, width: number, height: number, dist: number) {\n        const neighbours: { x: number, y: number, dist: number }[] = [];\n        const startY = this.getStartY(height);\n        const endY = this.getEndY(height);\n        if (this.reverse) {\n            for (\n                let y = Math.min(startY, baseY + dist);\n                y > Math.max(endY, baseY - dist - 1);\n                --y\n            ) {\n                const startX = this.getStartX(y, width);\n                const endX = this.getEndX(y, width);\n                for (\n                    let x: number = Math.min(startX, baseX + dist);\n                    x > Math.max(endX, baseX - dist - 1);\n                    --x\n                ) {\n                    const dist = GameMath.sqrt(GameMath.pow(x - baseX, 2) + GameMath.pow(y - baseY, 2));\n                    neighbours.push({x, y, dist});\n                }\n            }\n        } else {\n            for (\n                let y = Math.max(startY, baseY - dist);\n                y < Math.min(endY, baseY + dist + 1);\n                ++y\n            ) {\n                const startX = this.getStartX(y, width);\n                const endX = this.getEndX(y, width);\n                for (\n                    let x: number = Math.max(startX, baseX - dist);\n                    x < Math.min(endX, baseX + dist + 1);\n                    ++x\n                ) {\n                    const dist = GameMath.sqrt(GameMath.pow(x - baseX, 2) + GameMath.pow(y - baseY, 2));\n                    neighbours.push({x, y, dist});\n                }\n            }\n        }\n        return neighbours;\n    }\n\n    clone() {\n        return new SequentialScanStrategy(this.maxPasses, this.diagonalMapBounds, this.reverse);\n    }\n\n    isRepeatable(): boolean {\n        return this.maxPasses === null;\n    }\n\n    isFinished(): boolean {\n        return this.passCount > 0 && this.maxPasses === null;\n    }\n}\n\n/**\n * Scan that composes other scan strategies in stages.\n */\nexport class StagedScanStrategy implements IncrementalGridCacheUpdateStrategy<number> {\n    private stageIndex: number;\n    private originalStages: IncrementalGridCacheUpdateStrategy<number>[];\n    private hasFinishedAtLeastOnce: boolean = false;\n\n    constructor(private stages: IncrementalGridCacheUpdateStrategy<number>[], private isRepeating = false) {\n        this.originalStages = [...stages];\n        this.stageIndex = 0;\n    }\n\n    public setRepeating() {\n        this.isRepeating = true;\n        return this;\n    }\n\n    getNextCellToUpdate(width: number, height: number) {\n        if (this.stages.length === 0) {\n            return null;\n        }\n        const head = this.stages[0];\n        const headValue = head.getNextCellToUpdate(width, height);\n        if (headValue !== null) {\n            return {\n                ...headValue,\n                // override arg with our own stage index\n                arg: this.stageIndex\n            }\n        }\n        if (head.isRepeatable()) {\n            // come back to it next time\n            return null;\n        }\n        // head returned null, move to next and try again\n        this.stages.shift();\n        const next = this.stages[0];\n        ++this.stageIndex;\n        if (!next) {\n            if (this.isRepeating) {\n                this.hasFinishedAtLeastOnce = true;\n                this.reset();\n            }\n            return null;\n        }\n        const nextValue = next.getNextCellToUpdate(width, height);\n        if (!nextValue) {\n            return null;\n        }\n        return {\n            ...nextValue,\n            // override arg with our own stage index\n            arg: this.stageIndex\n        }\n    }\n\n    private reset() {\n        this.stageIndex = 0;\n        this.stages = [...this.originalStages.map((s) => s.clone())];\n    }\n\n    getNeighbours(x: number, y: number, width: number, height: number, dist: number) {\n        if (this.stages.length === 0) {\n            return [];\n        }\n        return this.stages[0].getNeighbours(x, y, width, height, dist);\n    }\n\n    isRepeatable(): boolean {\n        return this.isRepeating || this.originalStages.some((s) => s.isRepeatable());\n    }\n\n    isFinished() {\n        return this.hasFinishedAtLeastOnce;\n    }\n\n    clone() {\n        return new StagedScanStrategy(this.originalStages.map((s) => s.clone()), this.isRepeating);\n    }\n}"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/map/map.ts",
    "content": "import { GameApi, GameMath, MapApi, PlayerData, Size, Tile, UnitData, Vector2 } from \"../../../game-api\";\n\nexport function calculateAreaVisibility(\n    mapApi: MapApi,\n    playerData: PlayerData,\n    startPoint: Vector2,\n    endPoint: Vector2,\n): { visibleTiles: number; validTiles: number, clearTiles: number} {\n    let validTiles: number = 0,\n        visibleTiles: number = 0,\n        clearTiles: number = 0;\n    for (let xx = startPoint.x; xx < endPoint.x; ++xx) {\n        for (let yy = startPoint.y; yy < endPoint.y; ++yy) {\n            let tile = mapApi.getTile(xx, yy);\n            if (tile) {\n                ++validTiles;\n                if (mapApi.isVisibleTile(tile, playerData.name)) {\n                    ++visibleTiles;\n                }\n                if (tile.rampType === 0) {\n                    ++clearTiles;\n                }\n            }\n        }\n    }\n    return { visibleTiles, validTiles, clearTiles };\n}\n\nexport function getPointTowardsOtherPoint(\n    gameApi: GameApi,\n    startLocation: Vector2,\n    endLocation: Vector2,\n    minRadius: number,\n    maxRadius: number,\n    randomAngle: number,\n): Vector2 {\n    // TODO: Use proper vector maths here.\n    let radius = minRadius + Math.round(gameApi.generateRandom() * (maxRadius - minRadius));\n    let directionToEndLocation = GameMath.atan2(endLocation.y - startLocation.y, endLocation.x - startLocation.x);\n    let randomisedDirection =\n        directionToEndLocation -\n        (randomAngle * (Math.PI / 12) + 2 * randomAngle * gameApi.generateRandom() * (Math.PI / 12));\n    let candidatePointX = Math.round(startLocation.x + GameMath.cos(randomisedDirection) * radius);\n    let candidatePointY = Math.round(startLocation.y + GameMath.sin(randomisedDirection) * radius);\n    return new Vector2(candidatePointX, candidatePointY);\n}\n\nexport function getDistanceBetweenPoints(startLocation: Vector2, endLocation: Vector2): number {\n    // TODO: Remove this now we have Vector2s.\n    return startLocation.distanceTo(endLocation);\n}\n\nexport function getDistanceBetweenTileAndPoint(tile: Tile, vector: Vector2): number {\n    // TODO: Remove this now we have Vector2s.\n    return new Vector2(tile.rx, tile.ry).distanceTo(vector);\n}\n\nexport function getDistanceBetweenUnits(unit1: UnitData, unit2: UnitData): number {\n    return new Vector2(unit1.tile.rx, unit1.tile.ry).distanceTo(new Vector2(unit2.tile.rx, unit2.tile.ry));\n}\n\nexport function getDistanceBetween(unit: UnitData, point: Vector2): number {\n    return getDistanceBetweenPoints(new Vector2(unit.tile.rx, unit.tile.ry), point);\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/map/sector.ts",
    "content": "// A sector is a uniform-sized segment of the map.\n\nimport { MapApi, Size, SpeedType, Tile } from \"../../../game-api\";\nimport { BasicIncrementalGridCache, DiagonalMapBounds, IncrementalGridCache, IncrementalGridCell, SequentialScanStrategy, StagedScanStrategy, toHeatmapColor, toRGBNum } from \"./incrementalGridCache\";\nimport { getDirectionToSector, getNeighbourTiles, getSectorTilesInDirection, OPPOSITE_DIRECTION, Sector, SECTOR_SIZE, SectorAndDist } from \"./sectorUtils\";\n\n/**\n * Wrapper around IncrementalGridCache that handles scaling from tile coordinates to sectors (could probably also be refactored out)\n */\nexport class SectorCache implements IncrementalGridCache<Sector> {\n    private gridCache: BasicIncrementalGridCache<Sector, number>;\n\n    constructor(private mapBounds: Size,\n        diagonalMapBounds: DiagonalMapBounds,\n        initFn: (startX: number, startY: number) => Sector,\n        updateFn: (startX: number, startY: number, size: number, currentValue: Sector, neighbors: SectorAndDist[]) => Sector) {\n        const sectorsX = Math.ceil(mapBounds.width / SECTOR_SIZE);\n        const sectorsY = Math.ceil(mapBounds.height / SECTOR_SIZE);\n\n        // diagonal map bounds is in terms of tiles, so needs to be scaled too. In this case we take the floor of the starts and ceil of the ends\n        // so we \"overscan\"\n        function scaleBoundsArray(bounds: number[], isStart: boolean) {\n            let result: number[] = [];\n            function handleBatch(values: number[]) {\n                if (isStart) {\n                    // minimum, and floor\n                    return values.map((v) => Math.floor(v / SECTOR_SIZE)).reduce((pV, v) => v < pV ? v : pV, sectorsX);\n                }\n                // maximum, and ceil\n                return values.map((v) => Math.ceil(v / SECTOR_SIZE)).reduce((pV, v) => v > pV ? v : pV, 0);\n            }\n            let n = 0;\n            for (; n < bounds.length; n += SECTOR_SIZE) {\n                const values = bounds.slice(n, n + SECTOR_SIZE);\n                result.push(handleBatch(values));\n            }\n            if (n < bounds.length) {\n                const values = bounds.slice(n, n + SECTOR_SIZE);\n                result.push(handleBatch(values));\n            }\n            return result;\n        }\n\n        const scaledDiagonalMapBounds: DiagonalMapBounds = {\n            yStart: Math.floor(diagonalMapBounds.yStart / SECTOR_SIZE),\n            yEnd: Math.ceil(diagonalMapBounds.yEnd / SECTOR_SIZE),\n            xStarts: scaleBoundsArray(diagonalMapBounds.xStarts, true),\n            xEnds: scaleBoundsArray(diagonalMapBounds.xEnds, false),\n        };\n\n        let minThreatColored = Number.MAX_VALUE;\n        let maxThreatColored = Number.MIN_VALUE;\n        let lastScanStage = -1;\n        this.gridCache = new BasicIncrementalGridCache<Sector, number>(\n            sectorsX,\n            sectorsY,\n            initFn,\n            (sectorX, sectorY, currentValue, scanStage) => {\n                const neighbours: SectorAndDist[] = [];\n                // send the neighbours as well, to allow for diffuse sector threat\n                this.gridCache.forEachInRadius(sectorX, sectorY, 1, (nX, nY, s) => {\n                    if (nX !== sectorX || nY !== sectorY) {\n                        const dist = (sectorX === nX || sectorY === nY ? 1 : 0.707);\n                        neighbours.push({sector: s.value, x: nX, y: nY, dist});\n                    }\n                });\n                minThreatColored = Math.min(currentValue.diffuseThreatLevel ?? 0, minThreatColored);\n                maxThreatColored = Math.max(currentValue.diffuseThreatLevel ?? 0, maxThreatColored);\n                // decay the scale every full scan\n                if (scanStage !== lastScanStage) {\n                    lastScanStage = scanStage;\n                    minThreatColored = minThreatColored * 0.95;\n                    maxThreatColored = maxThreatColored * 0.95;\n                }\n                return updateFn(sectorX * SECTOR_SIZE, sectorY * SECTOR_SIZE, SECTOR_SIZE, currentValue, neighbours)\n            },\n            new StagedScanStrategy([\n                new SequentialScanStrategy(1, scaledDiagonalMapBounds),\n                new SequentialScanStrategy(1, scaledDiagonalMapBounds).setReverse()\n            ]).setRepeating(),\n            // Function to determine what colour should be rendered in the debug grid for this heatmap.\n            (sector) => {\n                // debug diffuse threat level:\n                return toHeatmapColor(sector.diffuseThreatLevel, minThreatColored, maxThreatColored);\n                // debug scouting:\n                //return toHeatmapColor(sector.sectorVisibilityRatio);\n                // debug sector connectedness\n                //return toHeatmapColor(sector.connectedSectorIds.length > 0 ? 1 : 0, 0, 1);\n            }\n        );\n    }\n\n    getSize() {\n        return this.gridCache.getSize();\n    }\n    \n    getCell(tileX: number, tileY: number) {\n        return this.gridCache.getCell(Math.floor(tileX / SECTOR_SIZE), Math.floor(tileY / SECTOR_SIZE));\n    }\n\n    forEach(fn: (tileX: number, tileY: number, cell: IncrementalGridCell<Sector>) => void): void {\n        this.gridCache.forEach((x, y, cell) => {\n            fn(Math.floor(x * SECTOR_SIZE + SECTOR_SIZE / 2), Math.floor(y * SECTOR_SIZE + SECTOR_SIZE / 2), cell);\n        });\n    }\n\n    public updateSectors(currentGameTick: number, maxSectorsToUpdate: number) {\n        this.gridCache.updateCells(maxSectorsToUpdate, currentGameTick);\n    }\n\n\n    // Return % of sectors that are updated since a certain time\n    public getSectorUpdateRatio(sectorsUpdatedSinceGameTick: number): number {\n        let updated = 0,\n            total = 0;\n        this.gridCache.forEach((_x, _y, cell) => {\n            if (\n                cell.lastUpdatedTick !== null &&\n                cell.lastUpdatedTick >= sectorsUpdatedSinceGameTick\n            ) {\n                ++updated;\n            }\n            ++total;\n        });\n        return updated / total;\n    }\n\n    /**\n     * Return the ratio (0-1) of tiles that are visible.\n     */\n    public getOverallVisibility(): number | undefined {\n        let visible = 0,\n            total = 0;\n        this.gridCache.forEach((_x, _y, cell) => {\n            const sector = cell.value;\n            // Undefined visibility.\n            if (sector.sectorVisibilityRatio != undefined) {\n                visible += sector.sectorVisibilityRatio;\n                total += 1.0;\n            }\n        });\n        return visible / total;\n    }\n\n    public forEachInRadius(\n        tileX: number,\n        tileY: number,\n        radius: number,\n        fn: (x: number, y: number, sector: IncrementalGridCell<Sector>, dist: number) => void) {\n        const startingSector = this.getSectorCoordinatesForWorldPosition(tileX, tileY);\n        if (!startingSector) {\n            return;\n        }\n        this.gridCache.forEachInRadius(startingSector.sectorX,\n            startingSector.sectorY, Math.ceil(radius / SECTOR_SIZE), (x, y, cell, distance) => {\n                fn(\n                    Math.floor(x * SECTOR_SIZE + SECTOR_SIZE / 2),\n                    Math.floor(y * SECTOR_SIZE + SECTOR_SIZE / 2),\n                    cell,\n                    distance);\n            });\n    }\n\n    private getSectorCoordinatesForWorldPosition(x: number, y: number) {\n        if (x < 0 || x >= this.mapBounds.width || y < 0 || y >= this.mapBounds.height) {\n            return undefined;\n        }\n        return {\n            sectorX: Math.floor(x / SECTOR_SIZE),\n            sectorY: Math.floor(y / SECTOR_SIZE),\n        };\n    }\n\n    public _renderScale() {\n        return SECTOR_SIZE;\n    }\n\n    public _getCellDebug(tileX: number, tileY: number): (IncrementalGridCell<Sector> & { color: number; }) | null {\n        return this.gridCache._getCellDebug(Math.floor(tileX / SECTOR_SIZE), Math.floor(tileY / SECTOR_SIZE));\n    }\n}\n\n/**\n * Computes which neighbour sectors can be pathed to from the sector starting at tileX,tileY. This is expensive, and therefore only calculated\n * when the sector is dirty (the pathing is changed in some way).\n */\nexport function calculateConnectedSectorIds(mapApi: MapApi, tileX: number, tileY: number, neighbours: SectorAndDist[], speedType: SpeedType = SpeedType.Track) {\n    // Algorithm: If you can reach the edge towards another sector, from the opposite edge, that sector is connected.\n    // For diagonal connectivity we just test the corners for now.\n    const allTiles = mapApi.getTilesInRect({x: tileX, y: tileY, width: SECTOR_SIZE, height: SECTOR_SIZE});\n    const tiles = allTiles.filter((tile) => {\n        return mapApi.isPassableTile(tile, speedType, tile.onBridgeLandType ? true : false, true);\n    });\n    if (tiles.length === 0) {\n        return [];\n    }\n    const connectedSectors = neighbours.filter((neighbour) => {\n        const direction = getDirectionToSector(tileX, tileY, neighbour);\n        const goalTiles = new Set(getSectorTilesInDirection(tileX, tileY, tiles, OPPOSITE_DIRECTION[direction]));\n        const openList = getSectorTilesInDirection(tileX, tileY, tiles, direction);\n        const closedSet = new Set();\n        let head: Tile | undefined;\n        while (head = openList.shift()) {\n            const neighbourTiles = getNeighbourTiles(head.rx, head.ry, tiles);\n            for (const neighbour of neighbourTiles) {\n                if (goalTiles.has(neighbour)) {\n                    return true;\n                }\n                if (!closedSet.has(neighbour)) {\n                    closedSet.add(neighbour);\n                    openList.push(neighbour);\n                }\n            }\n        }\n        return false;\n    });\n    \n    return connectedSectors.map((s) => s.sector.id);\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/map/sectorUtils.ts",
    "content": "import { Tile } from \"../../../game-api\";\n\nexport const SECTOR_SIZE = 8;\n\nexport function getSectorId(x: number, y: number) {\n    // 16 bits for number x 8 tiles = max tile size of 524280 :)\n    return x | y << 16;\n}\n\n/**\n * A Sector is an 8x8 area of the map, grouped together for scouting purposes.\n */\nexport type Sector = {\n    id: number;\n\n    /**\n     * Null means there are no valid tiles in the sector.\n     */\n    sectorVisibilityRatio: number | null;\n    /**\n     * Raw threat level in the sector (based on actual observation)\n     */\n    threatLevel: number | null;\n    /**\n     * Derived threat level in the sector (based on diffusing the threat to neighbouring sectors)\n     */\n    diffuseThreatLevel: number | null;\n\n    totalMoney: number | null;\n    \n    /**\n     * True if the connected sectors is dirty (e.g. pathing state has updated due to broken bridges etc)\n     */\n    connectedSectorsDirty: boolean;\n    connectedSectorIds: number[];\n};\n\nexport type SectorAndDist = {\n    sector: Sector;\n    /**\n     * x-coordinate of the sector (not tile coordinate)\n     */\n    x: number;\n    /**\n     * y-coordinate of the sector (not tile coordinate)\n     */\n    y: number;\n    /**\n     * Distance from origin sector to this sector (e.g. when iterating neighbours of a sector)\n     */\n    dist: number;\n}\n\ntype Direction = 'NW' | 'N' | 'NE' | 'W' | 'E' | 'SW' | 'S' | 'SE';\nexport const OPPOSITE_DIRECTION: Record<Direction, Direction> = {\n    \"NW\": \"SE\",\n    \"N\": \"S\",\n    \"NE\": \"SW\",\n    \"W\": \"E\",\n    \"E\": \"W\",\n    \"SW\": \"NE\",\n    \"S\": \"N\",\n    \"SE\": \"NW\",\n};\n\nexport function getDirectionToSector(tileX: number, tileY: number, neighbour: SectorAndDist): Direction{\n    // in tiles\n    const nX = neighbour.x * SECTOR_SIZE;\n    const nY = neighbour.y * SECTOR_SIZE;\n    if (nX === tileX - SECTOR_SIZE && nY === tileY - SECTOR_SIZE) {\n        return 'NW';\n    } else if (nX === tileX && nY === tileY - SECTOR_SIZE) {\n        return 'N';\n    } else if (nX === tileX + SECTOR_SIZE && nY === tileY - SECTOR_SIZE) {\n        return 'NE';\n    } else if (nX === tileX - SECTOR_SIZE && nY === tileY) {\n        return 'W';\n    } else if (nX === tileX + SECTOR_SIZE && nY === tileY) {\n        return 'E';\n    } else if (nX === tileX - SECTOR_SIZE && nY === tileY + SECTOR_SIZE) {\n        return 'SW';\n    } else if (nX === tileX && nY === tileY + SECTOR_SIZE) {\n        return 'S';\n    } else if (nX === tileX + SECTOR_SIZE && nY === tileY + SECTOR_SIZE) {\n        return 'SE';\n    } else {\n        throw new Error(`unable to determine sector direction from ${tileX},${tileY} to ${nX},${nY}`);\n    }\n}\n\nexport function getSectorTilesInDirection(tileX: number, tileY: number, tiles: Tile[], direction: Direction) {\n    const edgeX = tileX + SECTOR_SIZE - 1;\n    const edgeY = tileY + SECTOR_SIZE - 1;\n    return tiles.filter((tile) => {\n        switch (direction) {\n            case \"NW\":\n                return tile.rx === tileX && tile.ry === tileY;\n            case \"N\":\n                return tile.ry === tileY;\n            case \"NE\":\n                return tile.rx === edgeX && tile.ry === tileY;\n            case \"W\":\n                return tile.rx === tileX;\n            case \"E\":\n                return tile.rx === edgeX;\n            case \"SW\":\n                return tile.rx === tileX && tile.ry === edgeY;\n            case \"S\":\n                return tile.ry === edgeY;\n            case \"SE\":\n                return tile.rx === edgeX && tile.ry === edgeY;\n        }\n    });\n}\n\nexport function getNeighbourTiles(tileX: number, tileY: number, tiles: Tile[]) {\n    return tiles.filter(({rx, ry}) => {\n        return (rx === tileX + 1 || rx === tileX - 1 || ry === tileY + 1 || ry === tileY - 1) && rx !== tileX && ry !== tileY;\n    })\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/actionBatcher.ts",
    "content": "// Used to group related actions together to minimise actionApi calls. For example, if multiple units\n\nimport { ActionsApi, OrderType, Vector2 } from \"../../../game-api\";\nimport { groupBy } from \"../common/utils\";\n\n// are ordered to move to the same location, all of them will be ordered to move in a single action.\nexport class BatchableAction {\n    private constructor(\n        private _unitId: number,\n        private _orderType: OrderType,\n        private _point?: Vector2,\n        private _targetId?: number,\n        // If you don't want this action to be swallowed by dedupe, provide a unique nonce\n        private _nonce: number = 0,\n    ) {}\n\n    static noTarget(unitId: number, orderType: OrderType, nonce: number = 0) {\n        return new BatchableAction(unitId, orderType, undefined, undefined, nonce);\n    }\n\n    static toPoint(unitId: number, orderType: OrderType, point: Vector2, nonce: number = 0) {\n        return new BatchableAction(unitId, orderType, point, undefined);\n    }\n\n    static toTargetId(unitId: number, orderType: OrderType, targetId: number, nonce: number = 0) {\n        return new BatchableAction(unitId, orderType, undefined, targetId, nonce);\n    }\n\n    public get unitId() {\n        return this._unitId;\n    }\n\n    public get orderType() {\n        return this._orderType;\n    }\n\n    public get point() {\n        return this._point;\n    }\n\n    public get targetId() {\n        return this._targetId;\n    }\n\n    public isSameAs(other: BatchableAction) {\n        if (this._unitId !== other._unitId) {\n            return false;\n        }\n        if (this._orderType !== other._orderType) {\n            return false;\n        }\n        if (this._point !== other._point) {\n            return false;\n        }\n        if (this._targetId !== other._targetId) {\n            return false;\n        }\n        if (this._nonce !== other._nonce) {\n            return false;\n        }\n        return true;\n    }\n}\n\nexport class ActionBatcher {\n    private actions: BatchableAction[];\n\n    constructor() {\n        this.actions = [];\n    }\n\n    push(action: BatchableAction) {\n        this.actions.push(action);\n    }\n\n    resolve(actionsApi: ActionsApi) {\n        const groupedCommands = groupBy(this.actions, (action) => action.orderType.valueOf().toString());\n        const vectorToStr = (v: Vector2) => v.x + \",\" + v.y;\n        const strToVector = (str: string) => {\n            const [x, y] = str.split(\",\");\n            return new Vector2(parseInt(x), parseInt(y));\n        };\n\n        // Group by command type.\n        Object.entries(groupedCommands).forEach(([commandValue, commands]) => {\n            // i hate this\n            const commandType: OrderType = parseInt(commandValue) as OrderType;\n            // Group by command target ID.\n            const byTarget = groupBy(\n                commands.filter((command) => !!command.targetId),\n                (command) => command.targetId?.toString()!,\n            );\n            Object.entries(byTarget).forEach(([targetId, unitCommands]) => {\n                actionsApi.orderUnits(\n                    unitCommands.map((command) => command.unitId),\n                    commandType,\n                    parseInt(targetId),\n                );\n            });\n            // Group by position (the vector is encoded as a string of the form \"x,y\")\n            const byPosition = groupBy(\n                commands.filter((command) => !!command.point),\n                (command) => vectorToStr(command.point!),\n            );\n            Object.entries(byPosition).forEach(([point, unitCommands]) => {\n                const vector = strToVector(point);\n                actionsApi.orderUnits(\n                    unitCommands.map((command) => command.unitId),\n                    commandType,\n                    vector.x,\n                    vector.y,\n                );\n            });\n            // Actions with no targets\n            const noTargets = commands.filter((command) => !command.targetId && !command.point);\n            if (noTargets.length > 0) {\n                actionsApi.orderUnits(\n                    noTargets.map((action) => action.unitId),\n                    commandType,\n                );\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/mission.ts",
    "content": "import { GameApi, GameObjectData, TechnoRules, Tile, UnitData, Vector2 } from \"../../../game-api\";\nimport { countBy, DebugLogger } from \"../common/utils\";\nimport { getDistanceBetweenTileAndPoint } from \"../map/map\";\nimport { getCachedTechnoRules } from \"../common/rulesCache\";\nimport { MissionContext } from \"../common/context\";\nimport { UnitComposition } from \"../../strategy/strategy\";\n\nconst calculateCenterOfMass: (unitTiles: Tile[]) => {\n    centerOfMass: Vector2;\n    maxDistance: number;\n} | null = (unitTiles) => {\n    if (unitTiles.length === 0) {\n        return null;\n    }\n    // TODO: use median here\n    const sums = unitTiles.reduce(\n        ({ x, y }, tile) => {\n            return {\n                x: x + (tile?.rx || 0),\n                y: y + (tile?.ry || 0),\n            };\n        },\n        { x: 0, y: 0 },\n    );\n    const centerOfMass = new Vector2(Math.round(sums.x / unitTiles.length), Math.round(sums.y / unitTiles.length));\n\n    // max distance of units to the center of mass\n    const distances = unitTiles.map((tile) => getDistanceBetweenTileAndPoint(tile, centerOfMass));\n    const maxDistance = Math.max(...distances);\n    return { centerOfMass, maxDistance };\n};\n// AI starts Missions based on heuristics.\nexport abstract class Mission<FailureReasons = undefined> {\n    private active = true;\n    private unitIds: number[] = [];\n    private centerOfMass: Vector2 | null = null;\n    private maxDistanceToCenterOfMass: number | null = null;\n\n    private onFinish: (unitIds: number[], reason: FailureReasons) => void = () => {};\n\n    constructor(\n        private uniqueName: string,\n        protected logger: DebugLogger,\n    ) {}\n\n    // TODO call this\n    protected updateCenterOfMass(gameApi: GameApi) {\n        const unitTiles = this.unitIds\n            .map((unitId) => gameApi.getGameObjectData(unitId))\n            .map((unit) => unit?.tile)\n            .filter((tile) => !!tile) as Tile[];\n        const tileMetrics = calculateCenterOfMass(unitTiles);\n        if (tileMetrics) {\n            this.centerOfMass = tileMetrics.centerOfMass;\n            this.maxDistanceToCenterOfMass = tileMetrics.maxDistance;\n        } else {\n            this.centerOfMass = null;\n            this.maxDistanceToCenterOfMass = null;\n        }\n    }\n\n    public onAiUpdate(context: MissionContext): MissionAction {\n        this.updateCenterOfMass(context.game);\n        return this._onAiUpdate(context);\n    }\n\n    // TODO: fix this weird indirection where we call onAiUpdate publically to call the implementation of the class.\n    abstract _onAiUpdate(context: MissionContext): MissionAction;\n\n    isActive(): boolean {\n        return this.active;\n    }\n\n    public getUnitIds(): number[] {\n        return this.unitIds;\n    }\n\n    public removeUnit(unitIdToRemove: number): void {\n        this.unitIds = this.unitIds.filter((unitId) => unitId != unitIdToRemove);\n    }\n\n    public addUnit(unitIdToAdd: number): void {\n        this.unitIds.push(unitIdToAdd);\n    }\n\n    // Note: don't call this unless you REALLY need the UnitData instead of the GameObjectData.\n    public getUnits(gameApi: GameApi): UnitData[] {\n        return this.unitIds\n            .map((unitId) => gameApi.getUnitData(unitId))\n            .filter((unit) => unit != null)\n            .map((unit) => unit!);\n    }\n\n    // returns GameObjectData, which is significantly faster to retrieve.\n    public getUnitsGameObjectData(gameApi: GameApi): GameObjectData[] {\n        return this.unitIds\n            .map((unitId) => gameApi.getGameObjectData(unitId))\n            .filter((unit) => unit != null)\n            .map((unit) => unit!);\n    }\n\n    public getUnitsOfTypes(gameApi: GameApi, ...names: string[]): UnitData[] {\n        return this.unitIds\n            .map((unitId) => gameApi.getUnitData(unitId))\n            .filter((unit) => !!unit && names.includes(unit.name))\n            .map((unit) => unit!);\n    }\n\n    public getUnitsMatchingByRule(gameApi: GameApi, filter: (r: TechnoRules) => boolean): number[] {\n        type ValidEntry = {\n            unitId: number;\n            rules: TechnoRules;\n        };\n        return this.unitIds\n            .map((unitId) => ({\n                unitId,\n                rules: getCachedTechnoRules(gameApi, unitId),\n            }))\n            .filter((entry): entry is ValidEntry => entry.rules !== null)\n            .filter(({ rules }) => filter(rules))\n            .map(({ unitId }) => unitId);\n    }\n\n    protected getMissingUnits(gameApi: GameApi, targetComposition: UnitComposition): [string, number][] {\n        const currentComposition: UnitComposition = countBy(this.getUnitsGameObjectData(gameApi), (unit) => unit.name);\n        return Object.entries(targetComposition)\n            .filter(([unitType, targetAmount]) => {\n                return !currentComposition[unitType] || currentComposition[unitType] < targetAmount;\n            })\n            .filter(([unitType, targetAmount]) => targetAmount > 0);\n    }\n\n    public getCenterOfMass() {\n        return this.centerOfMass;\n    }\n\n    public getMaxDistanceToCenterOfMass() {\n        return this.maxDistanceToCenterOfMass;\n    }\n\n    getUniqueName(): string {\n        return this.uniqueName;\n    }\n\n    // Don't call this from the mission itself\n    endMission(reason: FailureReasons): void {\n        this.onFinish(this.unitIds, reason);\n        this.active = false;\n    }\n\n    /**\n     * Declare a callback that is executed when the mission is disbanded for whatever reason.\n     */\n    withOnFinish(onFinish: (unitIds: number[], reason: FailureReasons) => void): Mission<FailureReasons> {\n        this.onFinish = onFinish;\n        return this;\n    }\n\n    abstract getGlobalDebugText(): string | undefined;\n\n    /**\n     * Determines whether units can be stolen from this mission by other missions with higher priority.\n     */\n    public isUnitsLocked(): boolean {\n        return true;\n    }\n\n    abstract getPriority(): number;\n}\n\nexport type MissionWithAction<T extends MissionAction> = {\n    mission: Mission<any>;\n    action: T;\n};\n\nexport type MissionActionNoop = {\n    type: \"noop\";\n};\n\nexport type MissionActionDisband = {\n    type: \"disband\";\n    reason: any | null;\n};\n\nexport type MissionActionRequestUnits = {\n    type: \"request\";\n    unitNameToPriority: Record<string, number>;\n};\n\nexport type MissionActionRequestSpecificUnits = {\n    type: \"requestSpecific\";\n    unitIds: number[];\n    priority: number;\n};\n\nexport type MissionActionGrabFreeCombatants = {\n    type: \"requestCombatants\";\n    point: Vector2;\n    radius: number;\n};\n\nexport type MissionActionReleaseUnits = {\n    type: \"releaseUnits\";\n    unitIds: number[];\n};\n\nexport type MissionActionBuildStructureAtLocation = {\n    type: \"buildStructureAtLocation\";\n    rulesName: string;\n    priority: number;\n    rx: number;\n    ry: number;\n};\n\nexport const noop = () =>\n    ({\n        type: \"noop\",\n    }) as MissionActionNoop;\n\nexport const disbandMission = (reason?: any) => ({ type: \"disband\", reason }) as MissionActionDisband;\nexport const isDisbandMission = (a: MissionWithAction<MissionAction>): a is MissionWithAction<MissionActionDisband> =>\n    a.action.type === \"disband\";\n\nexport const requestUnits = (unitNameToPriority: Record<string, number>) =>\n    ({ type: \"request\", unitNameToPriority }) as MissionActionRequestUnits;\nexport const requestUnitsWithSamePriority = (unitNames: string[], priority: number) =>\n    ({\n        type: \"request\",\n        unitNameToPriority: Object.fromEntries(unitNames.map((name) => [name, priority])),\n    }) as MissionActionRequestUnits;\nexport const isRequestUnits = (\n    a: MissionWithAction<MissionAction>,\n): a is MissionWithAction<MissionActionRequestUnits> => a.action.type === \"request\";\n\nexport const requestSpecificUnits = (unitIds: number[], priority: number) =>\n    ({ type: \"requestSpecific\", unitIds, priority }) as MissionActionRequestSpecificUnits;\nexport const isRequestSpecificUnits = (\n    a: MissionWithAction<MissionAction>,\n): a is MissionWithAction<MissionActionRequestSpecificUnits> => a.action.type === \"requestSpecific\";\n\nexport const grabCombatants = (point: Vector2, radius: number) =>\n    ({ type: \"requestCombatants\", point, radius }) as MissionActionGrabFreeCombatants;\nexport const isGrabCombatants = (\n    a: MissionWithAction<MissionAction>,\n): a is MissionWithAction<MissionActionGrabFreeCombatants> => a.action.type === \"requestCombatants\";\n\nexport const releaseUnits = (unitIds: number[]) => ({ type: \"releaseUnits\", unitIds }) as MissionActionReleaseUnits;\nexport const isReleaseUnits = (\n    a: MissionWithAction<MissionAction>,\n): a is MissionWithAction<MissionActionReleaseUnits> => a.action.type === \"releaseUnits\";\n\nexport const buildStructureAtLocation = (rulesName: string, priority: number, rx: number, ry: number) =>\n    ({ type: \"buildStructureAtLocation\", rulesName, priority, rx, ry }) satisfies MissionActionBuildStructureAtLocation;\nexport const isBuildStructureAtLocation = (\n    a: MissionWithAction<MissionAction>,\n): a is MissionWithAction<MissionActionBuildStructureAtLocation> => a.action.type === \"buildStructureAtLocation\";\n\nexport type MissionAction =\n    | MissionActionNoop\n    | MissionActionDisband\n    | MissionActionRequestUnits\n    | MissionActionRequestSpecificUnits\n    | MissionActionGrabFreeCombatants\n    | MissionActionReleaseUnits\n    | MissionActionBuildStructureAtLocation;\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/missionController.ts",
    "content": "// Meta-controller for forming and controlling missions.\n// Missions are groups of zero or more units that aim to accomplish a particular goal.\n\nimport { ActionsApi, BotContext, GameApi, GameObjectData, Vector2 } from \"../../../game-api\";\nimport {\n    Mission,\n    MissionActionDisband,\n    MissionActionRequestSpecificUnits,\n    MissionWithAction,\n    isBuildStructureAtLocation,\n    isDisbandMission,\n    isGrabCombatants,\n    isReleaseUnits,\n    isRequestSpecificUnits,\n    isRequestUnits,\n} from \"./mission\";\nimport { ActionBatcher } from \"./actionBatcher\";\nimport { countBy, isSelectableCombatant } from \"../common/utils\";\nimport { MissionContext, SupabotContext } from \"../common/context\";\n\n// `missingUnitTypes` priority decays by this much every update loop.\nconst MISSING_UNIT_TYPE_REQUEST_DECAY_MULT_RATE = 0.75;\nconst MISSING_UNIT_TYPE_REQUEST_DECAY_FLAT_RATE = 1;\n\nexport type UnitRequest = {\n    priority: number;\n    // Relevant for structures only.\n    specificLocation: Vector2 | null;\n};\ntype UnitRequestWithMission = { mission: Mission<any> } & UnitRequest;\n\nexport class MissionController {\n    private missions: Mission<any>[] = [];\n\n    // A mapping of unit IDs to the missions they are assigned to. This may contain units that are dead, but\n    // is periodically cleaned in the update loop.\n    private unitIdToMission: Map<number, Mission<any>> = new Map();\n\n    // A mapping of unit types to the highest priority requested for a mission.\n    // This decays over time if requests are not 'refreshed' by mission.\n    private requestedUnitTypes: Map<string, UnitRequest> = new Map();\n\n    // Tracks missions to be externally disbanded the next time the mission update loop occurs.\n    private forceDisbandedMissions: string[] = [];\n\n    constructor(private logger: (message: string, sayInGame?: boolean) => void) {}\n\n    private updateUnitIds(botContext: BotContext) {\n        // Check for units in multiple missions, this shouldn't happen.\n        this.unitIdToMission = new Map();\n        this.missions.forEach((mission) => {\n            const toRemove: number[] = [];\n            mission.getUnitIds().forEach((unitId) => {\n                if (this.unitIdToMission.has(unitId)) {\n                    this.logger(`WARNING: unit ${unitId} is in multiple missions, please debug.`);\n                } else if (!botContext.game.getGameObjectData(unitId)) {\n                    // say, if a unit was killed\n                    toRemove.push(unitId);\n                } else {\n                    this.unitIdToMission.set(unitId, mission);\n                }\n            });\n            toRemove.forEach((unitId) => mission.removeUnit(unitId));\n        });\n    }\n\n    public onAiUpdate(context: SupabotContext) {\n        // Remove inactive missions.\n        this.missions = this.missions.filter((missions) => missions.isActive());\n\n        this.updateUnitIds(context);\n\n        // Batch actions to reduce spamming of actions for larger armies.\n        const actionBatcher = new ActionBatcher();\n\n        const missionContext = {\n            ...context,\n            actionBatcher,\n        } satisfies MissionContext;\n\n        // Poll missions for requested actions.\n        const missionActions: MissionWithAction<any>[] = this.missions.map((mission) => ({\n            mission,\n            action: mission.onAiUpdate(missionContext),\n        }));\n\n        // Handle disbands and merges.\n        const disbandedMissions: Map<string, any> = new Map();\n        this.forceDisbandedMissions.forEach((name) => disbandedMissions.set(name, null));\n        this.forceDisbandedMissions = [];\n        missionActions.filter(isDisbandMission).forEach((a) => {\n            this.logger(`Mission ${a.mission.getUniqueName()} disbanding as requested.`);\n            a.mission.getUnitIds().forEach((unitId) => {\n                this.unitIdToMission.delete(unitId);\n                context.player.actions.setUnitDebugText(unitId, undefined);\n            });\n            disbandedMissions.set(a.mission.getUniqueName(), (a.action as MissionActionDisband).reason);\n        });\n\n        // Handle unit requests.\n\n        // Release units\n        missionActions.filter(isReleaseUnits).forEach((a) => {\n            a.action.unitIds.forEach((unitId) => {\n                if (this.unitIdToMission.get(unitId)?.getUniqueName() === a.mission.getUniqueName()) {\n                    this.removeUnitFromMission(a.mission, unitId, context.player.actions);\n                }\n            });\n        });\n\n        // Request specific units by ID\n        const unitIdToHighestRequest = missionActions.filter(isRequestSpecificUnits).reduce(\n            (prev, missionWithAction) => {\n                const { unitIds } = missionWithAction.action;\n                unitIds.forEach((unitId) => {\n                    if (prev.hasOwnProperty(unitId)) {\n                        if (missionWithAction.action.priority > prev[unitId].action.priority) {\n                            prev[unitId] = missionWithAction;\n                        }\n                    } else {\n                        prev[unitId] = missionWithAction;\n                    }\n                });\n                return prev;\n            },\n            {} as Record<number, MissionWithAction<MissionActionRequestSpecificUnits>>,\n        );\n\n        // Map of Mission ID to Unit Type to Count.\n        const newMissionAssignments = Object.entries(unitIdToHighestRequest)\n            .flatMap(([id, request]) => {\n                const unitId = Number.parseInt(id);\n                const unit = context.game.getGameObjectData(unitId);\n                const { mission: requestingMission } = request;\n                const missionName = requestingMission.getUniqueName();\n                if (!unit) {\n                    this.logger(`mission ${missionName} requested non-existent unit ${unitId}`);\n                    return [];\n                }\n                if (!this.unitIdToMission.has(unitId)) {\n                    this.addUnitToMission(requestingMission, unit, context.player.actions);\n                    return [{ unitName: unit?.name, mission: requestingMission.getUniqueName() }];\n                }\n                return [];\n            })\n            .reduce(\n                (acc, curr) => {\n                    if (!acc[curr.mission]) {\n                        acc[curr.mission] = {};\n                    }\n                    if (!acc[curr.mission][curr.unitName]) {\n                        acc[curr.mission][curr.unitName] = 0;\n                    }\n                    acc[curr.mission][curr.unitName] = acc[curr.mission][curr.unitName] + 1;\n                    return acc;\n                },\n                {} as Record<string, Record<string, number>>,\n            );\n        Object.entries(newMissionAssignments).forEach(([mission, assignments]) => {\n            this.logger(\n                `Mission ${mission} received: ${Object.entries(assignments)\n                    .map(([unitType, count]) => unitType + \" x \" + count)\n                    .join(\", \")}`,\n            );\n        });\n\n        // Request units by type - store the highest priority mission for each unit type.\n        const unitTypeToHighestRequest = missionActions.filter(isRequestUnits).reduce(\n            (prev, missionWithAction) => {\n                const { unitNameToPriority } = missionWithAction.action;\n                Object.entries(unitNameToPriority).forEach(([unitName, requestedPriority]) => {\n                    if (prev.hasOwnProperty(unitName)) {\n                        if (requestedPriority > prev[unitName].priority) {\n                            prev[unitName] = {\n                                mission: missionWithAction.mission,\n                                priority: requestedPriority,\n                                specificLocation: null,\n                            };\n                        }\n                    } else {\n                        prev[unitName] = {\n                            mission: missionWithAction.mission,\n                            priority: requestedPriority,\n                            specificLocation: null,\n                        };\n                    }\n                });\n                return prev;\n            },\n            {} as Record<string, UnitRequestWithMission>,\n        );\n\n        // Request combat-capable units in an area\n        const grabRequests = missionActions.filter(isGrabCombatants);\n\n        // Find un-assigned units and distribute them among all the requesting missions.\n        const unitIds = context.game.getVisibleUnits(context.player.name, \"self\");\n        type UnitWithMission = {\n            unit: GameObjectData;\n            mission: Mission<any> | undefined;\n        };\n        // List of units that are unassigned or not in a locked mission.\n        const freeUnits: UnitWithMission[] = unitIds\n            .map((unitId) => context.game.getGameObjectData(unitId))\n            .filter((unit): unit is GameObjectData => !!unit)\n            .map((unit) => ({\n                unit,\n                mission: this.unitIdToMission.get(unit.id),\n            }))\n            .filter((unitWithMission) => !unitWithMission.mission || unitWithMission.mission.isUnitsLocked() === false);\n\n        // Sort free units so that unassigned units get chosen before assigned (but unlocked) units.\n        freeUnits.sort((u1, u2) => (u1.mission?.getPriority() ?? 0) - (u2.mission?.getPriority() ?? 0));\n\n        type AssignmentWithType = { unitName: string; missionName: string; method: \"type\" | \"grab\" };\n        const newAssignmentsByType = freeUnits\n            .flatMap(({ unit: freeUnit, mission: donatingMission }) => {\n                if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {\n                    const { mission: requestingMission, priority: requestedPriority } =\n                        unitTypeToHighestRequest[freeUnit.name];\n                    if (donatingMission) {\n                        if (\n                            donatingMission === requestingMission ||\n                            donatingMission.getPriority() > requestedPriority\n                        ) {\n                            return [];\n                        }\n                        this.removeUnitFromMission(donatingMission, freeUnit.id, context.player.actions);\n                    }\n                    this.logger(\n                        `granting unit ${freeUnit.id}#${freeUnit.name} to mission ${requestingMission.getUniqueName()}`,\n                    );\n                    this.addUnitToMission(requestingMission, freeUnit, context.player.actions);\n                    delete unitTypeToHighestRequest[freeUnit.name];\n                    return [\n                        { unitName: freeUnit.name, missionName: requestingMission.getUniqueName(), method: \"type\" },\n                    ] as AssignmentWithType[];\n                } else if (grabRequests.length > 0) {\n                    const grantedMission = grabRequests.find((request) => {\n                        const canGrabUnit = isSelectableCombatant(freeUnit);\n                        return (\n                            canGrabUnit &&\n                            request.action.point.distanceTo(new Vector2(freeUnit.tile.rx, freeUnit.tile.ry)) <=\n                                request.action.radius\n                        );\n                    });\n                    if (grantedMission) {\n                        if (donatingMission) {\n                            if (\n                                donatingMission === grantedMission.mission ||\n                                donatingMission.getPriority() > grantedMission.mission.getPriority()\n                            ) {\n                                return [];\n                            }\n                            this.removeUnitFromMission(donatingMission, freeUnit.id, context.player.actions);\n                        }\n                        this.addUnitToMission(grantedMission.mission, freeUnit, context.player.actions);\n                        return [\n                            {\n                                unitName: freeUnit.name,\n                                missionName: grantedMission.mission.getUniqueName(),\n                                method: \"grab\",\n                            },\n                        ] as AssignmentWithType[];\n                    }\n                }\n                return [];\n            })\n            .reduce(\n                (acc, curr) => {\n                    if (!acc[curr.missionName]) {\n                        acc[curr.missionName] = {};\n                    }\n                    if (!acc[curr.missionName][curr.unitName]) {\n                        acc[curr.missionName][curr.unitName] = { grab: 0, type: 0 };\n                    }\n                    acc[curr.missionName][curr.unitName][curr.method] =\n                        acc[curr.missionName][curr.unitName][curr.method] + 1;\n                    return acc;\n                },\n                {} as Record<string, Record<string, Record<\"type\" | \"grab\", number>>>,\n            );\n        Object.entries(newAssignmentsByType).forEach(([mission, assignments]) => {\n            this.logger(\n                `Mission ${mission} received: ${Object.entries(assignments)\n                    .flatMap(([unitType, methodToCount]) =>\n                        Object.entries(methodToCount)\n                            .filter(([, count]) => count > 0)\n                            .map(([method, count]) => unitType + \" x \" + count + \" (by \" + method + \")\"),\n                    )\n                    .join(\", \")}`,\n            );\n        });\n\n        // Handle structure requests.\n        missionActions.filter(isBuildStructureAtLocation).forEach((a) => {\n            const { rulesName, rx, ry } = a.action;\n            if (rulesName in unitTypeToHighestRequest) {\n                const currentPriority = unitTypeToHighestRequest[rulesName].priority;\n                if (a.mission.getPriority() > currentPriority) {\n                    unitTypeToHighestRequest[rulesName] = {\n                        mission: a.mission,\n                        priority: a.action.priority,\n                        specificLocation: new Vector2(rx, ry),\n                    };\n                }\n            } else {\n                unitTypeToHighestRequest[rulesName] = {\n                    mission: a.mission,\n                    priority: a.action.priority,\n                    specificLocation: new Vector2(rx, ry),\n                };\n            }\n        });\n\n        this.updateRequestedUnitTypes(unitTypeToHighestRequest);\n\n        // Send all actions that can be batched together.\n        actionBatcher.resolve(context.player.actions);\n\n        // Remove disbanded and merged missions.\n        this.missions\n            .filter((missions) => disbandedMissions.has(missions.getUniqueName()))\n            .forEach((disbandedMission) => {\n                const reason = disbandedMissions.get(disbandedMission.getUniqueName());\n                this.logger(`mission disbanded: ${disbandedMission.getUniqueName()}, reason: ${reason}`);\n                disbandedMission.endMission(disbandedMissions.get(disbandedMission.getUniqueName()));\n            });\n        this.missions = this.missions.filter((missions) => !disbandedMissions.has(missions.getUniqueName()));\n    }\n\n    private updateRequestedUnitTypes(missingUnitTypeToHighestRequest: Record<string, UnitRequestWithMission>) {\n        // Decay the priority over time.\n        for (const [unitType, currentRequest] of this.requestedUnitTypes.entries()) {\n            const newPriority =\n                currentRequest.priority * MISSING_UNIT_TYPE_REQUEST_DECAY_MULT_RATE -\n                MISSING_UNIT_TYPE_REQUEST_DECAY_FLAT_RATE;\n            if (newPriority > 0.5) {\n                this.requestedUnitTypes.set(unitType, {\n                    ...currentRequest,\n                    priority: newPriority,\n                });\n            } else {\n                this.requestedUnitTypes.delete(unitType);\n            }\n        }\n        // Add the new missing units to the priority set, if the request is higher than the existing value.\n        Object.entries(missingUnitTypeToHighestRequest).forEach(([unitType, request]) => {\n            const currentRequest = this.requestedUnitTypes.get(unitType);\n            if (!currentRequest) {\n                this.requestedUnitTypes.set(unitType, request);\n                return;\n            }\n            this.requestedUnitTypes.set(\n                unitType,\n                request.priority > currentRequest.priority ? request : currentRequest,\n            );\n        });\n    }\n\n    /**\n     * Returns the set of units that have been requested for production by the missions.\n     *\n     * @returns A map of unit type to the highest priority for that unit type.\n     */\n    public getRequestedUnitTypes(): Map<string, UnitRequest> {\n        return this.requestedUnitTypes;\n    }\n\n    private addUnitToMission(mission: Mission<any>, unit: GameObjectData, actionsApi: ActionsApi) {\n        mission.addUnit(unit.id);\n        this.unitIdToMission.set(unit.id, mission);\n        actionsApi.setUnitDebugText(unit.id, mission.getUniqueName() + \"_\" + unit.id);\n    }\n\n    private removeUnitFromMission(mission: Mission<any>, unitId: number, actionsApi: ActionsApi) {\n        mission.removeUnit(unitId);\n        this.unitIdToMission.delete(unitId);\n        actionsApi.setUnitDebugText(unitId, undefined);\n    }\n\n    /**\n     * Attempts to add a mission to the active set.\n     * @param mission\n     * @returns The mission if it was accepted, or null if it was not.\n     */\n    public addMission(mission: Mission<any>): Mission<any> | null {\n        if (this.missions.some((m) => m.getUniqueName() === mission.getUniqueName())) {\n            // reject non-unique mission names\n            return null;\n        }\n        this.logger(`Added mission: ${mission.getUniqueName()}`);\n        this.missions.push(mission);\n        return mission;\n    }\n\n    /**\n     * Disband the provided mission on the next possible opportunity.\n     */\n    public disbandMission(missionName: string) {\n        this.forceDisbandedMissions.push(missionName);\n    }\n\n    // return text to display for global debug\n    public getGlobalDebugText(gameApi: GameApi): string {\n        const unitsInMission = (unitIds: number[]) =>\n            countBy(unitIds, (unitId) => gameApi.getGameObjectData(unitId)?.name);\n\n        let globalDebugText = \"\";\n\n        this.missions.forEach((mission) => {\n            this.logger(\n                `Mission ${mission.getUniqueName()}: ${Object.entries(unitsInMission(mission.getUnitIds()))\n                    .map(([unitName, count]) => `${unitName} x ${count}`)\n                    .join(\", \")}`,\n            );\n            const missionDebugText = mission.getGlobalDebugText();\n            if (missionDebugText) {\n                globalDebugText += mission.getUniqueName() + \": \" + missionDebugText + \"\\n\";\n            }\n        });\n        return globalDebugText;\n    }\n\n    public updateDebugText(actionsApi: ActionsApi) {\n        this.missions.forEach((mission) => {\n            mission\n                .getUnitIds()\n                .forEach((unitId) => actionsApi.setUnitDebugText(unitId, `${unitId}: ${mission.getUniqueName()}`));\n        });\n    }\n\n    public getMissions() {\n        return this.missions;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/missions/attackMission.ts",
    "content": "import { GameApi, ObjectType, PlayerData, UnitData, Vector2 } from \"../../../../game-api\";\nimport { CombatSquad } from \"./squads/combatSquad\";\nimport { Mission, MissionAction, disbandMission, noop, requestUnits } from \"../mission\";\nimport { MatchAwareness } from \"../../awareness\";\nimport { MissionController } from \"../missionController\";\nimport { RetreatMission } from \"./retreatMission\";\nimport { DebugLogger, isOwnedByNeutral, maxBy } from \"../../common/utils\";\nimport { manageMoveMicro } from \"./squads/common\";\nimport { MissionContext, SupabotContext } from \"../../common/context\";\nimport { UnitComposition } from \"../../../strategy/strategy\";\nimport { SideComposition } from \"../../../strategy/compositionUtils\";\n\nexport enum AttackFailReason {\n    NoTargets = \"NoTargets\",\n    DefenceTooStrong = \"DefenceTooStrong\",\n    UnableToAcquireUnits = \"UnableToAcquireUnits\",\n    OutOfUnits = \"OutOfUnits\",\n}\n\nenum AttackMissionState {\n    Preparing = 0,\n    Attacking = 1,\n    Retreating = 2,\n}\n\nconst NO_TARGET_RETARGET_TICKS = 300;\nconst NO_TARGET_IDLE_TIMEOUT_TICKS = 600;\n\nconst ATTACK_MISSION_PRIORITY_RAMP = 1.01;\nconst ATTACK_MISSION_MAX_PRIORITY = 50;\n// While preparing the squad, how many ticks to wait before dropping one unit from the desired squad size. If the squad size drops below the minimum, the attack mission is aborted.\nconst REQUESTED_UNIT_COUNT_DECAY_TICKS = 120;\n\n/**\n * A mission that tries to attack a certain area.\n */\nexport class AttackMission extends Mission<AttackFailReason> {\n    private squad: CombatSquad;\n\n    private lastTargetSeenAt = 0;\n    private hasPickedNewTarget: boolean = false;\n\n    private state: AttackMissionState = AttackMissionState.Preparing;\n    private requestedUnitCount: number;\n    private lastRequestedUnitCountDecayAt: number | null = null;\n\n    constructor(\n        uniqueName: string,\n        private priority: number,\n        rallyArea: Vector2,\n        private attackArea: Vector2,\n        private radius: number,\n        private composition: SideComposition,\n        logger: DebugLogger,\n    ) {\n        super(uniqueName, logger);\n        this.squad = new CombatSquad(rallyArea, attackArea, radius);\n        this.requestedUnitCount = composition.maximumUnits;\n    }\n\n    _onAiUpdate(context: MissionContext): MissionAction {\n        switch (this.state) {\n            case AttackMissionState.Preparing:\n                return this.handlePreparingState(context);\n            case AttackMissionState.Attacking:\n                return this.handleAttackingState(context);\n            case AttackMissionState.Retreating:\n                return this.handleRetreatingState(context);\n        }\n    }\n\n    private handlePreparingState(context: MissionContext) {\n        const { game } = context;\n        this.decayDesiredCompositionIfNeeded(game);\n        if (this.requestedUnitCount < this.composition.minimumUnits) {\n            return disbandMission(AttackFailReason.UnableToAcquireUnits);\n        }\n\n        const desiredComposition = this.getDesiredComposition();\n        const missingUnits = this.getMissingUnits(game, desiredComposition);\n        if (missingUnits.length > 0) {\n            this.priority = Math.min(this.priority * ATTACK_MISSION_PRIORITY_RAMP, ATTACK_MISSION_MAX_PRIORITY);\n            // distribute the priority among the amount of missing units of each type\n            const totalMissingUnits = missingUnits.reduce((sum, [, numMissing]) => sum + numMissing, 0);\n            const unitPriorities = Object.fromEntries(\n                missingUnits.map(([unitName, numMissing]) => [\n                    unitName,\n                    (this.priority * numMissing) / totalMissingUnits,\n                ]),\n            );\n            return requestUnits(unitPriorities);\n        } else {\n            this.priority = ATTACK_MISSION_INITIAL_PRIORITY;\n            this.state = AttackMissionState.Attacking;\n            return noop();\n        }\n    }\n\n    private handleAttackingState(context: MissionContext) {\n        const { game, matchAwareness, actionBatcher } = context;\n        const playerData = game.getPlayerData(context.player.name);\n        if (this.getUnitIds().length === 0) {\n            // TODO: disband directly (we no longer retreat when losing)\n            this.state = AttackMissionState.Retreating;\n            return noop();\n        }\n\n        const foundTargets = matchAwareness\n            .getHostilesNearPoint2d(this.attackArea, this.radius)\n            .map((unit) => game.getUnitData(unit.unitId))\n            .filter((unit) => !isOwnedByNeutral(unit)) as UnitData[];\n\n        const update = this.squad.onAiUpdate(context, this, this.logger);\n\n        if (update.type !== \"noop\") {\n            return update;\n        }\n\n        if (foundTargets.length > 0) {\n            this.lastTargetSeenAt = game.getCurrentTick();\n            this.hasPickedNewTarget = false;\n        } else if (game.getCurrentTick() > this.lastTargetSeenAt + NO_TARGET_IDLE_TIMEOUT_TICKS) {\n            return disbandMission(AttackFailReason.NoTargets);\n        } else if (\n            !this.hasPickedNewTarget &&\n            game.getCurrentTick() > this.lastTargetSeenAt + NO_TARGET_RETARGET_TICKS\n        ) {\n            const newTarget = generateTarget(game, playerData, matchAwareness);\n            if (newTarget) {\n                this.squad.setAttackArea(newTarget);\n                this.hasPickedNewTarget = true;\n            }\n        }\n\n        return noop();\n    }\n\n    private handleRetreatingState(context: MissionContext) {\n        const { game, actionBatcher, matchAwareness } = context;\n        this.getUnits(game).forEach((unitId) => {\n            actionBatcher.push(manageMoveMicro(unitId, matchAwareness.getMainRallyPoint()));\n        });\n        // Note: probably should just disband rather than have a retreating state\n        return disbandMission(AttackFailReason.OutOfUnits);\n    }\n\n    public getGlobalDebugText(): string | undefined {\n        return this.squad.getGlobalDebugText() ?? \"<none>\";\n    }\n\n    public getState() {\n        return this.state;\n    }\n\n    // This mission can give up its units while preparing.\n    public isUnitsLocked(): boolean {\n        return this.state !== AttackMissionState.Preparing;\n    }\n\n    public getPriority() {\n        return this.priority;\n    }\n\n    private decayDesiredCompositionIfNeeded(game: GameApi): void {\n        const currentTick = game.getCurrentTick();\n        if (this.lastRequestedUnitCountDecayAt === null) {\n            this.lastRequestedUnitCountDecayAt = currentTick;\n            return;\n        }\n\n        if (currentTick <= this.lastRequestedUnitCountDecayAt + REQUESTED_UNIT_COUNT_DECAY_TICKS) {\n            return;\n        }\n\n        this.lastRequestedUnitCountDecayAt = currentTick;\n        this.requestedUnitCount--;\n    }\n\n    private getDesiredComposition(): UnitComposition {\n        const compositionWeights = this.composition.composition;\n        const totalWeights = Object.values(compositionWeights).reduce((a, b) => a + b, 0);\n        if (totalWeights <= 0) {\n            return {};\n        }\n\n        return Object.fromEntries(\n            Object.entries(compositionWeights).map(([unitName, weight]) => [\n                unitName,\n                Math.round((weight * this.requestedUnitCount) / totalWeights),\n            ]),\n        );\n    }\n}\n\n// Calculates the weight for initiating an attack on the position of a unit or building.\n// This is separate from unit micro; the squad will be ordered to attack in the vicinity of the point.\nconst getTargetWeight: (unitData: UnitData, tryFocusHarvester: boolean) => number = (unitData, tryFocusHarvester) => {\n    if (tryFocusHarvester && unitData.rules.harvester) {\n        return 100000;\n    } else if (unitData.type as any === ObjectType.Building) {\n        return unitData.maxHitPoints * 10;\n    } else {\n        return unitData.maxHitPoints;\n    }\n};\n\nfunction generateTarget(\n    gameApi: GameApi,\n    playerData: PlayerData,\n    matchAwareness: MatchAwareness,\n    includeBaseLocations: boolean = false,\n): Vector2 | null {\n    // Randomly decide between harvester and base.\n    try {\n        const tryFocusHarvester = gameApi.generateRandomInt(0, 1) === 0;\n        const enemyUnits = gameApi\n            .getVisibleUnits(playerData.name, \"enemy\")\n            .map((unitId) => gameApi.getUnitData(unitId))\n            .filter((u) => !!u && gameApi.getPlayerData(u.owner).isCombatant) as UnitData[];\n\n        const maxUnit = maxBy(enemyUnits, (u) => getTargetWeight(u, tryFocusHarvester));\n        if (maxUnit) {\n            return new Vector2(maxUnit.tile.rx, maxUnit.tile.ry);\n        }\n        if (includeBaseLocations) {\n            const mapApi = gameApi.mapApi;\n            const enemyPlayers = gameApi\n                .getPlayers()\n                .map((p) => gameApi.getPlayerData(p))\n                .filter((otherPlayer) => !gameApi.areAlliedPlayers(playerData.name, otherPlayer.name));\n\n            const unexploredEnemyLocations = enemyPlayers.filter((otherPlayer) => {\n                const tile = mapApi.getTile(otherPlayer.startLocation.x, otherPlayer.startLocation.y);\n                if (!tile) {\n                    return false;\n                }\n                return !mapApi.isVisibleTile(tile, playerData.name);\n            });\n            if (unexploredEnemyLocations.length > 0) {\n                const idx = gameApi.generateRandomInt(0, unexploredEnemyLocations.length - 1);\n                return unexploredEnemyLocations[idx].startLocation;\n            }\n        }\n    } catch (err) {\n        // There's a crash here when accessing a building that got destroyed. Will catch and ignore or now.\n        return null;\n    }\n    return null;\n}\n\n// Number of ticks between attacking visible targets.\nconst VISIBLE_TARGET_ATTACK_COOLDOWN_TICKS = 60;\n\n// Number of ticks between attacking \"bases\" (enemy starting locations).\nconst BASE_ATTACK_COOLDOWN_TICKS = 600;\n\nconst ATTACK_MISSION_INITIAL_PRIORITY = 1;\n\nexport class AttackMissionFactory {\n    constructor(private lastAttackAt: number = -VISIBLE_TARGET_ATTACK_COOLDOWN_TICKS) {}\n\n    getName(): string {\n        return \"AttackMissionFactory\";\n    }\n\n    maybeCreateMissions(\n        context: SupabotContext,\n        missionController: MissionController,\n        logger: DebugLogger,\n        composition: SideComposition,\n    ): void {\n        const { game, matchAwareness } = context;\n        const playerData = game.getPlayerData(context.player.name);\n        if (!composition) {\n            return;\n        }\n\n        if (game.getCurrentTick() < this.lastAttackAt + VISIBLE_TARGET_ATTACK_COOLDOWN_TICKS) {\n            return;\n        }\n\n        // Limit concurrent preparing attacks to 2.\n        const preparingCount = missionController\n            .getMissions()\n            .filter(\n                (mission): mission is AttackMission =>\n                    mission instanceof AttackMission && mission.getState() === AttackMissionState.Preparing,\n            ).length;\n        if (preparingCount >= 2) {\n            return;\n        }\n\n        const attackRadius = 10;\n\n        const includeEnemyBases = game.getCurrentTick() > this.lastAttackAt + BASE_ATTACK_COOLDOWN_TICKS;\n\n        const attackArea = generateTarget(game, playerData, matchAwareness, includeEnemyBases);\n\n        if (!attackArea) {\n            return;\n        }\n\n        const squadName = \"attack_\" + game.getCurrentTick();\n\n        const tryAttack = missionController.addMission(\n            new AttackMission(\n                squadName,\n                ATTACK_MISSION_INITIAL_PRIORITY,\n                matchAwareness.getMainRallyPoint(),\n                attackArea,\n                attackRadius,\n                composition,\n                logger,\n            ).withOnFinish((unitIds, reason) => {\n                logger(\n                    `Attack ${squadName} (${JSON.stringify(composition)}) with ${\n                        unitIds.length\n                    } units finished with reason: ${reason}`,\n                );\n                missionController.addMission(\n                    new RetreatMission(\n                        \"retreat-from-\" + squadName + game.getCurrentTick(),\n                        matchAwareness.getMainRallyPoint(),\n                        unitIds,\n                        logger,\n                    ),\n                );\n            }),\n        );\n        if (tryAttack) {\n            this.lastAttackAt = game.getCurrentTick();\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/missions/baseBuildingMission.ts",
    "content": "import { GameApi, PlayerData, QueueType, TechnoRules } from \"../../../../game-api\";\nimport { MissionContext } from \"../../common/context\";\nimport { DebugLogger, maxBy } from \"../../common/utils\";\nimport { buildStructureAtLocation, Mission, MissionAction, noop } from \"../mission\";\nimport { GlobalThreat } from \"../../threat/threat\";\nimport {\n    BUILDING_NAME_TO_RULES,\n    DEFAULT_BUILDING_PRIORITY,\n    getDefaultPlacementLocation,\n} from \"../../building/buildingRules\";\nimport { queueTypeToName } from \"../../building/queueController\";\n\n// Legacy mission encompassing the old \"build queue\" logic.\nexport class BaseBuildingMission extends Mission {\n    constructor(\n        private queueType: QueueType,\n        logger: DebugLogger,\n    ) {\n        super(`building-mission-${queueTypeToName(queueType)}`, logger);\n    }\n\n    _onAiUpdate(context: MissionContext): MissionAction {\n        const options = context.player.production.getAvailableObjects(this.queueType);\n        const playerData = context.game.getPlayerData(context.player.name);\n        if (options.length === 0) {\n            return noop();\n        }\n\n        const { game, matchAwareness } = context;\n        const threatCache = matchAwareness.getThreatCache();\n\n        const optionWithPriority = options.map((option) => {\n            return {\n                option,\n                priority: this.getPriorityForBuildingOption(option, game, playerData, threatCache),\n            };\n        });\n\n        const bestOption = maxBy(optionWithPriority, (option) => option.priority);\n\n        if (!bestOption || bestOption.priority === 0) {\n            return noop();\n        }\n\n        const bestLocation = this.getBestLocationForStructure(game, playerData, bestOption.option);\n\n        if (!bestLocation) {\n            return noop();\n        }\n\n        return buildStructureAtLocation(bestOption.option.name, bestOption.priority, bestLocation.rx, bestLocation.ry);\n    }\n\n    getGlobalDebugText(): string | undefined {\n        return undefined;\n    }\n\n    getPriority(): number {\n        return 0;\n    }\n\n    private getPriorityForBuildingOption(\n        option: TechnoRules,\n        game: GameApi,\n        playerStatus: PlayerData,\n        threatCache: GlobalThreat | null,\n    ) {\n        if (BUILDING_NAME_TO_RULES.has(option.name)) {\n            let logic = BUILDING_NAME_TO_RULES.get(option.name)!;\n            return logic.getPriority(game, playerStatus, option, threatCache);\n        } else {\n            // Fallback priority when there are no rules.\n            return (\n                DEFAULT_BUILDING_PRIORITY - game.getVisibleUnits(playerStatus.name, \"self\", (r) => r == option).length\n            );\n        }\n    }\n\n    private getBestLocationForStructure(\n        game: GameApi,\n        playerData: PlayerData,\n        objectReady: TechnoRules,\n    ): { rx: number; ry: number } | undefined {\n        if (BUILDING_NAME_TO_RULES.has(objectReady.name)) {\n            let logic = BUILDING_NAME_TO_RULES.get(objectReady.name)!;\n            return logic.getPlacementLocation(game, playerData, objectReady);\n        } else {\n            // fallback placement logic\n            return getDefaultPlacementLocation(game, playerData, playerData.startLocation, objectReady);\n        }\n    }\n\n    private handleBuildingReady(context: MissionContext, objectReady: TechnoRules) {\n        const { game, player } = context;\n        const { actions: actionsApi } = player;\n        const playerData = game.getPlayerData(player.name);\n        let location: { rx: number; ry: number } | undefined = this.getBestLocationForStructure(\n            game,\n            playerData,\n            objectReady,\n        );\n        if (location !== undefined) {\n            this.logger(\n                `Completed (${queueTypeToName(this.queueType)}): ${objectReady.name}, placing at ${location.rx},${\n                    location.ry\n                }`,\n            );\n            actionsApi.placeBuilding(objectReady.name, location.rx, location.ry);\n        } else {\n            this.logger(`Completed (${queueTypeToName(this.queueType)}): ${objectReady.name} but nowhere to place it`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/missions/defenceMission.ts",
    "content": "import { ActionsApi, BotContext, GameApi, GameObjectData, PlayerData, UnitData, Vector2 } from \"../../../../game-api\";\nimport { MatchAwareness } from \"../../awareness\";\nimport { MissionController } from \"../missionController\";\nimport { Mission, MissionAction, grabCombatants, noop, releaseUnits, requestUnits } from \"../mission\";\nimport { CombatSquad } from \"./squads/combatSquad\";\nimport { DebugLogger, isOwnedByNeutral, toVector2 } from \"../../common/utils\";\nimport { ActionBatcher } from \"../actionBatcher\";\nimport { MissionContext, SupabotContext } from \"../../common/context\";\n\nexport const MAX_PRIORITY = 100;\nexport const PRIORITY_INCREASE_PER_TICK_RATIO = 1.025;\n\n/**\n * A mission that tries to defend a certain area.\n */\nexport class DefenceMission extends Mission<CombatSquad> {\n    private squad: CombatSquad;\n\n    constructor(\n        uniqueName: string,\n        private priority: number,\n        rallyArea: Vector2,\n        private defenceArea: Vector2,\n        private radius: number,\n        logger: DebugLogger,\n    ) {\n        super(uniqueName, logger);\n        this.squad = new CombatSquad(rallyArea, defenceArea, radius);\n    }\n\n    _onAiUpdate(context: MissionContext): MissionAction {\n        const { game, matchAwareness } = context;\n        // Dispatch missions.\n        const foundTargets = matchAwareness\n            .getHostilesNearPoint2d(this.defenceArea, this.radius)\n            .map((unit) => game.getUnitData(unit.unitId))\n            .filter((unit) => !isOwnedByNeutral(unit)) as UnitData[];\n\n        const update = this.squad.onAiUpdate(context, this, this.logger);\n\n        if (update.type !== \"noop\") {\n            return update;\n        }\n\n        if (foundTargets.length === 0) {\n            this.priority = 0;\n            if (this.getUnitIds().length > 0) {\n                this.logger(`(Defence Mission ${this.getUniqueName()}): No targets found, releasing units.`);\n                return releaseUnits(this.getUnitIds());\n            } else {\n                return noop();\n            }\n        }\n        const targetUnit = foundTargets[0];\n        this.logger(\n            `(Defence Mission ${this.getUniqueName()}): Focused on target ${targetUnit?.name} (${\n                foundTargets.length\n            } found in area ${this.radius})`,\n        );\n        this.squad.setAttackArea(new Vector2(foundTargets[0].tile.rx, foundTargets[0].tile.ry));\n        this.priority = MAX_PRIORITY;\n        return grabCombatants(this.defenceArea, this.priority);\n    }\n\n    public getGlobalDebugText(): string | undefined {\n        return this.squad.getGlobalDebugText() ?? \"<none>\";\n    }\n\n    public getPriority() {\n        return this.priority;\n    }\n}\n\nconst DEFENCE_CHECK_TICKS = 30;\n\n// Starting radius around the player's base to trigger defense.\nconst DEFENCE_STARTING_RADIUS = 6;\n// Every game tick, we increase the defendable area by this amount.\nconst DEFENCE_RADIUS_INCREASE_PER_GAME_TICK = 0.0001;\n\nexport class DefenceMissionFactory {\n    private lastDefenceCheckAt = 0;\n\n    constructor() {}\n\n    getName(): string {\n        return \"DefenceMissionFactory\";\n    }\n\n    maybeCreateMissions(context: SupabotContext, missionController: MissionController, logger: DebugLogger): void {\n        const { game, matchAwareness } = context;\n        if (game.getCurrentTick() < this.lastDefenceCheckAt + DEFENCE_CHECK_TICKS) {\n            return;\n        }\n        this.lastDefenceCheckAt = game.getCurrentTick();\n\n        const defendablePoints = this.getDefendablePoints(context);\n\n        const defendableRadius =\n            DEFENCE_STARTING_RADIUS + DEFENCE_RADIUS_INCREASE_PER_GAME_TICK * game.getCurrentTick();\n        for (const defendablePoint of defendablePoints) {\n            const enemiesNearPoint = matchAwareness\n                .getHostilesNearPoint2d(defendablePoint, defendableRadius)\n                .map((unit) => game.getUnitData(unit.unitId))\n                .filter((unit) => !isOwnedByNeutral(unit)) as UnitData[];\n\n            if (enemiesNearPoint.length > 0) {\n                logger(\n                    `Starting defence mission, ${\n                        enemiesNearPoint.length\n                    } found in radius ${defendableRadius} (tick ${game.getCurrentTick()})`,\n                );\n                missionController.addMission(\n                    new DefenceMission(\n                        `globalDefence.${defendablePoint.x}.${defendablePoint.y}`,\n                        10,\n                        matchAwareness.getMainRallyPoint(),\n                        defendablePoint,\n                        defendableRadius,\n                        logger,\n                    ),\n                );\n            }\n        }\n    }\n\n    private getDefendablePoints(context: SupabotContext) {\n        const { game, player } = context;\n        return game\n            .getVisibleUnits(player.name, \"self\", (r) => r.constructionYard || r.name === \"AMCV\" || r.name === \"SMCV\")\n            .map((unitId) => game.getGameObjectData(unitId))\n            .filter((unit): unit is GameObjectData => unit != null)\n            .map((unit) => toVector2(unit.tile));\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/missions/engineerMission.ts",
    "content": "import { GameApi, GameObjectData, OrderType, SideType, SpeedType, UnitData } from \"../../../../game-api\";\nimport {\n    Mission,\n    MissionAction,\n    disbandMission,\n    noop,\n    requestUnits,\n    requestUnitsWithSamePriority,\n} from \"../mission\";\nimport { MissionController } from \"../missionController\";\nimport { DebugLogger, toPathNode, toVector2 } from \"../../common/utils\";\nimport { computeAdjacentRect, getAdjacentTiles } from \"../../common/tileUtils\";\nimport { MissionContext, SupabotContext } from \"../../common/context\";\nimport { UnitComposition } from \"../../../strategy/strategy\";\n\nconst CAPTURE_COOLDOWN_TICKS = 30;\n\nenum EngineerMissionState {\n    Preparing = 0,\n    Capturing = 1,\n}\n\nconst LOST_ENGINEER = \"lost_engineer\";\nconst NO_PATH = \"no_path\";\n\n/**\n * A mission that tries to send an engineer into a building (e.g. to capture tech building or repair bridge)\n */\nexport class EngineerMission extends Mission {\n    private state = EngineerMissionState.Preparing;\n    private lastCaptureAttemptTick = -1;\n\n    constructor(\n        uniqueName: string,\n        private priority: number,\n        private captureTargetId: number,\n        private escortLevel: number,\n        logger: DebugLogger,\n    ) {\n        super(uniqueName, logger);\n    }\n\n    get targetId() {\n        return this.captureTargetId;\n    }\n\n    public _onAiUpdate(context: MissionContext): MissionAction {\n        const { game } = context;\n        const actionsApi = context.player.actions;\n        const playerData = game.getPlayerData(context.player.name);\n        const engineers = this.getUnitsOfTypes(game, ...[\"SENGINEER\", \"ENGINEER\"]);\n\n        const target = game.getGameObjectData(this.captureTargetId);\n        if (!target || target.owner === playerData.name) {\n            // Target gone or already captured, disband.\n            return disbandMission();\n        }\n\n        if (engineers.length === 0 && this.state === EngineerMissionState.Capturing) {\n            // Engineer died and we already tried to capture\n            return disbandMission(LOST_ENGINEER);\n        }\n\n        if (this.state === EngineerMissionState.Preparing) {\n            const composition: UnitComposition = {};\n            switch (playerData.country!.side) {\n                case SideType.Nod:\n                    composition[\"SENGINEER\"] = 1;\n                    composition[\"DOG\"] = Math.max(0, this.escortLevel - 1); // 0, 1, 2\n                    composition[\"HTNK\"] = Math.max(0, this.escortLevel - 2); // 0, 0, 1\n                    break;\n                case SideType.GDI:\n                    composition[\"ENGINEER\"] = 1;\n                    composition[\"ADOG\"] = Math.max(0, this.escortLevel - 1); // 0, 1, 2\n                    composition[\"MTNK\"] = Math.max(0, this.escortLevel - 2); // 0, 0, 1\n                    break;\n            }\n            const missingUnits = this.getMissingUnits(game, composition);\n            if (missingUnits.length > 0) {\n                return requestUnitsWithSamePriority(\n                    missingUnits.map(([unitName]) => unitName),\n                    this.priority,\n                );\n            }\n            this.state = EngineerMissionState.Capturing;\n        }\n\n        if (\n            this.state === EngineerMissionState.Capturing &&\n            game.getCurrentTick() > this.lastCaptureAttemptTick + CAPTURE_COOLDOWN_TICKS\n        ) {\n            const engineer = engineers[0];\n            if (!canReachStructure(game, engineer, target)) {\n                return disbandMission(NO_PATH);\n            }\n            actionsApi.orderUnits([engineer.id], OrderType.Capture, this.captureTargetId);\n            const escortUnits = this.getUnitsOfTypes(game, \"DOG\", \"HTNK\", \"ADOG\", \"MTNK\");\n            if (escortUnits.length > 0) {\n                actionsApi.orderUnits(\n                    escortUnits.map((u) => u.id),\n                    OrderType.Guard,\n                    engineer.id,\n                );\n            }\n            // Add a cooldown to deploy attempts.\n            this.lastCaptureAttemptTick = game.getCurrentTick();\n        }\n        return noop();\n    }\n\n    public getGlobalDebugText(): string | undefined {\n        return undefined;\n    }\n\n    public getPriority() {\n        return this.priority;\n    }\n}\n\nfunction canReachStructure(gameApi: GameApi, engineer: UnitData, target: GameObjectData) {\n    const reachabilityMap = gameApi.map.getReachabilityMap(SpeedType.Foot, true);\n    // unfortunately we have to test tiles around the target, because the target blocks pathing\n    const range = computeAdjacentRect(toVector2(target.tile), target.foundation, 1);\n    const adjacentTiles = getAdjacentTiles(gameApi, range, false);\n    for (const tile of adjacentTiles) {\n        if (\n            reachabilityMap.isReachable(toPathNode(engineer.tile, engineer.onBridge ?? false) as any, toPathNode(tile, false) as any)\n        ) {\n            return true;\n        }\n    }\n    return false;\n}\n\nconst TECH_CHECK_INTERVAL_TICKS = 300;\nconst MAX_CAPTURE_ATTEMPT_COUNT = 3;\n\nexport class EngineerMissionFactory {\n    private lastCheckAt = 0;\n    private lostEngineerCounts: { [buildingId: number]: number } = {};\n    private noPathCounts: { [buildingId: number]: number } = {};\n\n    getName(): string {\n        return \"EngineerMissionFactory\";\n    }\n\n    maybeCreateMissions(context: SupabotContext, missionController: MissionController, logger: DebugLogger): void {\n        const { game } = context;\n        const playerData = game.getPlayerData(context.player.name);\n        if (!(game.getCurrentTick() > this.lastCheckAt + TECH_CHECK_INTERVAL_TICKS)) {\n            return;\n        }\n        this.lastCheckAt = game.getCurrentTick();\n        const eligibleTechBuildings = game.getVisibleUnits(\n            playerData.name,\n            \"hostile\",\n            (r) => r.capturable && r.produceCashAmount > 0,\n        );\n\n        eligibleTechBuildings.forEach((techBuildingId) => {\n            if (\n                this.lostEngineerCounts[techBuildingId] >= MAX_CAPTURE_ATTEMPT_COUNT ||\n                this.noPathCounts[techBuildingId] >= MAX_CAPTURE_ATTEMPT_COUNT\n            ) {\n                return;\n            }\n            const escortLevel = (this.lostEngineerCounts[techBuildingId] ?? 0) + 1;\n            missionController.addMission(\n                new EngineerMission(\"capture-\" + techBuildingId, 100, techBuildingId, escortLevel, logger).withOnFinish(\n                    (unitIds, reason) => {\n                        if (reason === LOST_ENGINEER) {\n                            this.lostEngineerCounts[techBuildingId] =\n                                (this.lostEngineerCounts[techBuildingId] ?? 0) + 1;\n                        } else if (reason === NO_PATH) {\n                            this.noPathCounts[techBuildingId] = (this.noPathCounts[techBuildingId] ?? 0) + 1;\n                        }\n                    },\n                ),\n            );\n        });\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/missions/expansionMission.ts",
    "content": "import {\n    ActionsApi,\n    BotContext,\n    Box2,\n    GameApi,\n    GameMath,\n    GameObjectData,\n    ObjectType,\n    OrderType,\n    PlayerData,\n    Rectangle,\n    Tile,\n    UnitData,\n    Vector2,\n} from \"../../../../game-api\";\nimport {\n    Mission,\n    MissionAction,\n    disbandMission,\n    noop,\n    requestSpecificUnits,\n    requestUnits,\n    requestUnitsWithSamePriority,\n} from \"../mission\";\nimport { MatchAwareness } from \"../../awareness\";\nimport { MissionController } from \"../missionController\";\nimport { DebugLogger, isTechnoRulesObject, maxBy, minBy, toPathNode, toVector2 } from \"../../common/utils\";\nimport { ActionBatcher } from \"../actionBatcher\";\nimport { getCachedTechnoRules } from \"../../common/rulesCache\";\nimport { canBuildOnTile } from \"../../common/tileUtils\";\nimport { MissionContext, SupabotContext } from \"../../common/context\";\n\nconst ORDER_COOLDOWN_TICKS = 60;\n\nconst mcvTypes = [\"AMCV\", \"SMCV\"];\n\nconst CONYARD_SCAN_DISTANCE = 15; // distance to check a conyard is already in place\nconst CONYARD_DEPLOY_SCAN_DISTANCE = 10; // distance to check for a deployable location\nconst CONYARD_DEPLOY_DISTANCE = 5;\n\n/**\n * A mission that tries to create an MCV (if it doesn't exist) and deploy it somewhere it can be deployed.\n */\nexport class ExpansionMission extends Mission {\n    private destination: Vector2 | null = null;\n    private lastOrderAt: number | null = null;\n\n    private lastOrderDeploy = false;\n    private deployAttempts = 0;\n    private static readonly MAX_DEPLOY_ATTEMPTS = 8;\n\n    constructor(\n        uniqueName: string,\n        private priority: number,\n        private selectedMcvId: number | null,\n        private candidates: Vector2[],\n        logger: DebugLogger,\n    ) {\n        super(uniqueName, logger);\n        if (candidates.length === 1) {\n            this.destination = candidates[0];\n        } else if (candidates.length === 0) {\n            throw new Error(\"ExpansionMission requires at least one candidate location\");\n        }\n    }\n\n    public _onAiUpdate(context: MissionContext): MissionAction {\n        const { game, matchAwareness, actionBatcher } = context;\n        const actionsApi = context.player.actions;\n        const playerData = context.game.getPlayerData(context.player.name);\n        const mcvs = this.getUnitsOfTypes(game, ...mcvTypes);\n        if (mcvs.length === 0) {\n            // Perhaps we deployed already (or the unit was destroyed), end the mission.\n            if (this.lastOrderAt !== null) {\n                return disbandMission();\n            }\n            // We need an mcv!\n            if (this.selectedMcvId && !!game.getUnitData(this.selectedMcvId)) {\n                return requestSpecificUnits([this.selectedMcvId], this.priority);\n            }\n            return requestUnitsWithSamePriority(mcvTypes, this.priority);\n        }\n\n        // use the highest-hp MCV\n        const selectedMcvUnit = maxBy(mcvs, (mcv) => mcv.hitPoints)!;\n        this.selectedMcvId = selectedMcvUnit?.id ?? null;\n\n        if (this.destination) {\n            return this.moveMcvToDestination(\n                game,\n                actionsApi,\n                playerData,\n                matchAwareness,\n                actionBatcher,\n                selectedMcvUnit,\n            );\n        } else {\n            const reachableCandidates = this.candidates\n                .map((candidate) => game.mapApi.getTile(candidate.x, candidate.y))\n                .filter((t): t is Tile => !!t)\n                .filter((t) => {\n                    try {\n                        const path = game.mapApi.findPath(\n                            selectedMcvUnit.rules.speedType!,\n                            toPathNode(selectedMcvUnit.tile, !!selectedMcvUnit.onBridge),\n                            toPathNode(t, false),\n                            { bestEffort: true, maxExpandedNodes: 1500 },\n                        );\n                        return path.length > 0;\n                    } catch (_err) {\n                        return false;\n                    }\n                });\n            const closestReachableCandidate = minBy(reachableCandidates, (candidate) => {\n                return toVector2(selectedMcvUnit.tile).distanceTo(toVector2(candidate));\n            });\n            if (!closestReachableCandidate) {\n                // can't reach any candidates yet, return to start location\n                this.destination = playerData.startLocation;\n            } else {\n                this.destination = toVector2(closestReachableCandidate);\n            }\n            return noop();\n        }\n    }\n\n    public moveMcvToDestination(\n        gameApi: GameApi,\n        actionsApi: ActionsApi,\n        playerData: PlayerData,\n        matchAwareness: MatchAwareness,\n        actionBatcher: ActionBatcher,\n        mcv: UnitData,\n    ) {\n        if (!this.destination) {\n            return noop();\n        }\n        // if there's a conyard near the destination, we're done.\n        const conYards = gameApi\n            .getUnitsInArea(\n                new Box2(\n                    this.destination.clone().subScalar(CONYARD_SCAN_DISTANCE),\n                    this.destination.clone().addScalar(CONYARD_SCAN_DISTANCE),\n                ),\n            )\n            .map((id) => getCachedTechnoRules(gameApi, id))\n            .filter((r) => r?.constructionYard);\n        if (conYards.length > 0) {\n            return disbandMission();\n        }\n        const isClose = toVector2(mcv.tile).distanceTo(this.destination) <= CONYARD_DEPLOY_DISTANCE;\n        const canOrder = !this.lastOrderAt || gameApi.getCurrentTick() > this.lastOrderAt + ORDER_COOLDOWN_TICKS;\n        if (!canOrder) {\n            return noop();\n        }\n        if (isClose) {\n            this.deployAttempts++;\n            // If too many failed attempts at this location, find a new deployable spot\n            if (this.deployAttempts > ExpansionMission.MAX_DEPLOY_ATTEMPTS) {\n                const deployableLocations = findDeployableLocations(\n                    playerData.name,\n                    gameApi,\n                    {\n                        x: mcv.tile.rx - CONYARD_DEPLOY_SCAN_DISTANCE,\n                        y: mcv.tile.ry - CONYARD_DEPLOY_SCAN_DISTANCE,\n                        width: CONYARD_DEPLOY_SCAN_DISTANCE * 2,\n                        height: CONYARD_DEPLOY_SCAN_DISTANCE * 2,\n                    },\n                    mcv.rules.deploysInto,\n                );\n                const bestLocation = minBy(deployableLocations, (d) => toVector2(mcv.tile).distanceToSquared(d));\n                if (bestLocation) {\n                    // Update destination to the new deployable location\n                    this.destination = bestLocation.clone();\n                    this.deployAttempts = 0;\n                    this.lastOrderDeploy = false;\n                    actionsApi.orderUnits([mcv.id], OrderType.Move, bestLocation.x, bestLocation.y);\n                } else {\n                    // No deployable location found at all, scatter and retry\n                    actionsApi.orderUnits([mcv.id], OrderType.Scatter);\n                    this.deployAttempts = 0;\n                }\n                this.lastOrderAt = gameApi.getCurrentTick();\n                return noop();\n            }\n\n            if (!this.lastOrderDeploy) {\n                actionsApi.orderUnits([mcv.id], OrderType.DeploySelected);\n                this.lastOrderDeploy = true;\n            } else {\n                // Deploy failed, find a nearby clear spot and move there\n                const deployableLocations = findDeployableLocations(\n                    playerData.name,\n                    gameApi,\n                    {\n                        x: mcv.tile.rx - CONYARD_DEPLOY_SCAN_DISTANCE,\n                        y: mcv.tile.ry - CONYARD_DEPLOY_SCAN_DISTANCE,\n                        width: CONYARD_DEPLOY_SCAN_DISTANCE * 2,\n                        height: CONYARD_DEPLOY_SCAN_DISTANCE * 2,\n                    },\n                    mcv.rules.deploysInto,\n                );\n                const bestLocation = minBy(deployableLocations, (d) => toVector2(mcv.tile).distanceToSquared(d));\n\n                if (bestLocation) {\n                    // Update destination so next cycle we move toward this new spot\n                    this.destination = bestLocation.clone();\n                    actionsApi.orderUnits([mcv.id], OrderType.Move, bestLocation.x, bestLocation.y);\n                } else {\n                    actionsApi.orderUnits([mcv.id], OrderType.Scatter);\n                }\n                this.lastOrderDeploy = false;\n            }\n            this.lastOrderAt = gameApi.getCurrentTick();\n        } else if (!isClose) {\n            // find a 4x4 area near the destination that is clear.\n            const rx = this.destination.x;\n            const ry = this.destination.y;\n            actionsApi.orderUnits([mcv.id], OrderType.Move, rx, ry);\n            this.lastOrderAt = gameApi.getCurrentTick();\n        }\n        return noop();\n    }\n\n    public getGlobalDebugText(): string | undefined {\n        return `Expand with MCV ${this.selectedMcvId}`;\n    }\n\n    public getPriority() {\n        return this.priority;\n    }\n}\n\nfunction findDeployableLocations(playerName: string, gameApi: GameApi, rectangle: Rectangle, rules: string) {\n    const tiles = gameApi.map.getTilesInRect(rectangle);\n    const { foundation, foundationCenter } = gameApi.getBuildingPlacementData(rules);\n\n    if (foundation.width !== foundation.height) {\n        throw new Error(\"only implemented for square foundations\");\n    }\n\n    const grid: number[][] = new Array(rectangle.width).fill(() => 0).map(() => new Array(rectangle.height).fill(0));\n\n    // fill tiles that are not buildable\n    for (const tile of tiles) {\n        const gridX = tile.rx - rectangle.x;\n        const gridY = tile.ry - rectangle.y;\n        if (canBuildOnTile(tile, gameApi)) {\n            grid[gridX][gridY] = 1;\n        }\n    }\n\n    // we have to start from the bottom-right and calculate backwards\n    for (let x = rectangle.width - 2; x >= 0; --x) {\n        for (let y = rectangle.height - 2; y >= 0; --y) {\n            if (grid[x][y] === 0) {\n                continue;\n            }\n            const right = x < rectangle.width - 1 ? grid[x + 1][y] : 0;\n            const bottom = y < rectangle.height - 1 ? grid[y][y + 1] : 0;\n            grid[x][y] = Math.min(right + 1, bottom + 1);\n        }\n    }\n\n    const locations: Vector2[] = [];\n\n    for (const tile of tiles) {\n        const gridX = tile.rx - rectangle.x;\n        const gridY = tile.ry - rectangle.y;\n        if (grid[gridX][gridY] >= foundation.width && grid[gridX][gridY] >= foundation.height) {\n            locations.push(toVector2(tile).add(foundationCenter));\n        }\n    }\n\n    return locations;\n}\n\nexport class PackConyardMission extends Mission {\n    constructor(\n        uniqueName: string,\n        private conyardId: number,\n        logger: DebugLogger,\n    ) {\n        super(uniqueName, logger);\n    }\n\n    public _onAiUpdate(context: MissionContext): MissionAction {\n        const { game } = context;\n        const actionsApi = context.player.actions;\n        const conyardOrMcv = game.getGameObjectData(this.conyardId);\n        if (!conyardOrMcv) {\n            // maybe it died, or unpacked already\n            return disbandMission();\n        }\n        actionsApi.orderUnits([this.conyardId], OrderType.Move, conyardOrMcv.tile.rx, conyardOrMcv.tile.ry);\n        return noop();\n    }\n\n    public getGlobalDebugText(): string | undefined {\n        return `Pack conyard ${this.conyardId}`;\n    }\n\n    public getPriority() {\n        return 10000;\n    }\n}\n\nconst CONYARD_PACK_COOLDOWN = 15 * 60 * 6; // 6 mins\nconst DO_NOT_EXPAND_BEFORE_TICKS = 15 * 60 * 6; // 6 minutes\n\nexport class ExpansionMissionFactory {\n    constructor(private lastConyardPackAt = Number.MIN_VALUE) {}\n    getName(): string {\n        return \"ExpansionMissionFactory\";\n    }\n\n    maybeCreateMissions(context: SupabotContext, missionController: MissionController, logger: DebugLogger): void {\n        const { game, player, matchAwareness } = context;\n        const playerData = game.getPlayerData(player.name);\n        const mcvs = game.getVisibleUnits(player.name, \"self\", (r) => game.getGeneralRules().baseUnit.includes(r.name));\n        const expandToCandidates = matchAwareness.getNextExpansionCandidates();\n\n        // This is used for deploying the initial MCV.\n        if (game.getCurrentTick() < DO_NOT_EXPAND_BEFORE_TICKS) {\n            mcvs.forEach((mcv) => {\n                missionController.addMission(\n                    new ExpansionMission(\"initial-deploy-mcv-\" + mcv, 100, mcv, [playerData.startLocation], logger),\n                );\n            });\n        } else if (expandToCandidates.length > 0) {\n            mcvs.forEach((mcv) => {\n                missionController.addMission(\n                    new ExpansionMission(\"expansion-mcv-\" + mcv, 100, mcv, expandToCandidates, logger),\n                );\n            });\n        }\n\n        const threatCache = matchAwareness.getThreatCache();\n        if (!expandToCandidates[0] || !threatCache) {\n            return;\n        }\n\n        if (\n            game.getCurrentTick() < DO_NOT_EXPAND_BEFORE_TICKS ||\n            game.getCurrentTick() < this.lastConyardPackAt + CONYARD_PACK_COOLDOWN\n        ) {\n            return;\n        }\n        // TODO: do not pack up if currently producing something from the conyard\n\n        // if we have a war factory and at least 1 refinery, try expand\n        const conYards = game.getVisibleUnits(player.name, \"self\", (r) => r.constructionYard);\n        const warFactories = game.getVisibleUnits(player.name, \"self\", (r) => r.weaponsFactory);\n        const isSafeToExpand = threatCache.totalAvailableAntiGroundFirepower > threatCache.totalOffensiveLandThreat;\n        const refineries = game.getVisibleUnits(player.name, \"self\", (r) => r.refinery);\n        if (conYards.length === 0 || warFactories.length === 0 || refineries.length === 0 || !isSafeToExpand) {\n            return;\n        }\n        const selectedConyard = game.getGameObjectData(conYards[0])!;\n        const refineryNearconyard = game\n            .getUnitsInArea(\n                new Box2(toVector2(selectedConyard.tile).subScalar(10), toVector2(selectedConyard.tile).addScalar(14)),\n            )\n            .map((id) => game.getGameObjectData(id))\n            .filter(isTechnoRulesObject)\n            .filter((obj) => obj.rules.refinery);\n        if (refineryNearconyard.length > 0) {\n            missionController.addMission(\n                new PackConyardMission(\"pack-up-\" + selectedConyard.id, selectedConyard.id, logger),\n            );\n            logger(\"Time to pack the conyard and expand\", false);\n            this.lastConyardPackAt = game.getCurrentTick();\n        } else {\n            logger(\"Not time to pack up, no refinery yet\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/missions/retreatMission.ts",
    "content": "import { DebugLogger } from \"../../common/utils\";\nimport { ActionsApi, BotContext, GameApi, OrderType, PlayerData, Vector2 } from \"../../../../game-api\";\nimport { Mission, MissionAction, disbandMission, requestSpecificUnits } from \"../mission\";\nimport { ActionBatcher } from \"../actionBatcher\";\nimport { MatchAwareness } from \"../../awareness\";\nimport { MissionContext } from \"../../common/context\";\n\nexport class RetreatMission extends Mission {\n    private createdAt: number | null = null;\n\n    constructor(\n        uniqueName: string,\n        private retreatToPoint: Vector2,\n        private withUnitIds: number[],\n        logger: DebugLogger,\n    ) {\n        super(uniqueName, logger);\n    }\n\n    public _onAiUpdate(context: MissionContext): MissionAction {\n        const { game } = context;\n        const actionsApi = context.player.actions;\n        if (!this.createdAt) {\n            this.createdAt = game.getCurrentTick();\n        }\n        if (this.getUnitIds().length > 0) {\n            // Only send the order once we have managed to claim some units.\n            actionsApi.orderUnits(\n                this.getUnitIds(),\n                OrderType.AttackMove,\n                this.retreatToPoint.x,\n                this.retreatToPoint.y,\n            );\n            return disbandMission();\n        }\n        if (this.createdAt && game.getCurrentTick() > this.createdAt + 240) {\n            // Disband automatically after 240 ticks in case we couldn't actually claim any units.\n            return disbandMission();\n        } else {\n            return requestSpecificUnits(this.withUnitIds, 1000);\n        }\n    }\n\n    public getGlobalDebugText(): string | undefined {\n        return `retreat with ${this.withUnitIds.length} units`;\n    }\n\n    public getPriority() {\n        return 100;\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/missions/scoutingMission.ts",
    "content": "import { ActionsApi, BotContext, GameApi, OrderType, PlayerData, Vector2 } from \"../../../../game-api\";\nimport { MatchAwareness } from \"../../awareness\";\nimport {\n    Mission,\n    MissionAction,\n    disbandMission,\n    noop,\n    requestUnits,\n    requestUnitsWithSamePriority,\n} from \"../mission\";\nimport { AttackMission } from \"./attackMission\";\nimport { MissionController } from \"../missionController\";\nimport { DebugLogger } from \"../../common/utils\";\nimport { ActionBatcher } from \"../actionBatcher\";\nimport { getDistanceBetweenTileAndPoint } from \"../../map/map\";\nimport { PrioritisedScoutTarget } from \"../../common/scout\";\nimport { MissionContext, SupabotContext } from \"../../common/context\";\n\nconst SCOUT_MOVE_COOLDOWN_TICKS = 30;\n\n// Max units to spend on a particular scout target.\nconst MAX_ATTEMPTS_PER_TARGET = 5;\n\n// Maximum ticks to spend trying to scout a target *without making progress towards it*.\n// Every time a unit gets closer to the target, the timer refreshes.\nconst MAX_TICKS_PER_TARGET = 600;\n\n/**\n * A mission that tries to scout around the map with a cheap, fast unit (usually attack dogs)\n */\nexport class ScoutingMission extends Mission {\n    private scoutTarget: Vector2 | null = null;\n    private attemptsOnCurrentTarget: number = 0;\n    private scoutTargetRefreshedAt: number = 0;\n    private lastMoveCommandTick: number = 0;\n    private scoutTargetIsPermanent: boolean = false;\n\n    // Minimum distance from a scout to the target.\n    private scoutMinDistance?: number;\n\n    private hadUnit: boolean = false;\n\n    constructor(\n        uniqueName: string,\n        private priority: number,\n        logger: DebugLogger,\n    ) {\n        super(uniqueName, logger);\n    }\n\n    public _onAiUpdate(context: MissionContext): MissionAction {\n        const { game, matchAwareness } = context;\n        const actionsApi = context.player.actions;\n        const playerData = game.getPlayerData(context.player.name);\n        const scoutNames = [\"ADOG\", \"DOG\", \"E1\", \"E2\", \"FV\", \"HTK\"];\n        const scouts = this.getUnitsOfTypes(game, ...scoutNames);\n\n        if ((matchAwareness.getSectorCache().getOverallVisibility() || 0) > 0.9) {\n            return disbandMission();\n        }\n\n        if (scouts.length === 0) {\n            // Count the number of times the scout dies trying to uncover the current scoutTarget.\n            if (this.scoutTarget && this.hadUnit) {\n                this.attemptsOnCurrentTarget++;\n                this.hadUnit = false;\n            }\n            return requestUnitsWithSamePriority(scoutNames, this.priority);\n        } else if (this.scoutTarget) {\n            this.hadUnit = true;\n            if (!this.scoutTargetIsPermanent) {\n                if (this.attemptsOnCurrentTarget > MAX_ATTEMPTS_PER_TARGET) {\n                    this.logger(\n                        `Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too many attempts, moving to next`,\n                    );\n                    this.setScoutTarget(null, 0);\n                    return noop();\n                }\n                if (game.getCurrentTick() > this.scoutTargetRefreshedAt + MAX_TICKS_PER_TARGET) {\n                    this.logger(\n                        `Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too long, moving to next`,\n                    );\n                    this.setScoutTarget(null, 0);\n                    return noop();\n                }\n            }\n            const targetTile = game.mapApi.getTile(this.scoutTarget.x, this.scoutTarget.y);\n            if (!targetTile) {\n                throw new Error(`target tile ${this.scoutTarget.x},${this.scoutTarget.y} does not exist`);\n            }\n            if (game.getCurrentTick() > this.lastMoveCommandTick + SCOUT_MOVE_COOLDOWN_TICKS) {\n                this.lastMoveCommandTick = game.getCurrentTick();\n                scouts.forEach((unit) => {\n                    if (this.scoutTarget) {\n                        actionsApi.orderUnits([unit.id], OrderType.AttackMove, this.scoutTarget.x, this.scoutTarget.y);\n                    }\n                });\n                // Check that a scout is actually moving closer to the target.\n                const distances = scouts.map((unit) => getDistanceBetweenTileAndPoint(unit.tile, this.scoutTarget!));\n                const newMinDistance = Math.min(...distances);\n                if (!this.scoutMinDistance || newMinDistance < this.scoutMinDistance) {\n                    this.logger(\n                        `Scout timeout refreshed because unit moved closer to point (${newMinDistance} < ${this.scoutMinDistance})`,\n                    );\n                    this.scoutTargetRefreshedAt = game.getCurrentTick();\n                    this.scoutMinDistance = newMinDistance;\n                }\n            }\n            if (game.mapApi.isVisibleTile(targetTile, playerData.name)) {\n                this.logger(\n                    `Scout target ${this.scoutTarget.x},${this.scoutTarget.y} successfully scouted, moving to next`,\n                );\n                this.setScoutTarget(null, game.getCurrentTick());\n            }\n        } else {\n            const nextScoutTarget = matchAwareness.getScoutingManager().getNewScoutTarget();\n            if (!nextScoutTarget) {\n                this.logger(`No more scouting targets available, disbanding.`);\n                return disbandMission();\n            }\n            this.setScoutTarget(nextScoutTarget, game.getCurrentTick());\n        }\n        return noop();\n    }\n\n    setScoutTarget(target: PrioritisedScoutTarget | null, currentTick: number) {\n        this.attemptsOnCurrentTarget = 0;\n        this.scoutTargetRefreshedAt = currentTick;\n        this.scoutTarget = target?.target ?? null;\n        this.scoutMinDistance = undefined;\n        this.scoutTargetIsPermanent = target?.permanent ?? false;\n    }\n\n    public getGlobalDebugText(): string | undefined {\n        return \"scouting\";\n    }\n\n    public getPriority() {\n        return this.priority;\n    }\n}\n\nconst SCOUT_COOLDOWN_TICKS = 300;\n\nexport class ScoutingMissionFactory {\n    constructor(private lastScoutAt: number = -SCOUT_COOLDOWN_TICKS) {}\n\n    getName(): string {\n        return \"ScoutingMissionFactory\";\n    }\n\n    maybeCreateMissions(context: SupabotContext, missionController: MissionController, logger: DebugLogger): void {\n        const { game, matchAwareness } = context;\n        if (game.getCurrentTick() < this.lastScoutAt + SCOUT_COOLDOWN_TICKS) {\n            return;\n        }\n        if (!matchAwareness.getScoutingManager().hasScoutTargets()) {\n            return;\n        }\n        if (!missionController.addMission(new ScoutingMission(\"globalScout\", 10, logger))) {\n            this.lastScoutAt = game.getCurrentTick();\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/missions/squads/combatSquad.ts",
    "content": "import {\n    ActionsApi,\n    AttackState,\n    BotContext,\n    GameApi,\n    GameMath,\n    MovementZone,\n    PlayerData,\n    UnitData,\n    Vector2,\n} from \"../../../../../game-api\";\nimport { MatchAwareness } from \"../../../awareness\";\nimport { getAttackWeight, manageAttackMicro, manageMoveMicro } from \"./common\";\nimport { DebugLogger, isOwnedByNeutral, maxBy, minBy } from \"../../../common/utils\";\nimport { ActionBatcher, BatchableAction } from \"../../actionBatcher\";\nimport { Squad } from \"./squad\";\nimport { Mission, MissionAction, grabCombatants, noop } from \"../../mission\";\nimport { MissionContext } from \"../../../common/context\";\n\nconst TARGET_UPDATE_INTERVAL_TICKS = 10;\n\n// Units must be in a certain radius of the center of mass before attacking.\n// This scales for number of units in the squad though.\nconst MIN_GATHER_RADIUS = 5;\n\n// If the radius expands beyond this amount then we should switch back to gathering mode.\nconst MAX_GATHER_RADIUS = 15;\n\nconst GATHER_RATIO = 10;\n\nconst ATTACK_SCAN_AREA = 15;\n\nenum SquadState {\n    Gathering,\n    Attacking,\n}\n\nexport class CombatSquad implements Squad {\n    private lastCommand: number | null = null;\n    private state = SquadState.Gathering;\n\n    private debugLastTarget: string | undefined;\n\n    private lastOrderGiven: { [unitId: number]: BatchableAction } = {};\n\n    /**\n     *\n     * @param rallyArea the initial location to grab combatants\n     * @param targetArea\n     * @param radius\n     */\n    constructor(\n        private rallyArea: Vector2,\n        private targetArea: Vector2,\n        private radius: number,\n    ) {}\n\n    public getGlobalDebugText(): string | undefined {\n        return this.debugLastTarget ?? \"<none>\";\n    }\n\n    public setAttackArea(targetArea: Vector2) {\n        this.targetArea = targetArea;\n    }\n\n    public onAiUpdate(context: MissionContext, mission: Mission<any>, logger: DebugLogger): MissionAction {\n        const { game, actionBatcher, matchAwareness } = context;\n        const playerData = game.getPlayerData(context.player.name);\n        if (\n            mission.getUnitIds().length > 0 &&\n            (!this.lastCommand || game.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS)\n        ) {\n            this.lastCommand = game.getCurrentTick();\n            const centerOfMass = mission.getCenterOfMass();\n            const maxDistance = mission.getMaxDistanceToCenterOfMass();\n            const unitIds = mission.getUnitsMatchingByRule(game, (r) => r.isSelectableCombatant);\n            const units = unitIds.map((unitId) => game.getUnitData(unitId)).filter((unit): unit is UnitData => !!unit);\n\n            // Only use ground units for center of mass.\n            const groundUnitIds = mission.getUnitsMatchingByRule(\n                game,\n                (r) =>\n                    r.isSelectableCombatant &&\n                    (r.movementZone === MovementZone.Infantry ||\n                        r.movementZone === MovementZone.Normal ||\n                        r.movementZone === MovementZone.InfantryDestroyer),\n            );\n\n            if (this.state === SquadState.Gathering) {\n                const requiredGatherRadius = GameMath.sqrt(groundUnitIds.length) * GATHER_RATIO + MIN_GATHER_RADIUS;\n                if (\n                    centerOfMass &&\n                    maxDistance &&\n                    game.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&\n                    maxDistance > requiredGatherRadius\n                ) {\n                    units.forEach((unit) => {\n                        this.submitActionIfNew(actionBatcher, manageMoveMicro(unit, centerOfMass));\n                    });\n                } else {\n                    logger(`CombatSquad ${mission.getUniqueName()} switching back to attack mode (${maxDistance})`);\n                    this.state = SquadState.Attacking;\n                }\n            } else {\n                const targetPoint = this.targetArea || playerData.startLocation;\n                const requiredGatherRadius = GameMath.sqrt(groundUnitIds.length) * GATHER_RATIO + MAX_GATHER_RADIUS;\n                if (\n                    centerOfMass &&\n                    maxDistance &&\n                    game.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&\n                    maxDistance > requiredGatherRadius\n                ) {\n                    // Switch back to gather mode\n                    logger(`CombatSquad ${mission.getUniqueName()} switching back to gather (${maxDistance})`);\n                    this.state = SquadState.Gathering;\n                    return noop();\n                }\n                // The unit with the shortest range chooses the target. Otherwise, a base range of 5 is chosen.\n                const getRangeForUnit = (unit: UnitData) =>\n                    unit.primaryWeapon?.maxRange ?? unit.secondaryWeapon?.maxRange ?? 5;\n                const attackLeader = minBy(units, getRangeForUnit);\n                if (!attackLeader) {\n                    return noop();\n                }\n                // Find units within double the range of the leader.\n                const nearbyHostiles = matchAwareness\n                    .getHostilesNearPoint(attackLeader.tile.rx, attackLeader.tile.ry, ATTACK_SCAN_AREA)\n                    .map(({ unitId }) => game.getUnitData(unitId))\n                    .filter((unit) => !isOwnedByNeutral(unit)) as UnitData[];\n\n                for (const unit of units) {\n                    const bestUnit = maxBy(nearbyHostiles, (target) => getAttackWeight(unit, target));\n                    if (bestUnit) {\n                        this.submitActionIfNew(actionBatcher, manageAttackMicro(unit, bestUnit));\n                        this.debugLastTarget = `Unit ${bestUnit.id.toString()}`;\n                    } else {\n                        this.submitActionIfNew(actionBatcher, manageMoveMicro(unit, targetPoint));\n                        this.debugLastTarget = `@${targetPoint.x},${targetPoint.y}`;\n                    }\n                }\n            }\n        }\n        return noop();\n    }\n\n    /**\n     * Sends an action to the actionBatcher if and only if the action is different from the last action we submitted to it.\n     * Prevents spamming redundant orders, which affects performance and can also cause the unit to sit around doing nothing.\n     */\n    private submitActionIfNew(actionBatcher: ActionBatcher, action: BatchableAction) {\n        const lastAction = this.lastOrderGiven[action.unitId];\n        if (!lastAction || !lastAction.isSameAs(action)) {\n            actionBatcher.push(action);\n            this.lastOrderGiven[action.unitId] = action;\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/missions/squads/common.ts",
    "content": "import { AttackState, ObjectType, OrderType, StanceType, UnitData, Vector2, ZoneType } from \"../../../../../game-api\";\nimport { getDistanceBetweenPoints, getDistanceBetweenUnits } from \"../../../map/map\";\nimport { BatchableAction } from \"../../actionBatcher\";\n\nconst NONCE_GI_DEPLOY = 0;\nconst NONCE_GI_UNDEPLOY = 1;\n\n// Micro methods\nexport function manageMoveMicro(attacker: UnitData, attackPoint: Vector2): BatchableAction {\n    if (attacker.name === \"E1\") {\n        const isDeployed = (attacker.stance as any) === StanceType.Deployed;\n        if (isDeployed) {\n            return BatchableAction.noTarget(attacker.id, OrderType.DeploySelected, NONCE_GI_UNDEPLOY);\n        }\n    }\n\n    return BatchableAction.toPoint(attacker.id, OrderType.AttackMove, attackPoint);\n}\n\nexport function manageAttackMicro(attacker: UnitData, target: UnitData): BatchableAction {\n    const distance = getDistanceBetweenUnits(attacker, target);\n    if (attacker.name === \"E1\") {\n        // Para (deployed weapon) range is 5.\n        const deployedWeaponRange = attacker.secondaryWeapon?.maxRange || 5;\n        const isDeployed = (attacker.stance as any) === StanceType.Deployed;\n        if (!isDeployed && (distance <= deployedWeaponRange || (attacker.attackState as any) === AttackState.JustFired)) {\n            return BatchableAction.noTarget(attacker.id, OrderType.DeploySelected, NONCE_GI_DEPLOY);\n        } else if (isDeployed && distance > deployedWeaponRange) {\n            return BatchableAction.noTarget(attacker.id, OrderType.DeploySelected, NONCE_GI_UNDEPLOY);\n        }\n    }\n    let targetData = target;\n    let orderType: OrderType = OrderType.Attack;\n    const primaryWeaponRange = attacker.primaryWeapon?.maxRange || 5;\n    if ((targetData?.type as any) == ObjectType.Building && distance < primaryWeaponRange * 0.8) {\n        orderType = OrderType.Attack;\n    } else if (targetData?.rules.canDisguise) {\n        // Special case for mirage tank/spy as otherwise they just sit next to it.\n        orderType = OrderType.Attack;\n    }\n    return BatchableAction.toTargetId(attacker.id, orderType, target.id);\n}\n\n/**\n *\n * @param attacker\n * @param target\n * @returns A number describing the weight of the given target for the attacker, or null if it should not attack it.\n */\nexport function getAttackWeight(attacker: UnitData, target: UnitData): number | null {\n    const { rx: x, ry: y } = attacker.tile;\n    const { rx: hX, ry: hY } = target.tile;\n\n    if (!attacker.primaryWeapon?.projectileRules.isAntiAir && target.zone === ZoneType.Air) {\n        return null;\n    }\n\n    if (!attacker.primaryWeapon?.projectileRules.isAntiGround && target.zone === ZoneType.Ground) {\n        return null;\n    }\n\n    return 1000000 - getDistanceBetweenPoints(new Vector2(x, y), new Vector2(hX, hY));\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/mission/missions/squads/squad.ts",
    "content": "import { Mission, MissionAction } from \"../../mission\";\nimport { DebugLogger } from \"../../../common/utils\";\nimport { MissionContext } from \"../../../common/context\";\n\nexport interface Squad {\n    onAiUpdate(context: MissionContext, mission: Mission<any>, logger: DebugLogger): MissionAction;\n\n    getGlobalDebugText(): string | undefined;\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/threat/sectorThreat.ts",
    "content": "import { Box2, GameApi, GameMath, MapApi, PlayerData, Vector2 } from \"../../../game-api\";\nimport { Sector, SectorAndDist } from \"../map/sectorUtils\";\n\nexport function calculateSectorThreat(startX: number, startY: number, sectorSize: number, gameApi: GameApi, playerData: PlayerData) {\n    const unitsInArea = gameApi.getUnitsInArea(new Box2(new Vector2(startX, startY), new Vector2(startX + sectorSize, startY + sectorSize)));\n\n    let threat = 0;\n    for (const unitId of unitsInArea) {\n        const unit = gameApi.getGameObjectData(unitId);\n        if (!unit || !unit.owner) {\n            continue;\n        }\n        if (unit.owner === playerData.name) {\n            threat -= unit.maxHitPoints ?? 1;\n            continue;\n        }\n        if (gameApi.areAlliedPlayers(playerData.name, unit.owner)) {\n            continue;\n        }\n        const owner = gameApi.getPlayerData(unit.owner);\n        if (!owner.isCombatant) {\n            continue;\n        }\n        threat += unit.maxHitPoints ?? 1;\n    }\n    return threat;\n}\n\nexport function calculateDiffuseSectorThreat(sector: Sector, neighbours: SectorAndDist[]) {\n    // the objective is for a cell's threat to slowly spread (diffuse) into its neighbouring cells.\n    const connectedSectorIds = new Set(sector.connectedSectorIds);\n    const totalNeighbourThreat = (sector.threatLevel ?? 0) + neighbours.reduce((acc, cV) => acc + (cV.sector.threatLevel ?? 0), 0);\n    // Based on the max of the closest _connected_ sectors\n    const maxOfNeighboursThreat = neighbours\n        .filter((n) => connectedSectorIds.has(n.sector.id))\n        .reduce((pV, cV) => Math.max(pV, (cV.sector.diffuseThreatLevel ?? 0) * cV.dist), 0);\n    return Math.max(totalNeighbourThreat, maxOfNeighboursThreat * 0.95);\n}\n\nexport function calculateMoney(startX: number, startY: number, size: number, mapApi: MapApi) {\n    return mapApi\n        .getTilesInRect({ x: startX, y: startY, width: size, height: size})\n        .map((t) => mapApi.getTileResourceData(t)).map((t) => t ? t.gems + t.ore : 0)\n        .reduce((pV, cV) => pV + cV, 0);\n}"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/threat/threat.ts",
    "content": "// A periodically-refreshed cache of known threats to a bot so we can use it in decision making.\n\nexport class GlobalThreat {\n    constructor(\n        public certainty: number, // 0.0 - 1.0 based on approximate visibility around the map.\n        public totalOffensiveLandThreat: number, // a number that approximates how much land-based firepower our opponents have.\n        public totalOffensiveAirThreat: number, // a number that approximates how much airborne firepower our opponents have.\n        public totalOffensiveAntiAirThreat: number, // a number that approximates how much anti-air firepower our opponents have.\n        public totalDefensiveThreat: number, // a number that approximates how much defensive power our opponents have.\n        public totalDefensivePower: number, // a number that approximates how much defensive power we have.\n        public totalAvailableAntiGroundFirepower: number, // how much anti-ground power we have\n        public totalAvailableAntiAirFirepower: number, // how much anti-air power we have\n        public totalAvailableAirPower: number, // how much firepower we have in air units\n    ) {}\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/logic/threat/threatCalculator.ts",
    "content": "import {\n    GameApi,\n    GameMath,\n    GameObjectData,\n    MovementZone,\n    ObjectType,\n    PlayerData,\n    ProjectileRules,\n    UnitData,\n    WeaponRules,\n} from \"../../../game-api\";\nimport { GlobalThreat } from \"./threat\";\nimport { getCachedTechnoRules } from \"../common/rulesCache\";\n\nexport function calculateGlobalThreat(game: GameApi, playerData: PlayerData, visibleAreaPercent: number): GlobalThreat {\n    let groundUnits = game.getVisibleUnits(\n        playerData.name,\n        \"enemy\",\n        (r) => r.type == ObjectType.Vehicle || r.type == ObjectType.Infantry,\n    );\n    let airUnits = game.getVisibleUnits(playerData.name, \"enemy\", (r) => r.movementZone == MovementZone.Fly);\n    let groundDefence = game\n        .getVisibleUnits(playerData.name, \"enemy\", (r) => r.type == ObjectType.Building)\n        .filter((unitId) => isAntiGround(game, unitId));\n    let antiAirPower = game\n        .getVisibleUnits(playerData.name, \"enemy\", (r) => r.type != ObjectType.Building)\n        .filter((unitId) => isAntiAir(game, unitId));\n\n    let ourAntiGroundUnits = game\n        .getVisibleUnits(playerData.name, \"self\", (r) => r.isSelectableCombatant)\n        .filter((unitId) => isAntiGround(game, unitId));\n    let ourAntiAirUnits = game\n        .getVisibleUnits(playerData.name, \"self\", (r) => r.isSelectableCombatant || r.type === ObjectType.Building)\n        .filter((unitId) => isAntiAir(game, unitId));\n    let ourGroundDefence = game\n        .getVisibleUnits(playerData.name, \"self\", (r) => r.type === ObjectType.Building)\n        .filter((unitId) => isAntiGround(game, unitId));\n    let ourAirUnits = game.getVisibleUnits(\n        playerData.name,\n        \"self\",\n        (r) => r.movementZone == MovementZone.Fly && r.isSelectableCombatant,\n    );\n\n    let observedGroundThreat = calculateFirepowerForUnits(game, groundUnits);\n    let observedAirThreat = calculateFirepowerForUnits(game, airUnits);\n    let observedAntiAirThreat = calculateFirepowerForUnits(game, antiAirPower);\n    let observedGroundDefence = calculateFirepowerForUnits(game, groundDefence);\n\n    let ourAntiGroundPower = calculateFirepowerForUnits(game, ourAntiGroundUnits);\n    let ourAntiAirPower = calculateFirepowerForUnits(game, ourAntiAirUnits);\n    let ourAirPower = calculateFirepowerForUnits(game, ourAirUnits);\n    let ourGroundDefencePower = calculateFirepowerForUnits(game, ourGroundDefence);\n\n    return new GlobalThreat(\n        visibleAreaPercent,\n        observedGroundThreat,\n        observedAirThreat,\n        observedAntiAirThreat,\n        observedGroundDefence,\n        ourGroundDefencePower,\n        ourAntiGroundPower,\n        ourAntiAirPower,\n        ourAirPower,\n    );\n}\n\n// For the purposes of determining if units can target air/ground, we look purely at the technorules and only the base weapon (not elite)\n// This excludes some special cases such as IFVs changing turrets, but we have to deal with it for now.\nfunction isAntiGround(gameApi: GameApi, unitId: any): boolean {\n    return testProjectile(gameApi, unitId, (p) => p.isAntiGround);\n}\nfunction isAntiAir(gameApi: GameApi, unitId: any): boolean {\n    return testProjectile(gameApi, unitId, (p) => p.isAntiAir);\n}\n\nfunction testProjectile(gameApi: GameApi, unitId: any, test: (p: ProjectileRules) => boolean) {\n    const rules = getCachedTechnoRules(gameApi, unitId);\n    if (!rules || !(rules.primary || rules.secondary)) {\n        return false;\n    }\n\n    const primaryWeapon = rules.primary ? gameApi.rulesApi.getWeapon(rules.primary) : null;\n    const primaryProjectile = getProjectileRules(gameApi, primaryWeapon);\n    if (primaryProjectile && test(primaryProjectile)) {\n        return true;\n    }\n\n    const secondaryWeapon = rules.secondary ? gameApi.rulesApi.getWeapon(rules.secondary) : null;\n    const secondaryProjectile = getProjectileRules(gameApi, secondaryWeapon);\n    if (secondaryProjectile && test(secondaryProjectile)) {\n        return true;\n    }\n\n    return false;\n}\n\nfunction getProjectileRules(gameApi: GameApi, weapon: WeaponRules | null): ProjectileRules | null {\n    const primaryProjectile = weapon ? gameApi.rulesApi.getProjectile(weapon.projectile) : null;\n    return primaryProjectile;\n}\n\nfunction calculateFirepowerForUnit(gameApi: GameApi, gameObjectData: GameObjectData): number {\n    const rules = getCachedTechnoRules(gameApi, gameObjectData.id);\n    if (!rules) {\n        return 0;\n    }\n    const currentHp = gameObjectData?.hitPoints || 0;\n    const maxHp = gameObjectData?.maxHitPoints || 0;\n    let threat = 0;\n    const hpRatio = currentHp / Math.max(1, maxHp);\n\n    if (rules.primary) {\n        const weapon = gameApi.rulesApi.getWeapon(rules.primary);\n        threat += (hpRatio * ((weapon.damage + 1) * GameMath.sqrt(weapon.range + 1))) / Math.max(weapon.rof, 1);\n    }\n    if (rules.secondary) {\n        const weapon = gameApi.rulesApi.getWeapon(rules.secondary);\n        threat += (hpRatio * ((weapon.damage + 1) * GameMath.sqrt(weapon.range + 1))) / Math.max(weapon.rof, 1);\n    }\n    return Math.min(800, threat);\n}\n\nfunction calculateFirepowerForUnits(game: GameApi, unitIds: any[]) {\n    let threat = 0;\n    unitIds.forEach((unitId) => {\n        const gameObjectData = game.getGameObjectData(unitId);\n        if (gameObjectData) {\n            threat += calculateFirepowerForUnit(game, gameObjectData);\n        }\n    });\n    return threat;\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/strategy/compositionUtils.ts",
    "content": "import { BotContext } from \"../../game-api\";\nimport { UnitComposition } from \"./strategy\";\n\nexport type SideComposition = {\n    composition: UnitComposition;\n    minimumUnits: number;\n    maximumUnits: number;\n};\n\nexport type Compositions = Record<string, SideComposition>;\n\n// Returns the compositions that the player can actually build right now.\nexport function getValidCompositions(context: BotContext, compositions: Compositions) {\n    const availableObjects = new Set(context.player.production.getAvailableObjects().map((o) => o.name));\n    return Object.keys(compositions).filter((compositionName) => {\n        const composition = compositions[compositionName];\n        return Object.keys(composition.composition).every((unitName) => availableObjects.has(unitName));\n    });\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/strategy/defaultStrategy.ts",
    "content": "import { Strategy } from \"./strategy\";\nimport { ExpansionMissionFactory } from \"../logic/mission/missions/expansionMission\";\nimport { ScoutingMissionFactory } from \"../logic/mission/missions/scoutingMission\";\nimport { AttackMissionFactory } from \"../logic/mission/missions/attackMission\";\nimport { DefenceMissionFactory } from \"../logic/mission/missions/defenceMission\";\nimport { EngineerMissionFactory } from \"../logic/mission/missions/engineerMission\";\nimport { SupabotContext } from \"../logic/common/context\";\nimport { MissionController } from \"../logic/mission/missionController\";\nimport { DebugLogger } from \"../logic/common/utils\";\nimport { Compositions, getValidCompositions, SideComposition } from \"./compositionUtils\";\n\n// These could be loaded from ai.ini\nconst DEFAULT_COMPOSITIONS: Compositions = {\n    conscripts: {\n        composition: {\n            E2: 1,\n        },\n        minimumUnits: 3,\n        maximumUnits: 10,\n    },\n    gis: {\n        composition: {\n            E1: 1,\n        },\n        minimumUnits: 3,\n        maximumUnits: 10,\n    },\n    sovietTanks: {\n        composition: {\n            HTNK: 5,\n            HTK: 1,\n        },\n        minimumUnits: 2,\n        maximumUnits: 20,\n    },\n    alliedTanks: {\n        composition: {\n            MTNK: 5,\n            FV: 1,\n        },\n        minimumUnits: 2,\n        maximumUnits: 20,\n    },\n    kirovs: {\n        composition: {\n            KIROV: 1,\n        },\n        minimumUnits: 1,\n        maximumUnits: 3,\n    },\n    rocketeers: {\n        composition: {\n            JUMPJET: 1,\n        },\n        minimumUnits: 2,\n        maximumUnits: 6,\n    },\n    heavySovietTanks: {\n        composition: {\n            APOC: 2,\n            HTNK: 1,\n        },\n        minimumUnits: 2,\n        maximumUnits: 10,\n    },\n    heavyAlliedTanks: {\n        composition: {\n            MTNK: 2,\n            MGTK: 1,\n        },\n        minimumUnits: 2,\n        maximumUnits: 10,\n    },\n    sovietArtillery: {\n        composition: {\n            V3: 2,\n            HTNK: 1,\n        },\n        minimumUnits: 3,\n        maximumUnits: 10,\n    },\n    alliedArtillery: {\n        composition: {\n            SREF: 2,\n            MTNK: 1,\n        },\n        minimumUnits: 3,\n        maximumUnits: 10,\n    },\n};\n\nexport class DefaultStrategy implements Strategy {\n    private expansionFactory = new ExpansionMissionFactory();\n    private scoutingFactory = new ScoutingMissionFactory();\n    private attackFactory = new AttackMissionFactory();\n    private defenceFactory = new DefenceMissionFactory();\n    private engineerFactory = new EngineerMissionFactory();\n\n    onAiUpdate(context: SupabotContext, missionController: MissionController, logger: DebugLogger) {\n        this.expansionFactory.maybeCreateMissions(context, missionController, logger);\n        this.scoutingFactory.maybeCreateMissions(context, missionController, logger);\n\n        const composition = this.selectRandomAttackComposition(context, logger);\n        if (composition) {\n            this.attackFactory.maybeCreateMissions(context, missionController, logger, composition);\n        }\n\n        this.defenceFactory.maybeCreateMissions(context, missionController, logger);\n        this.engineerFactory.maybeCreateMissions(context, missionController, logger);\n\n        return this;\n    }\n\n    private selectRandomAttackComposition(context: SupabotContext, logger: DebugLogger): SideComposition | null {\n        const playerData = context.game.getPlayerData(context.player.name);\n        const side = playerData.country?.side;\n        if (side === undefined) {\n            return null;\n        }\n\n        const validCompositions = getValidCompositions(context, DEFAULT_COMPOSITIONS);\n\n        if (validCompositions.length === 0) {\n            return null;\n        }\n\n        logger(`Valid compositions: ${validCompositions.join(\", \")}`);\n\n        const randomIndex = context.game.generateRandomInt(0, validCompositions.length - 1);\n        const compositionId = validCompositions[randomIndex];\n        return DEFAULT_COMPOSITIONS[compositionId];\n    }\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/bot/strategy/strategy.ts",
    "content": "import { SupabotContext } from \"../logic/common/context\";\nimport { MissionController } from \"../logic/mission/missionController\";\nimport { DebugLogger } from \"../logic/common/utils\";\n\nexport type UnitComposition = {\n    [unitType: string]: number;\n};\n\n/**\n * Defines how the bot builds units, selects missions,\n * and makes high-level tactical decisions.\n */\nexport interface Strategy {\n    /**\n     * Poll the strategy for new missions to create in the current game state.\n     * Strategy implementations should create or return missions as appropriate.\n     *\n     * @param context Current game context\n     * @param missionController Controller to add missions to\n     * @param logger Debug logger\n     * @return Strategy This strategy, or a new one to replace it.\n     */\n    onAiUpdate(context: SupabotContext, missionController: MissionController, logger: DebugLogger): Strategy;\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/builtIn/game-api.ts",
    "content": "/**\n * Local shim for @chronodivide/game-api.\n * Re-exports all game API types needed by the builtIn bot from local sources.\n */\n\nexport { ActionsApi } from '@/game/api/ActionsApi';\nexport { GameApi } from '@/game/api/GameApi';\nexport { MapApi } from '@/game/api/MapApi';\nexport { ProductionApi } from '@/game/api/ProductionApi';\nexport { Bot } from '@/game/bot/Bot';\nexport { ObjectType } from '@/engine/type/ObjectType';\nexport { OrderType } from '@/game/order/OrderType';\nexport { SideType } from '@/game/SideType';\nexport { QueueType } from '@/game/player/production/ProductionQueue';\nexport { QueueStatus } from '@/game/player/production/ProductionQueue';\nexport { GameMath } from '@/game/math/GameMath';\nexport { Box2 } from '@/game/math/Box2';\nexport { Vector2 } from '@/game/math/Vector2';\nexport { LandType } from '@/game/type/LandType';\nexport { SpeedType } from '@/game/type/SpeedType';\nexport { MovementZone } from '@/game/type/MovementZone';\nexport { TerrainType } from '@/engine/type/TerrainType';\nexport { AttackState } from '@/game/gameobject/trait/AttackTrait';\nexport { StanceType } from '@/game/gameobject/infantry/StanceType';\nexport { ZoneType } from '@/game/gameobject/unit/ZoneType';\nexport { FactoryType } from '@/game/rules/TechnoRules';\nexport { TechnoRules } from '@/game/rules/TechnoRules';\nexport { WeaponRules } from '@/game/rules/WeaponRules';\nexport { ProjectileRules } from '@/game/rules/ProjectileRules';\n\n// Re-export event types\nexport { ApiEventType } from '@/game/api/EventsApi';\n\n// Re-export interfaces\nexport type { GameObjectData } from '@/game/api/interface/GameObjectData';\nexport type { PlayerData } from '@/game/api/interface/PlayerData';\nexport type { UnitData } from '@/game/api/interface/UnitData';\nexport type { PathNode } from '@/game/api/interface/PathNode';\nexport type { Tile } from '@/game/map/Tile';\nexport type { BuildingPlacementData } from '@/game/api/interface/BuildingPlacementData';\n\n/**\n * Rectangle interface for bounding area calculations.\n */\nexport interface Rectangle {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\n\n// Types not directly exported from the original codebase - define locally\n\n/**\n * ApiEvent union type matching events dispatched by EventsApi.\n */\nexport type ApiEvent = {\n    type: number;\n    objectId?: number;\n    attackerInfo?: {\n        playerName: string;\n        objectId?: number;\n    };\n    [key: string]: any;\n};\n\n/**\n * BotContext - provides structured access to game, player, and APIs.\n * Used by the builtIn bot's mission/strategy system.\n */\nexport interface BotContext {\n    readonly game: import('@/game/api/GameApi').GameApi;\n    readonly player: {\n        readonly name: string;\n        readonly actions: import('@/game/api/ActionsApi').ActionsApi;\n        readonly production: import('@/game/api/ProductionApi').ProductionApi;\n    };\n}\n\n/**\n * Size interface for map dimensions.\n */\nexport interface Size {\n    width: number;\n    height: number;\n}\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/example/README.md",
    "content": "# Example AI Bot\n\nA simple example AI bot for Red Alert 2 Web. Supports both Allied and Soviet factions.\n\n## Usage\n\n1. Zip the contents of this folder (ensure `bot.ts` is at the zip root)\n2. Upload the zip file in the game's bot upload interface\n3. Start a game with an AI opponent — the uploaded bot will be used\n\nThe uploader accepts TypeScript (`.ts`) files directly — no compilation needed. Type annotations are automatically stripped at load time.\n\n## Features\n\n- Deploys MCV at game start\n- Follows a build order: Power → Refinery → Barracks → War Factory → Power → Refinery\n- Builds extra power plants when power runs low\n- Trains tanks and infantry in a loop\n- Sends idle harvesters to gather resources\n- Attacks enemy positions when 6+ combat units are available\n\n## API Reference\n\nThe bot's `onGameStart` and `onGameTick` callbacks receive a context object:\n\n```\nctx.gameApi        - Read-only game state (players, units, map, rules)\nctx.actionsApi     - Issue commands (build, order units, queue production)\nctx.productionApi  - Query production queue status\nctx.logger         - Logging (info, warn, error, debug)\nctx.playerName     - This bot's player name\nctx.country        - This bot's country name\n```\n\n### Key gameApi Methods\n\n| Method | Description |\n|--------|-------------|\n| `getPlayerData(name)` | Player info (credits, power, startLocation) |\n| `getVisibleUnits(player, type, filter?)` | Get unit IDs (\"self\"/\"enemy\"/\"allied\") |\n| `getUnitData(id)` | Unit details (tile, hitPoints, isIdle, weapons) |\n| `getGameObjectData(id)` | Generic object data |\n| `canPlaceBuilding(player, name, {rx, ry})` | Check if placement is valid |\n| `getBuildingPlacementData(name)` | Get foundation size |\n| `getCurrentTick()` | Current game tick |\n| `mapApi` | Map, tile, and pathfinding queries |\n| `rulesApi` | Game rules data |\n\n### Key actionsApi Methods\n\n| Method | Description |\n|--------|-------------|\n| `queueForProduction(queue, name, type, qty)` | Queue a unit/building |\n| `placeBuilding(name, x, y)` | Place a completed building |\n| `orderUnits(ids[], orderType, x?, y?)` | Issue orders to units |\n| `sellBuilding(id)` | Sell a building |\n\n### Queue Types\n\n```\nStructures: 0, Armory: 1, Infantry: 2, Vehicles: 3, Aircrafts: 4, Ships: 5\n```\n\n### Order Types\n\n```\nMove: 0, Attack: 2, AttackMove: 4, Guard: 5, Deploy: 9, Gather: 14\n```\n\n## Module Format\n\nThe bot must use CommonJS `module.exports`:\n\n```typescript\n(module as any).exports = {\n    id: \"unique-id\",\n    displayName: \"Bot Name\",\n    version: \"1.0.0\",\n    author: \"Your Name\",\n    createBot: function(playerName: string, country: string) {\n        return { onGameStart, onGameTick, onGameEvent, dispose };\n    }\n};\n```\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/example/bot.ts",
    "content": "/**\n * Example AI Bot for Red Alert 2 Web\n *\n * This bot is written as JavaScript-compatible TypeScript so it runs\n * directly in the sandbox without compilation.  Type information lives\n * in JSDoc comments and the companion README.\n *\n * To use as an uploaded bot:\n *   1. Zip this file so bot.ts is at the zip root\n *   2. Upload the zip in the game's lobby → \"Upload AI Bot\" dialog\n *\n * Context object received in onGameStart / onGameTick:\n *   ctx.gameApi        - read-only game state queries\n *   ctx.actionsApi     - issue commands (build, order units, production)\n *   ctx.productionApi  - query production queues\n *   ctx.logger         - logging (info, warn, error, debug)\n *   ctx.playerName     - this bot's player name\n *   ctx.country        - this bot's country name\n */\n\n// ---- Constants (must match engine enums) ----\n\nvar QueueType = { Structures: 0, Armory: 1, Infantry: 2, Vehicles: 3, Aircrafts: 4, Ships: 5 };\nvar QueueStatus = { Idle: 0, Active: 1, OnHold: 2, Ready: 3 };\nvar OrderType = { Move: 0, ForceMove: 1, Attack: 2, ForceAttack: 3, AttackMove: 4, Guard: 5, GuardArea: 6, Capture: 7, Occupy: 8, Deploy: 9, DeploySelected: 10, Stop: 11, Gather: 14 };\nvar ObjectType = { None: 0, Aircraft: 1, Building: 2, Infantry: 3, Overlay: 4, Smudge: 5, Terrain: 6, Vehicle: 7 };\n\n// ---- Faction data ----\n\nvar ALLIED_COUNTRIES = [\n    \"Americans\", \"British\", \"French\", \"Germans\", \"Koreans\", \"Alliance\",\n];\n\nvar ALLIED_BUILD_ORDER = [\n    { name: \"GAPOWR\", queue: QueueType.Structures, type: ObjectType.Building },  // Power Plant\n    { name: \"GAREFN\", queue: QueueType.Structures, type: ObjectType.Building },  // Refinery\n    { name: \"GAPILE\", queue: QueueType.Structures, type: ObjectType.Building },  // Barracks\n    { name: \"GAWEAP\", queue: QueueType.Structures, type: ObjectType.Building },  // War Factory\n    { name: \"GAPOWR\", queue: QueueType.Structures, type: ObjectType.Building },  // 2nd Power Plant\n    { name: \"GAREFN\", queue: QueueType.Structures, type: ObjectType.Building },  // 2nd Refinery\n];\n\nvar SOVIET_BUILD_ORDER = [\n    { name: \"NAPOWR\", queue: QueueType.Structures, type: ObjectType.Building },  // Tesla Reactor\n    { name: \"NAREFN\", queue: QueueType.Structures, type: ObjectType.Building },  // Refinery\n    { name: \"NAHAND\", queue: QueueType.Structures, type: ObjectType.Building },  // Barracks\n    { name: \"NAWEAP\", queue: QueueType.Structures, type: ObjectType.Building },  // War Factory\n    { name: \"NAPOWR\", queue: QueueType.Structures, type: ObjectType.Building },  // 2nd Tesla Reactor\n    { name: \"NAREFN\", queue: QueueType.Structures, type: ObjectType.Building },  // 2nd Refinery\n];\n\nvar ALLIED_UNITS = [\n    { name: \"MTNK\", queue: QueueType.Vehicles, type: ObjectType.Vehicle },   // Grizzly Tank\n    { name: \"MTNK\", queue: QueueType.Vehicles, type: ObjectType.Vehicle },\n    { name: \"E1\",   queue: QueueType.Infantry, type: ObjectType.Infantry },   // GI\n    { name: \"FV\",   queue: QueueType.Vehicles, type: ObjectType.Vehicle },    // IFV\n];\n\nvar SOVIET_UNITS = [\n    { name: \"HTNK\", queue: QueueType.Vehicles, type: ObjectType.Vehicle },   // Rhino Tank\n    { name: \"HTNK\", queue: QueueType.Vehicles, type: ObjectType.Vehicle },\n    { name: \"E2\",   queue: QueueType.Infantry, type: ObjectType.Infantry },   // Conscript\n    { name: \"HTK\",  queue: QueueType.Vehicles, type: ObjectType.Vehicle },    // Flak Track\n];\n\nvar POWER_BUILDING = {\n    allied: { name: \"GAPOWR\", queue: QueueType.Structures, type: ObjectType.Building },\n    soviet: { name: \"NAPOWR\", queue: QueueType.Structures, type: ObjectType.Building },\n};\n\n// ---- Bot implementation ----\n\nfunction createExampleBot(playerName, country) {\n    var isAllied = ALLIED_COUNTRIES.indexOf(country) !== -1;\n    var buildOrder = (isAllied ? ALLIED_BUILD_ORDER : SOVIET_BUILD_ORDER).slice();\n    var unitPool = isAllied ? ALLIED_UNITS : SOVIET_UNITS;\n    var powerBuilding = isAllied ? POWER_BUILDING.allied : POWER_BUILDING.soviet;\n\n    var buildOrderIndex = 0;\n    var unitPoolIndex = 0;\n    var startLocation = { rx: 50, ry: 50 };\n    var initialized = false;\n    var lastBuildAttemptTick = 0;\n    var lastUnitQueueTick = 0;\n    var lastAttackTick = 0;\n\n    // ---- Helpers ----\n\n    function getMyCombatUnits(gameApi) {\n        return gameApi.getVisibleUnits(playerName, \"self\", function (r) {\n            return (r.type === ObjectType.Vehicle || r.type === ObjectType.Infantry)\n                && !!r.primary;\n        });\n    }\n\n    function getEnemyUnits(gameApi) {\n        return gameApi.getVisibleUnits(playerName, \"enemy\");\n    }\n\n    function findPlacementNear(gameApi, buildingName, center) {\n        var placementData = gameApi.getBuildingPlacementData(buildingName);\n        if (!placementData) return null;\n\n        // Start from radius 2 to leave room around conyard for unit movement\n        for (var radius = 2; radius < 18; radius++) {\n            for (var dx = -radius; dx <= radius; dx++) {\n                for (var dy = -radius; dy <= radius; dy++) {\n                    if (Math.abs(dx) !== radius && Math.abs(dy) !== radius) continue;\n                    var pos = { rx: center.rx + dx, ry: center.ry + dy };\n                    if (gameApi.canPlaceBuilding(playerName, buildingName, pos)) {\n                        return pos;\n                    }\n                }\n            }\n        }\n        return null;\n    }\n\n    function isQueueIdle(productionApi, queueType) {\n        var data = productionApi.getQueueData(queueType);\n        return !!data && data.status === QueueStatus.Idle;\n    }\n\n    function isQueueReady(productionApi, queueType) {\n        var data = productionApi.getQueueData(queueType);\n        return !!data && data.status === QueueStatus.Ready;\n    }\n\n    // ---- Subsystems ----\n\n    function handleDeployMCV(ctx) {\n        var gameApi = ctx.gameApi, actionsApi = ctx.actionsApi, logger = ctx.logger;\n        var mcvs = gameApi.getVisibleUnits(playerName, \"self\", function (r) { return !!r.deploysInto; });\n        for (var i = 0; i < mcvs.length; i++) {\n            var data = gameApi.getUnitData(mcvs[i]);\n            if (data && data.isIdle) {\n                actionsApi.orderUnits([mcvs[i]], OrderType.DeploySelected);\n                logger.info(\"Deploying MCV\");\n                break;\n            }\n        }\n    }\n\n    function handleBuildOrder(ctx) {\n        var gameApi = ctx.gameApi, actionsApi = ctx.actionsApi, productionApi = ctx.productionApi, logger = ctx.logger;\n        var tick = gameApi.getCurrentTick();\n\n        if (buildOrderIndex >= buildOrder.length) return;\n        if (tick - lastBuildAttemptTick < 30) return;\n\n        if (isQueueReady(productionApi, QueueType.Structures)) {\n            var currentItem = buildOrder[buildOrderIndex];\n            var placement = findPlacementNear(gameApi, currentItem.name, startLocation);\n            if (placement) {\n                actionsApi.placeBuilding(currentItem.name, placement.rx, placement.ry);\n                logger.info(\"Placing \" + currentItem.name + \" at \" + placement.rx + \",\" + placement.ry);\n                buildOrderIndex++;\n                lastBuildAttemptTick = tick;\n            }\n            return;\n        }\n\n        if (isQueueIdle(productionApi, QueueType.Structures)) {\n            var nextItem = buildOrder[buildOrderIndex];\n            actionsApi.queueForProduction(nextItem.queue, nextItem.name, nextItem.type, 1);\n            logger.info(\"Queuing build: \" + nextItem.name);\n            lastBuildAttemptTick = tick;\n        }\n    }\n\n    function handlePower(ctx) {\n        var gameApi = ctx.gameApi, productionApi = ctx.productionApi, actionsApi = ctx.actionsApi, logger = ctx.logger;\n        var playerData = gameApi.getPlayerData(playerName);\n        if (!playerData || !playerData.power) return;\n\n        if (playerData.power.drain > (playerData.power.total || playerData.power.output || 0) - 50) {\n            if (isQueueIdle(productionApi, QueueType.Structures) && buildOrderIndex >= buildOrder.length) {\n                actionsApi.queueForProduction(powerBuilding.queue, powerBuilding.name, powerBuilding.type, 1);\n                logger.info(\"Queuing extra power plant (low power)\");\n                buildOrder.push(powerBuilding);\n            }\n        }\n    }\n\n    function handleUnitProduction(ctx) {\n        var gameApi = ctx.gameApi, actionsApi = ctx.actionsApi, productionApi = ctx.productionApi, logger = ctx.logger;\n        var tick = gameApi.getCurrentTick();\n\n        if (tick - lastUnitQueueTick < 60) return;\n\n        var vehicleData = productionApi.getQueueData(QueueType.Vehicles);\n        if (vehicleData && vehicleData.status === QueueStatus.Idle) {\n            var unit = unitPool[unitPoolIndex % unitPool.length];\n            if (unit.queue === QueueType.Vehicles) {\n                actionsApi.queueForProduction(unit.queue, unit.name, unit.type, 1);\n                logger.info(\"Queuing unit: \" + unit.name);\n                unitPoolIndex++;\n                lastUnitQueueTick = tick;\n            }\n        }\n\n        var infantryData = productionApi.getQueueData(QueueType.Infantry);\n        if (infantryData && infantryData.status === QueueStatus.Idle) {\n            var inf = unitPool[unitPoolIndex % unitPool.length];\n            if (inf.queue === QueueType.Infantry) {\n                actionsApi.queueForProduction(inf.queue, inf.name, inf.type, 1);\n                logger.info(\"Queuing infantry: \" + inf.name);\n                unitPoolIndex++;\n                lastUnitQueueTick = tick;\n            }\n        }\n    }\n\n    function handleHarvesters(ctx) {\n        var gameApi = ctx.gameApi, actionsApi = ctx.actionsApi;\n        var harvesters = gameApi.getVisibleUnits(playerName, \"self\", function (r) {\n            return !!r.harvester;\n        });\n        for (var i = 0; i < harvesters.length; i++) {\n            var data = gameApi.getUnitData(harvesters[i]);\n            if (data && data.isIdle) {\n                actionsApi.orderUnits([harvesters[i]], OrderType.Gather);\n            }\n        }\n    }\n\n    function handleAttack(ctx) {\n        var gameApi = ctx.gameApi, actionsApi = ctx.actionsApi, logger = ctx.logger;\n        var tick = gameApi.getCurrentTick();\n\n        if (tick - lastAttackTick < 450) return;\n\n        var myUnits = getMyCombatUnits(gameApi);\n        if (myUnits.length < 6) return;\n\n        var enemies = getEnemyUnits(gameApi);\n        if (enemies.length === 0) return;\n\n        var targetData = gameApi.getGameObjectData(enemies[0]);\n        if (!targetData || !targetData.tile) return;\n\n        var idleUnits = myUnits.filter(function (id) {\n            var d = gameApi.getUnitData(id);\n            return d && d.isIdle;\n        });\n\n        if (idleUnits.length >= 4) {\n            actionsApi.orderUnits(idleUnits, OrderType.AttackMove, targetData.tile.rx, targetData.tile.ry);\n            logger.info(\"Sending \" + idleUnits.length + \" units to attack at \" + targetData.tile.rx + \",\" + targetData.tile.ry);\n            lastAttackTick = tick;\n        }\n    }\n\n    // ---- Public interface ----\n\n    return {\n        onGameStart: function (ctx) {\n            var gameApi = ctx.gameApi, logger = ctx.logger;\n            logger.info(\"=== Example Bot Starting ===\");\n            logger.info(\"Player: \" + playerName + \", Country: \" + country + \", Side: \" + (isAllied ? \"Allied\" : \"Soviet\"));\n\n            var playerData = gameApi.getPlayerData(playerName);\n            if (playerData && playerData.startLocation) {\n                // startLocation from API is a Vector2 with x/y – convert to rx/ry\n                var loc = playerData.startLocation;\n                startLocation = { rx: loc.rx || loc.x, ry: loc.ry || loc.y };\n                logger.info(\"Start location: \" + startLocation.rx + \",\" + startLocation.ry);\n            } else {\n                logger.warn(\"No start location found, using fallback\");\n            }\n\n            initialized = true;\n        },\n\n        onGameTick: function (ctx) {\n            if (!initialized) return;\n\n            var tick = ctx.gameApi.getCurrentTick();\n\n            // Log heartbeat every 300 ticks\n            if (tick % 300 === 0) {\n                var pd = ctx.gameApi.getPlayerData(playerName);\n                var units = ctx.gameApi.getVisibleUnits(playerName, \"self\");\n                ctx.logger.info(\n                    \"[Heartbeat] tick=\" + tick + \" credits=\" + (pd ? pd.credits : \"?\") +\n                    \" units=\" + units.length + \" buildIdx=\" + buildOrderIndex + \"/\" + buildOrder.length\n                );\n            }\n\n            handleDeployMCV(ctx);\n            handleBuildOrder(ctx);\n            handlePower(ctx);\n            handleUnitProduction(ctx);\n            handleHarvesters(ctx);\n            handleAttack(ctx);\n        },\n\n        onGameEvent: function () {\n            // Can react to events here (unit destroyed, etc.)\n        },\n\n        dispose: function () {\n            // Cleanup if needed\n        },\n    };\n}\n\n// ---- Module export (CommonJS — required by BotSandbox) ----\n\nmodule.exports = {\n    id: \"example-bot\",\n    displayName: \"Example Bot\",\n    version: \"1.0.0\",\n    author: \"RedAlert2 Web\",\n    description: \"A simple example AI that builds a base, trains units, and attacks enemies. Supports both Allied and Soviet factions.\",\n    createBot: createExampleBot,\n};\n"
  },
  {
    "path": "src/game/ai/thirdpartbot/index.ts",
    "content": "export type { ThirdPartyBotInterface, ThirdPartyBotMeta } from './ThirdPartyBotInterface';\nexport { ThirdPartyBotAdapter } from './ThirdPartyBotAdapter';\nexport { BotRegistry } from './BotRegistry';\nexport { BotSandbox } from './BotSandbox';\nexport { BotUploader } from './BotUploader';\nexport { BuiltInBotAdapter, registerBuiltInBot } from './builtIn/BuiltInBotAdapter';\n"
  },
  {
    "path": "src/game/api/ActionsApi.ts",
    "content": "import { ActionType } from '@/game/action/ActionType';\nimport { UpdateType } from '@/game/action/UpdateQueueAction';\nimport { DebugCommand, DebugCommandType } from '@/game/action/DebugAction';\ninterface Tile {\n    x: number;\n    y: number;\n}\ninterface Target {\n}\ninterface BuildingRules {\n}\ninterface ObjectRules {\n}\ninterface Player {\n    name: string;\n}\ninterface ActionFactory {\n    create(actionType: ActionType): any;\n}\ninterface ActionQueue {\n    push(action: any): void;\n}\ninterface Game {\n    rules: {\n        getBuilding(type: any): BuildingRules;\n        getObject(type: any, subType: any): ObjectRules;\n    };\n    getPlayerByName(name: string): Player;\n    map: {\n        tiles: {\n            getByMapCoords(x: number, y: number): any;\n        };\n        tileOccupation: {\n            getBridgeOnTile(tile: any): any;\n        };\n    };\n    getWorld(): {\n        hasObjectId(id: number): boolean;\n    };\n    getObjectById(id: number): any;\n    createTarget(object: any, tile: any): Target;\n}\ninterface LocalPlayer {\n    name: string;\n    getDebugMode(): boolean;\n}\ninterface ChatApi {\n    sayAll(playerName: string, message: string): void;\n}\nexport class ActionsApi {\n    private actionFactory: ActionFactory;\n    private actionQueue: ActionQueue;\n    private game: Game;\n    private localPlayer: LocalPlayer;\n    private chatApi?: ChatApi;\n    constructor(game: Game, actionFactory: ActionFactory, actionQueue: ActionQueue, localPlayer: LocalPlayer, chatApi?: ChatApi) {\n        this.game = game;\n        this.actionFactory = actionFactory;\n        this.actionQueue = actionQueue;\n        this.localPlayer = localPlayer;\n        this.chatApi = chatApi;\n    }\n    placeBuilding(buildingType: any, x: number, y: number): void {\n        this.createAndPushAction(ActionType.PlaceBuilding, (action) => {\n            action.buildingRules = this.game.rules.getBuilding(buildingType);\n            action.tile = { x, y };\n        });\n    }\n    sellObject(objectId: number): void {\n        this.createAndPushAction(ActionType.SellObject, (action) => {\n            action.objectId = objectId;\n        });\n    }\n    sellBuilding(buildingId: number): void {\n        this.sellObject(buildingId);\n    }\n    toggleRepairWrench(buildingId: any): void {\n        this.createAndPushAction(ActionType.ToggleRepair, (action) => {\n            action.buildingId = buildingId;\n        });\n    }\n    toggleAlliance(playerName: string, toggle: boolean): void {\n        this.createAndPushAction(ActionType.ToggleAlliance, (action) => {\n            action.toPlayer = this.game.getPlayerByName(playerName);\n            action.toggle = toggle;\n        });\n    }\n    pauseProduction(queueType: any): void {\n        this.createAndPushAction(ActionType.UpdateQueue, (action) => {\n            action.queueType = queueType;\n            action.updateType = UpdateType.Pause;\n        });\n    }\n    resumeProduction(queueType: any): void {\n        this.createAndPushAction(ActionType.UpdateQueue, (action) => {\n            action.queueType = queueType;\n            action.updateType = UpdateType.Resume;\n        });\n    }\n\n    private normalizeObjectArgs(objectType: any, subType: any): {\n        objectType: any;\n        subType: any;\n    } {\n        // Compatibility: some third-party bots call queue APIs as (name, type)\n        // while the game API expects (type, name).\n        if (typeof objectType === 'string' && (typeof subType === 'number' || /^\\d+$/.test(String(subType)))) {\n            return {\n                objectType: subType,\n                subType: objectType,\n            };\n        }\n        return { objectType, subType };\n    }\n\n    queueForProduction(queueType: any, objectType: any, subType: any, quantity: number): void {\n        const normalized = this.normalizeObjectArgs(objectType, subType);\n        let item: any;\n        try {\n            item = this.game.rules.getObject(normalized.subType, normalized.objectType);\n        } catch (e) {\n            console.error(`[ActionsApi] queueForProduction failed: getObject(\"${normalized.subType}\", ${normalized.objectType}) threw:`, e);\n            return;\n        }\n        this.createAndPushAction(ActionType.UpdateQueue, (action) => {\n            action.queueType = queueType;\n            action.updateType = UpdateType.Add;\n            action.item = item;\n            action.quantity = quantity;\n        });\n    }\n    unqueueFromProduction(queueType: any, objectType: any, subType: any, quantity: number): void {\n        const normalized = this.normalizeObjectArgs(objectType, subType);\n        let item: any;\n        try {\n            item = this.game.rules.getObject(normalized.subType, normalized.objectType);\n        } catch (e) {\n            console.error(`[ActionsApi] unqueueFromProduction failed: getObject(\"${normalized.subType}\", ${normalized.objectType}) threw:`, e);\n            return;\n        }\n        this.createAndPushAction(ActionType.UpdateQueue, (action) => {\n            action.queueType = queueType;\n            action.updateType = UpdateType.Cancel;\n            action.item = item;\n            action.quantity = quantity;\n        });\n    }\n    activateSuperWeapon(superWeaponType: any, targetTile: {\n        rx: number;\n        ry: number;\n    }, secondaryTile?: {\n        rx: number;\n        ry: number;\n    }): void {\n        this.createAndPushAction(ActionType.ActivateSuperWeapon, (action) => {\n            action.superWeaponType = superWeaponType;\n            action.tile = { x: targetTile.rx, y: targetTile.ry };\n            action.tile2 = secondaryTile ? { x: secondaryTile.rx, y: secondaryTile.ry } : undefined;\n        });\n    }\n    orderUnits(unitIds: any[], orderType: any, targetX?: any, targetY?: any, useBridge?: boolean): void {\n        this.createAndPushAction(ActionType.SelectUnits, (action) => {\n            action.unitIds = unitIds;\n        });\n        let target: Target | undefined;\n        if (targetX !== undefined) {\n            let targetObject: any;\n            let targetTile: any;\n            if (targetY !== undefined) {\n                targetObject = undefined;\n                const tile = this.game.map.tiles.getByMapCoords(targetX, targetY);\n                if (!tile) {\n                    throw new Error(`No tile found at rx,ry=${targetX},${targetY}`);\n                }\n                targetTile = tile;\n                if (useBridge) {\n                    targetObject = this.game.map.tileOccupation.getBridgeOnTile(tile);\n                }\n            }\n            else {\n                if (!this.game.getWorld().hasObjectId(targetX)) {\n                    return;\n                }\n                targetObject = this.game.getObjectById(targetX);\n                targetTile = targetObject.tile;\n            }\n            target = this.game.createTarget(targetObject, targetTile);\n        }\n        this.createAndPushAction(ActionType.OrderUnits, (action) => {\n            action.orderType = orderType;\n            action.target = target;\n        });\n    }\n    sayAll(message: string): void {\n        this.chatApi?.sayAll(this.localPlayer.name, message);\n    }\n    setGlobalDebugText(text?: string): void {\n        if (this.localPlayer.getDebugMode()) {\n            this.createAndPushAction(ActionType.DebugCommand, (action) => {\n                action.command = new DebugCommand(DebugCommandType.SetGlobalDebugText, { text: text || \"\" });\n            });\n        }\n    }\n    setUnitDebugText(unitId: number, label?: string): void {\n        if (this.localPlayer.getDebugMode()) {\n            this.createAndPushAction(ActionType.DebugCommand, (action) => {\n                action.command = new DebugCommand(DebugCommandType.SetUnitDebugText, { unitId, label });\n            });\n        }\n    }\n    quitGame(): void {\n        this.createAndPushAction(ActionType.ResignGame);\n    }\n    private createAndPushAction(actionType: ActionType, configureAction?: (action: any) => void): void {\n        const action = this.actionFactory.create(actionType);\n        action.player = this.game.getPlayerByName(this.localPlayer.name);\n        configureAction?.(action);\n        this.actionQueue.push(action);\n    }\n}\n"
  },
  {
    "path": "src/game/api/ChatApi.ts",
    "content": "export class ChatApi {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/game/api/EventsApi.ts",
    "content": "import { EventType } from '@/game/event/EventType';\nexport enum ApiEventType {\n    ObjectOwnerChange = 0,\n    ObjectSpawn = 1,\n    ObjectUnspawn = 2,\n    ObjectDestroy = 3\n}\ninterface AttackerInfo {\n    playerName: string;\n    objId?: string;\n    weaponName?: string;\n}\ninterface ObjectOwnerChangeEvent {\n    type: ApiEventType.ObjectOwnerChange;\n    prevOwnerName: string;\n    newOwnerName: string;\n    target: string;\n}\ninterface ObjectSpawnEvent {\n    type: ApiEventType.ObjectSpawn;\n    target: string;\n}\ninterface ObjectUnspawnEvent {\n    type: ApiEventType.ObjectUnspawn;\n    target: string;\n}\ninterface ObjectDestroyEvent {\n    type: ApiEventType.ObjectDestroy;\n    target: string;\n    attackerInfo?: AttackerInfo;\n}\ntype ApiEvent = ObjectOwnerChangeEvent | ObjectSpawnEvent | ObjectUnspawnEvent | ObjectDestroyEvent;\nexport class EventsApi {\n    private eventSource: any;\n    private subscriptions: (() => void)[] = [];\n    constructor(eventSource: any) {\n        this.eventSource = eventSource;\n    }\n    subscribe(eventType: ApiEventType | ((event: ApiEvent) => void), callback?: (event: ApiEvent) => void) {\n        const type = typeof eventType === 'function' ? undefined : eventType;\n        const handler = typeof eventType === 'function' ? eventType : callback!;\n        const subscription = this.eventSource.subscribe((event: any) => {\n            const apiEvent = this.transformEvent(event);\n            if (!apiEvent || (type !== undefined && type !== apiEvent.type)) {\n                return;\n            }\n            handler(apiEvent);\n        });\n        this.subscriptions.push(subscription);\n        return subscription;\n    }\n    dispose() {\n        for (const subscription of this.subscriptions) {\n            subscription();\n        }\n        this.subscriptions.length = 0;\n    }\n    private transformEvent(event: any): ApiEvent | undefined {\n        switch (event.type) {\n            case EventType.ObjectOwnerChange:\n                return {\n                    type: ApiEventType.ObjectOwnerChange,\n                    prevOwnerName: event.prevOwner.name,\n                    newOwnerName: event.target.owner.name,\n                    target: event.target.id\n                };\n            case EventType.ObjectSpawn:\n                return {\n                    type: ApiEventType.ObjectSpawn,\n                    target: event.gameObject.id\n                };\n            case EventType.ObjectUnspawn:\n                return {\n                    type: ApiEventType.ObjectUnspawn,\n                    target: event.gameObject.id\n                };\n            case EventType.ObjectDestroy: {\n                if (event.target.isProjectile()) {\n                    return undefined;\n                }\n                return {\n                    type: ApiEventType.ObjectDestroy,\n                    target: event.target.id,\n                    attackerInfo: event.attackerInfo ? {\n                        playerName: event.attackerInfo.player.name,\n                        objId: event.attackerInfo.obj?.id,\n                        weaponName: event.attackerInfo.weapon?.rules.name\n                    } : undefined\n                };\n            }\n            default:\n                return undefined;\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/api/GameApi.ts",
    "content": "import { PowerLevel } from \"@/game/player/trait/PowerTrait\";\nimport { MapApi } from \"@/game/api/MapApi\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nimport { RulesApi } from \"@/game/api/RulesApi\";\nimport { PlayerData } from \"@/game/api/interface/PlayerData\";\nimport type { GameObjectData } from \"@/game/api/interface/GameObjectData\";\nimport type { UnitData } from \"@/game/api/interface/UnitData\";\ninterface WeaponData {\n    type: string;\n    rules: any;\n    projectileRules: any;\n    warheadRules: any;\n    minRange: number;\n    maxRange: number;\n    speed: number;\n    cooldownTicks: number;\n}\ninterface SuperWeaponData {\n    playerName: string;\n    type: string;\n    status: string;\n    timerSeconds: number;\n}\ninterface BuildingPlacementData {\n    foundation: any;\n    foundationCenter: any;\n}\nexport class GameApi {\n    private game: any;\n    private useGameRandom: boolean;\n    public mapApi: MapApi;\n    public rulesApi: RulesApi;\n    constructor(game: any, useGameRandom: boolean) {\n        this.game = game;\n        this.useGameRandom = useGameRandom;\n        this.mapApi = new MapApi(game);\n        this.rulesApi = new RulesApi(game.rules);\n    }\n    /** Alias for mapApi — backward compatibility with builtIn bot. */\n    get map(): MapApi {\n        return this.mapApi;\n    }\n    isPlayerDefeated(playerName: string): boolean {\n        return this.game.getPlayerByName(playerName).defeated;\n    }\n    areAlliedPlayers(playerName1: string, playerName2: string): boolean {\n        const player1 = this.game.getPlayerByName(playerName1);\n        if (!player1)\n            throw new Error(`Player \"${playerName1}\" doesn't exist`);\n        const player2 = this.game.getPlayerByName(playerName2);\n        if (!player2)\n            throw new Error(`Player \"${playerName2}\" doesn't exist`);\n        return this.game.alliances.areAllied(player1, player2);\n    }\n    canPlaceBuilding(playerName: string, arg2: any, arg3: any): boolean {\n        const player = this.game.getPlayerByName(playerName);\n        if (!player)\n            throw new Error(`Player \"${playerName}\" doesn't exist`);\n        // Backward/forward compatible with both signatures:\n        // canPlaceBuilding(playerName, position, buildingType)\n        // canPlaceBuilding(playerName, buildingType, position)\n        const buildingType = typeof arg2 === 'string' ? arg2 : arg3;\n        const position = typeof arg2 === 'string' ? arg3 : arg2;\n        return this.game\n            .getConstructionWorker(player)\n            .canPlaceAt(buildingType, position, { normalizedTile: true });\n    }\n    getBuildingPlacementData(buildingType: string): BuildingPlacementData {\n        const buildingData = this.game.art.getObject(buildingType, ObjectType.Building);\n        return {\n            foundation: buildingData.foundation,\n            foundationCenter: buildingData.foundationCenter,\n        };\n    }\n    getPlayers(): string[] {\n        return this.game\n            .getNonNeutralPlayers()\n            .map((player: any) => player.name);\n    }\n    getPlayerData(playerName: string): PlayerData {\n        const player = this.game.getPlayerByName(playerName);\n        if (!player)\n            throw new Error(`Player \"${playerName}\" doesn't exist`);\n        return {\n            name: player.name,\n            country: player.country,\n            startLocation: this.mapApi.getStartingLocations()[player.startLocation ?? 0],\n            isObserver: player.isObserver,\n            isAi: player.isAi,\n            isCombatant: player.isCombatant(),\n            credits: player.credits,\n            power: {\n                total: player.powerTrait?.power ?? 0,\n                drain: player.powerTrait?.drain ?? 0,\n                isLowPower: player.powerTrait?.level === PowerLevel.Low,\n            },\n            radarDisabled: !!player.radarTrait?.isDisabled(),\n        };\n    }\n    getAllTerrainObjects(): any[] {\n        return this.game\n            .getWorld()\n            .getAllObjects()\n            .filter((obj: any) => obj.isTerrain())\n            .map((obj: any) => obj.id);\n    }\n    getAllUnits(filter: (rules: any) => boolean = () => true): any[] {\n        return this.game\n            .getWorld()\n            .getAllObjects()\n            .filter((obj: any) => obj.isTechno() && filter(obj.rules))\n            .map((obj: any) => obj.id);\n    }\n    getNeutralUnits(filter: (rules: any) => boolean = () => true): any[] {\n        return this.game\n            .getCivilianPlayer()\n            .getOwnedObjects()\n            .filter((obj: any) => filter(obj.rules))\n            .map((obj: any) => obj.id);\n    }\n    getUnitsInArea(area: any): any[] {\n        return this.game.map.technosByTile\n            .queryRange(area)\n            .map((obj: any) => obj.id);\n    }\n    getVisibleUnits(playerName: string, type: \"self\" | \"allied\" | \"hostile\" | \"enemy\", filter: (rules: any) => boolean = () => true): any[] {\n        const player = this.game.getPlayerByName(playerName);\n        if (!player)\n            throw new Error(`Player \"${playerName}\" doesn't exist`);\n        if (type === \"self\") {\n            return player\n                .getOwnedObjects()\n                .filter((obj: any) => filter(obj.rules))\n                .map((obj: any) => obj.id);\n        }\n        let visibilityFilter: (obj: any) => boolean;\n        if (type === \"allied\") {\n            visibilityFilter = (obj: any) => obj.owner === player ||\n                this.game.alliances.areAllied(obj.owner, player);\n        }\n        else if (type === \"hostile\" || type === \"enemy\") {\n            const playerShroud = this.game.mapShroudTrait.getPlayerShroud(player);\n            visibilityFilter = (obj: any) => this.game.map.tileOccupation\n                .calculateTilesForGameObject(obj.tile, obj)\n                .some((tile: any) => !playerShroud?.isShrouded(tile, obj.tileElevation)) &&\n                obj.owner !== player &&\n                !this.game.alliances.areAllied(obj.owner, player) &&\n                (type !== \"enemy\" || obj.owner.isCombatant());\n        }\n        else {\n            throw new Error(\"Unexpected type \" + type);\n        }\n        return this.game\n            .getWorld()\n            .getAllObjects()\n            .filter((obj: any) => obj.isTechno() &&\n            !obj.isDestroyed &&\n            visibilityFilter(obj) &&\n            filter(obj.rules))\n            .map((obj: any) => obj.id);\n    }\n    getGameObjectData(objectId: any): GameObjectData | undefined {\n        if (this.game.getWorld().hasObjectId(objectId)) {\n            const obj = this.game.getObjectById(objectId);\n            return {\n                id: obj.id,\n                type: obj.type,\n                name: obj.name,\n                rules: obj.rules,\n                tile: obj.tile,\n                tileElevation: obj.tileElevation,\n                worldPosition: obj.position.worldPosition.clone(),\n                foundation: obj.getFoundation(),\n                hitPoints: obj.healthTrait?.getHitPoints(),\n                maxHitPoints: obj.healthTrait?.maxHitPoints,\n                owner: obj.isTechno() ? obj.owner.name : undefined,\n            };\n        }\n    }\n    getUnitData(objectId: any): UnitData | undefined {\n        const gameObjectData = this.getGameObjectData(objectId);\n        if (gameObjectData) {\n            const unit = this.game.getObjectById(objectId);\n            if (!unit.isTechno()) {\n                throw new Error(`Game object with id ${objectId} is not a Techno type`);\n            }\n            return {\n                ...gameObjectData,\n                owner: unit.owner.name,\n                sight: unit.sight,\n                veteranLevel: unit.veteranLevel,\n                guardMode: unit.guardMode,\n                purchaseValue: unit.purchaseValue,\n                primaryWeapon: unit.primaryWeapon\n                    ? this.getWeaponData(unit.primaryWeapon)\n                    : undefined,\n                secondaryWeapon: unit.secondaryWeapon\n                    ? this.getWeaponData(unit.secondaryWeapon)\n                    : undefined,\n                deathWeapon: unit.armedTrait?.deathWeapon\n                    ? this.getWeaponData(unit.armedTrait.deathWeapon)\n                    : undefined,\n                attackState: unit.attackTrait?.attackState,\n                direction: unit.direction,\n                onBridge: unit.isInfantry() || unit.isVehicle() ? unit.onBridge : undefined,\n                zone: unit.isUnit() ? unit.zone : undefined,\n                buildStatus: unit.isBuilding() ? unit.buildStatus : undefined,\n                factory: unit.isBuilding() && unit.factoryTrait\n                    ? {\n                        deliveringUnit: unit.factoryTrait.deliveringUnit?.id,\n                        status: unit.factoryTrait.status,\n                    }\n                    : undefined,\n                rallyPoint: unit.isBuilding() ? unit.rallyTrait?.getRallyPoint() : undefined,\n                isPoweredOn: unit.isBuilding() && unit.poweredTrait?.isPoweredOn(),\n                hasWrenchRepair: unit.isBuilding() && !unit.autoRepairTrait.isDisabled(),\n                turretFacing: unit.isBuilding() || unit.isVehicle() ? unit.turretTrait?.facing : undefined,\n                turretNo: unit.isVehicle() ? unit.turretNo : undefined,\n                garrisonUnitCount: unit.isBuilding() ? unit.garrisonTrait?.units.length : undefined,\n                garrisonUnitsMax: unit.isBuilding() ? unit.garrisonTrait?.maxOccupants : undefined,\n                passengerSlotCount: unit.isVehicle() ? unit.transportTrait?.getOccupiedCapacity() : undefined,\n                passengerSlotMax: unit.isVehicle() ? unit.transportTrait?.getMaxCapacity() : undefined,\n                isIdle: !unit.unitOrderTrait.hasTasks(),\n                canMove: unit.isUnit() ? !unit.moveTrait.isDisabled() : undefined,\n                velocity: unit.isUnit() ? unit.moveTrait.velocity.clone() : undefined,\n                stance: unit.isInfantry() ? unit.stance : undefined,\n                harvestedOre: unit.isVehicle() ? unit.harvesterTrait?.ore : undefined,\n                harvestedGems: unit.isVehicle() ? unit.harvesterTrait?.gems : undefined,\n                ammo: unit.isAircraft() ? unit.ammo : undefined,\n                isWarpedOut: unit.warpedOutTrait.isActive(),\n                mindControlledBy: unit.mindControllableTrait?.getController()?.id,\n                tntTimer: unit.tntChargeTrait?.getTicksLeft(),\n            };\n        }\n    }\n    getAllSuperWeaponData(): SuperWeaponData[] {\n        return this.game\n            .getCombatants()\n            .map((player: any) => player.superWeaponsTrait.getAll().map((weapon: any) => ({\n            playerName: player.name,\n            type: weapon.rules.type,\n            status: weapon.status,\n            timerSeconds: weapon.getTimerSeconds(),\n        })))\n            .flat();\n    }\n    getGeneralRules(): any {\n        return this.game.rules.general;\n    }\n    getRulesIni(): string {\n        return this.game.rules.getIni();\n    }\n    getArtIni(): string {\n        return this.game.art.getIni();\n    }\n    getAiIni(): string {\n        return this.game.ai.getIni();\n    }\n    generateRandomInt(min: number, max: number): number {\n        if (this.useGameRandom) {\n            return this.game.generateRandomInt(min, max);\n        }\n        const random = this.generateRandom();\n        return Math.round(random * (max - min)) + min;\n    }\n    generateRandom(): number {\n        return this.useGameRandom\n            ? this.game.generateRandom()\n            : Math.random();\n    }\n    getTickRate(): number {\n        return this.game.speed.value * GameSpeed.BASE_TICKS_PER_SECOND;\n    }\n    getBaseTickRate(): number {\n        return GameSpeed.BASE_TICKS_PER_SECOND;\n    }\n    getCurrentTick(): number {\n        return this.game.currentTick;\n    }\n    getCurrentTime(): number {\n        return this.game.currentTime / 1000;\n    }\n    private getWeaponData(weapon: any): WeaponData {\n        return {\n            type: weapon.type,\n            rules: weapon.rules,\n            projectileRules: weapon.projectileRules,\n            warheadRules: weapon.warhead.rules,\n            minRange: weapon.minRange,\n            maxRange: weapon.range,\n            speed: weapon.speed,\n            cooldownTicks: weapon.getCooldownTicks(),\n        };\n    }\n}\n"
  },
  {
    "path": "src/game/api/LoggerApi.ts",
    "content": "import { formatTimeDuration } from '@/util/format';\nimport { AppLogger } from '@/util/logger';\nexport class LoggerApi {\n    private logger: typeof AppLogger;\n    private gameTime: {\n        getCurrentTime(): number;\n    };\n    constructor(logger: typeof AppLogger, gameTime: {\n        getCurrentTime(): number;\n    }) {\n        this.logger = logger;\n        this.gameTime = gameTime;\n    }\n    setDebugLevel(debug: boolean): void {\n        this.logger.setLevel(debug ? AppLogger.DEBUG : AppLogger.INFO);\n    }\n    debug(...args: any[]): void {\n        this.logger.debug(this.getTimePrefix(), ...args);\n    }\n    info(...args: any[]): void {\n        this.logger.info(this.getTimePrefix(), ...args);\n    }\n    log(...args: any[]): void {\n        this.logger.log(this.getTimePrefix(), ...args);\n    }\n    warn(...args: any[]): void {\n        this.logger.warn(this.getTimePrefix(), ...args);\n    }\n    error(...args: any[]): void {\n        this.logger.error(this.getTimePrefix(), ...args);\n    }\n    time(label: string): void {\n        this.logger.time(label);\n    }\n    timeEnd(label: string): void {\n        this.logger.timeEnd(label);\n    }\n    private getTimePrefix(): string {\n        return `[${formatTimeDuration(Math.floor(this.gameTime.getCurrentTime()))}]`;\n    }\n}\n"
  },
  {
    "path": "src/game/api/MapApi.ts",
    "content": "import { SpeedType } from '@/game/type/SpeedType';\nimport { TiberiumTrait } from '@/game/gameobject/trait/TiberiumTrait';\nimport { TiberiumType } from '@/engine/type/TiberiumType';\nimport { Vector2 } from '@/game/math/Vector2';\ninterface Game {\n    map: Map;\n    getPlayerByName(name: string): Player;\n    getWorld(): World;\n    mapShroudTrait: {\n        getPlayerShroud(player: Player): {\n            isShrouded(tile: any, level: number): boolean;\n            revealAll?(): void;\n        } | undefined;\n    };\n}\ninterface Map {\n    tiles: {\n        getMapSize(): any;\n        getByMapCoords(x: number, y: number): any;\n        getInRectangle(rect: any, rect2?: any): any[];\n    };\n    startingLocations: {\n        x: number;\n        y: number;\n    }[];\n    getTheaterType(): any;\n    mapBounds: {\n        isWithinBounds(tile: any): boolean;\n    };\n    getObjectsOnTile(tile: any): any[];\n    tileOccupation: {\n        getBridgeOnTile(tile: any): {\n            isHighBridge(): boolean;\n        };\n    };\n    terrain: {\n        getPassableSpeed(tile: any, speedType: SpeedType, isFoot: boolean, options: any): number;\n        computePath(tile: any, isFoot: boolean, startTile: any, startOnBridge: boolean, endTile: any, endOnBridge: boolean, options: any): any[];\n        getIslandIdMap?(speedType: SpeedType, onBridge: boolean): {\n            get(tile: any, onBridge: boolean): number | undefined;\n        };\n    };\n}\ninterface Player {\n}\ninterface World {\n    getAllObjects(): any[];\n}\ninterface Tile {\n    onBridgeLandType?: any;\n    id: string;\n    isOverlay(): boolean;\n    isTiberium(): boolean;\n    isTerrain(): boolean;\n    rules: {\n        spawnsTiberium: boolean;\n    };\n    traits: {\n        get(trait: any): any;\n    };\n    tile: any;\n}\ninterface ResourceData {\n    tile: any;\n    ore: number;\n    gems: number;\n    spawnsOre: boolean;\n}\nexport class MapApi {\n    private game: Game;\n    private map: Map;\n    constructor(game: Game) {\n        this.game = game;\n        this.map = game.map;\n    }\n    getRealMapSize() {\n        return this.map.tiles.getMapSize();\n    }\n    getStartingLocations() {\n        return this.map.startingLocations.map(loc => new Vector2(loc.x, loc.y));\n    }\n    getTheaterType() {\n        return this.map.getTheaterType();\n    }\n    getTile(x: number, y: number) {\n        const tile = this.map.tiles.getByMapCoords(x, y);\n        if (tile && this.map.mapBounds.isWithinBounds(tile)) {\n            return tile;\n        }\n    }\n    getTilesInRect(rect: any, rect2?: any) {\n        const tiles = rect2\n            ? this.map.tiles.getInRectangle(rect, rect2)\n            : this.map.tiles.getInRectangle(rect);\n        return tiles.filter(tile => this.map.mapBounds.isWithinBounds(tile));\n    }\n    getObjectsOnTile(tile: Tile) {\n        return this.map.getObjectsOnTile(tile).map(obj => obj.id);\n    }\n    hasBridgeOnTile(tile: Tile) {\n        return !!tile.onBridgeLandType;\n    }\n    hasHighBridgeOnTile(tile: Tile) {\n        return !!this.map.tileOccupation.getBridgeOnTile(tile)?.isHighBridge();\n    }\n    isPassableTile(tile: any, speedType: SpeedType, options: any, isFoot?: boolean) {\n        isFoot = isFoot ?? speedType === SpeedType.Foot;\n        return this.map.terrain.getPassableSpeed(tile, speedType, isFoot, options) > 0;\n    }\n    findPath(tile: any, ...args: any[]) {\n        const [isFoot, start, end, options] = args[0] !== 'boolean'\n            ? [tile === SpeedType.Foot, ...args]\n            : args;\n        const path = this.game.map.terrain.computePath(tile, isFoot, start.tile, start.onBridge, end.tile, end.onBridge, {\n            bestEffort: options?.bestEffort,\n            excludeTiles: options?.excludeNodes\n                ? (node: any) => options.excludeNodes({\n                    tile: node.tile,\n                    onBridge: !!node.onBridge,\n                })\n                : undefined,\n            maxExpandedNodes: options?.maxExpandedNodes,\n        });\n        return path.map(node => ({ tile: node.tile, onBridge: !!node.onBridge }));\n    }\n    isVisibleTile(tile: any, playerName: string, level: number = 0) {\n        const player = this.game.getPlayerByName(playerName);\n        if (!player) {\n            throw new Error(`Player \"${playerName}\" doesn't exist`);\n        }\n        return !this.game.mapShroudTrait.getPlayerShroud(player)?.isShrouded(tile, level);\n    }\n    private getResourceData(obj: any): ResourceData | undefined {\n        if (obj.isOverlay() && obj.isTiberium()) {\n            const trait = obj.traits.get(TiberiumTrait);\n            const type = trait.getTiberiumType();\n            const count = trait.getBailCount();\n            return {\n                tile: obj.tile,\n                ore: type === TiberiumType.Ore ? count : 0,\n                gems: type === TiberiumType.Gems ? count : 0,\n                spawnsOre: false,\n            };\n        }\n        else if (obj.isTerrain() && obj.rules.spawnsTiberium) {\n            return {\n                tile: obj.tile,\n                ore: 0,\n                gems: 0,\n                spawnsOre: true,\n            };\n        }\n    }\n    getTileResourceData(tile: any) {\n        const obj = this.map.getObjectsOnTile(tile).find(obj => (obj.isOverlay() && obj.isTiberium()) || (obj.isTerrain() && obj.rules.spawnsTiberium));\n        return obj ? this.getResourceData(obj) : undefined;\n    }\n    getAllTilesResourceData() {\n        const data: ResourceData[] = [];\n        for (const obj of this.game.getWorld().getAllObjects()) {\n            const resourceData = this.getResourceData(obj);\n            if (resourceData) {\n                data.push(resourceData);\n            }\n        }\n        return data;\n    }\n    getReachabilityMap(speedType: SpeedType, onBridge: boolean) {\n        const terrain = this.map.terrain as any;\n        const islandIdMap = terrain.getIslandIdMap(speedType, onBridge);\n        return {\n            isReachable(from: { tile: any; onBridge?: boolean }, to: { tile: any; onBridge?: boolean }): boolean {\n                const fromId = islandIdMap.get(from.tile, !!from.onBridge);\n                const toId = islandIdMap.get(to.tile, !!to.onBridge);\n                return fromId !== undefined && fromId === toId;\n            },\n        };\n    }\n}\n"
  },
  {
    "path": "src/game/api/ProductionApi.ts",
    "content": "export class ProductionApi {\n    private readonly production: any;\n    constructor(production: any) {\n        this.production = production;\n    }\n    isAvailableForProduction(object: any): boolean {\n        return this.production.isAvailableForProduction(object);\n    }\n    getAvailableObjects(queueType?: any): any[] {\n        let objects = this.production.getAvailableObjects();\n        if (queueType !== undefined) {\n            objects = objects.filter(obj => this.getQueueTypeForObject(obj) === queueType);\n        }\n        return objects;\n    }\n    getQueueTypeForObject(object: any): any {\n        return this.production.getQueueTypeForObject(object);\n    }\n    getQueueData(queue: any): {\n        size: number;\n        maxSize: number;\n        status: any;\n        type: any;\n        items: Array<{\n            rules: any;\n            quantity: number;\n        }>;\n    } {\n        const queueData = this.production.getQueue(queue);\n        return {\n            size: queueData.currentSize,\n            maxSize: queueData.maxSize,\n            status: queueData.status,\n            type: queueData.type,\n            items: queueData.getAll().map(item => ({\n                rules: item.rules,\n                quantity: item.quantity\n            }))\n        };\n    }\n}\n"
  },
  {
    "path": "src/game/api/RulesApi.ts",
    "content": "export class RulesApi {\n    private rules: any;\n    constructor(rules: any) {\n        this.rules = rules;\n    }\n    get allObjectRules() {\n        return this.rules.allObjectRules;\n    }\n    get buildingRules() {\n        return this.rules.buildingRules;\n    }\n    get infantryRules() {\n        return this.rules.infantryRules;\n    }\n    get vehicleRules() {\n        return this.rules.vehicleRules;\n    }\n    get aircraftRules() {\n        return this.rules.aircraftRules;\n    }\n    get terrainRules() {\n        return this.rules.terrainRules;\n    }\n    get overlayRules() {\n        return this.rules.overlayRules;\n    }\n    get countryRules() {\n        return this.rules.countryRules;\n    }\n    get general() {\n        return this.rules.general;\n    }\n    get ai() {\n        return this.rules.ai;\n    }\n    get crateRules() {\n        return this.rules.crateRules;\n    }\n    get combatDamage() {\n        return this.rules.combatDamage;\n    }\n    get radiation() {\n        return this.rules.radiation;\n    }\n    hasObject(type: string, id: string): boolean {\n        return this.rules.hasObject(type, id);\n    }\n    getObject(type: string, id: string): any {\n        return this.rules.getObject(type, id);\n    }\n    getBuilding(id: string): any {\n        return this.rules.getBuilding(id);\n    }\n    getWeapon(id: string): any {\n        return this.rules.getWeapon(id);\n    }\n    getWarhead(id: string): any {\n        return this.rules.getWarhead(id);\n    }\n    getProjectile(id: string): any {\n        return this.rules.getProjectile(id);\n    }\n    getOverlayName(id: string): string {\n        return this.rules.getOverlayName(id);\n    }\n    getOverlayId(name: string): string {\n        return this.rules.getOverlayId(name);\n    }\n    getOverlay(id: string): any {\n        return this.rules.getOverlay(id);\n    }\n    getCountry(id: string): any {\n        return this.rules.getCountry(id);\n    }\n    getMultiplayerCountries(): any[] {\n        return this.rules.getMultiplayerCountries();\n    }\n    getIni(): any {\n        return this.rules.getIni();\n    }\n}\n"
  },
  {
    "path": "src/game/api/index.ts",
    "content": "import { Bot } from '@/game/bot/Bot';\nimport { ApiEventType } from '@/game/api/EventsApi';\nimport { GameMath } from '@/game/math/GameMath';\nimport { Box2 } from '@/game/math/Box2';\nimport { Vector2 } from '@/game/math/Vector2';\nimport { Vector3 } from '@/game/math/Vector3';\nimport { Euler } from '@/game/math/Euler';\nimport { Quaternion } from '@/game/math/Quaternion';\nimport { Matrix4 } from '@/game/math/Matrix4';\nimport { Spherical } from '@/game/math/Spherical';\nimport { Cylindrical } from '@/game/math/Cylindrical';\nimport { TheaterType } from '@/engine/TheaterType';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { BuildStatus } from '@/game/gameobject/Building';\nimport { StanceType } from '@/game/gameobject/infantry/StanceType';\nimport { ZoneType } from '@/game/gameobject/unit/ZoneType';\nimport { AttackState } from '@/game/gameobject/trait/AttackTrait';\nimport { FactoryStatus } from '@/game/gameobject/trait/FactoryTrait';\nimport { VeteranLevel } from '@/game/gameobject/unit/VeteranLevel';\nimport { OrderType } from '@/game/order/OrderType';\nimport { QueueType, QueueStatus } from '@/game/player/production/ProductionQueue';\nimport { PrereqCategory } from '@/game/rules/GeneralRules';\nimport { RadarEventType } from '@/game/rules/general/RadarRules';\nimport { SpeedType } from '@/game/type/SpeedType';\nimport { WeaponType } from '@/game/WeaponType';\nimport { FactoryType, BuildCat } from '@/game/rules/TechnoRules';\nimport { TagRepeatType } from '@/data/map/tag/TagRepeatType';\nimport { TerrainType } from '@/engine/type/TerrainType';\nimport { InfDeathType } from '@/game/gameobject/infantry/InfDeathType';\nimport { VeteranAbility } from '@/game/gameobject/unit/VeteranAbility';\nimport { SideType } from '@/game/SideType';\nimport { ArmorType } from '@/game/type/ArmorType';\nimport { LandTargeting } from '@/game/type/LandTargeting';\nimport { LandType } from '@/game/type/LandType';\nimport { LocomotorType } from '@/game/type/LocomotorType';\nimport { MovementZone } from '@/game/type/MovementZone';\nimport { NavalTargeting } from '@/game/type/NavalTargeting';\nimport { PipColor } from '@/game/type/PipColor';\nimport { SuperWeaponType } from '@/game/type/SuperWeaponType';\nimport { SuperWeaponStatus } from '@/game/SuperWeapon';\nimport { VhpScan } from '@/game/type/VhpScan';\nexport { Bot, ApiEventType, GameMath, Box2, Vector2, Vector3, Euler, Quaternion, Matrix4, Spherical, Cylindrical, TheaterType, ObjectType, BuildStatus, StanceType, ZoneType, AttackState, FactoryStatus, VeteranLevel, OrderType, QueueType, QueueStatus, PrereqCategory, RadarEventType, SpeedType, WeaponType, FactoryType, BuildCat, TagRepeatType, TerrainType, InfDeathType, VeteranAbility, SideType, ArmorType, LandTargeting, LandType, LocomotorType, MovementZone, NavalTargeting, PipColor, SuperWeaponType, SuperWeaponStatus, VhpScan };\n"
  },
  {
    "path": "src/game/api/interface/BuildingPlacementData.ts",
    "content": "export interface BuildingPlacementData {\n    id: string;\n}\n"
  },
  {
    "path": "src/game/api/interface/GameObjectData.ts",
    "content": "export interface GameObjectData {\n    id: any;\n    type: string;\n    name: string;\n    rules: any;\n    tile: any;\n    tileElevation: number;\n    worldPosition: any;\n    foundation: any;\n    hitPoints?: number;\n    maxHitPoints?: number;\n    owner?: string;\n}\n"
  },
  {
    "path": "src/game/api/interface/PathFinderOptions.ts",
    "content": "export interface PathFinderOptions {\n    id: string;\n}\n"
  },
  {
    "path": "src/game/api/interface/PathNode.ts",
    "content": "export interface PathNode {\n    id: string;\n}\n"
  },
  {
    "path": "src/game/api/interface/PlayerData.ts",
    "content": "export interface PlayerData {\n    name: string;\n    country: any;\n    startLocation: any;\n    isObserver: boolean;\n    isAi: boolean;\n    isCombatant: boolean;\n    credits: number;\n    power: {\n        total: number;\n        drain: number;\n        isLowPower: boolean;\n    };\n    radarDisabled: boolean;\n}\n"
  },
  {
    "path": "src/game/api/interface/PlayerStats.ts",
    "content": "export interface PlayerStats {\n    id: string;\n}\n"
  },
  {
    "path": "src/game/api/interface/SuperWeaponData.ts",
    "content": "export interface SuperWeaponData {\n    id: string;\n}\n"
  },
  {
    "path": "src/game/api/interface/TileResourceData.ts",
    "content": "export interface TileResourceData {\n    id: string;\n}\n"
  },
  {
    "path": "src/game/api/interface/UnitData.ts",
    "content": "import { GameObjectData } from './GameObjectData';\n\nexport interface UnitData extends GameObjectData {\n    owner: string;\n    sight: number;\n    veteranLevel: number;\n    guardMode: boolean;\n    purchaseValue: number;\n    primaryWeapon?: any;\n    secondaryWeapon?: any;\n    deathWeapon?: any;\n    attackState?: string;\n    direction: number;\n    onBridge?: boolean;\n    zone?: any;\n    buildStatus?: string;\n    factory?: {\n        deliveringUnit?: string;\n        status: string;\n    };\n    rallyPoint?: any;\n    isPoweredOn?: boolean;\n    hasWrenchRepair?: boolean;\n    turretFacing?: number;\n    turretNo?: number;\n    garrisonUnitCount?: number;\n    garrisonUnitsMax?: number;\n    passengerSlotCount?: number;\n    passengerSlotMax?: number;\n    isIdle: boolean;\n    canMove?: boolean;\n    velocity?: any;\n    stance?: string;\n    harvestedOre?: number;\n    harvestedGems?: number;\n    ammo?: number;\n    isWarpedOut: boolean;\n    mindControlledBy?: string;\n    tntTimer?: number;\n}\n"
  },
  {
    "path": "src/game/art/Art.ts",
    "content": "import { ObjectArt } from './ObjectArt';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { ObjectRules } from '@/game/rules/ObjectRules';\nimport { IniSection } from '@/data/IniSection';\nexport class Art {\n    private rules: any;\n    private artIni: any;\n    private mapFile: any;\n    private logger: any;\n    private objectArt: Map<any, Map<string, ObjectArt>>;\n    constructor(rules: any, artIni: any, mapFile: any, logger: any) {\n        this.rules = rules;\n        this.artIni = artIni;\n        this.mapFile = mapFile;\n        this.logger = logger;\n        this.objectArt = new Map();\n        this.parse();\n    }\n    hasObject(name: string, type: ObjectType): boolean {\n        return this.objectArt.get(type)?.has(name) ?? false;\n    }\n    getObject(name: string, type: ObjectType): ObjectArt {\n        if (!name) {\n            throw new Error(`Must specify an art name for type \"${ObjectType[type]}\"`);\n        }\n        const art = this.objectArt.get(type)?.get(name);\n        if (art) {\n            return art;\n        }\n        this.logger?.debug(`Missing art for object \"${name}\"`);\n        return new ObjectArt(type, this.rules.hasObject(name, type)\n            ? this.rules.getObject(name, type)\n            : new ObjectRules(type, new IniSection(name)), new IniSection(name));\n    }\n    getAnimation(name: string): ObjectArt {\n        return this.getObject(name, ObjectType.Animation);\n    }\n    getProjectile(name: string): ObjectArt {\n        const projectile = this.rules.getProjectile(name);\n        const imageName = projectile.imageName;\n        let section = this.artIni.getSection(imageName);\n        if (!section) {\n            this.logger?.debug(`Image ${imageName} (Projectile: ${name}) has no section in art.ini`);\n            section = new IniSection(imageName);\n        }\n        return ObjectArt.factory(projectile.type, projectile, this.artIni, section);\n    }\n    getIni(): any {\n        return this.artIni;\n    }\n    private parse(): void {\n        this.rules.allObjectRules.forEach((rules: any[], type: ObjectType) => {\n            const artMap = new Map<string, ObjectArt>();\n            this.objectArt.set(type, artMap);\n            rules.forEach((rule) => {\n                const imageSection = this.artIni.getSection(rule.imageName);\n                const nameSection = this.artIni.getSection(rule.name);\n                const section = this.applyUnitMapOverrides(rule, this.mapFile, nameSection, imageSection);\n                if (section) {\n                    const art = ObjectArt.factory(rule.type, rule, this.artIni, section);\n                    artMap.set(rule.name, art);\n                }\n                else {\n                    this.logger?.debug(`${ObjectType[rule.type]} \"${rule.name}\" has no art section \"${rule.imageName}\"`);\n                }\n            });\n        });\n        const animations = [[ObjectType.Animation, this.rules.animationNames]];\n        animations.forEach(([type, names]) => {\n            const artMap = new Map<string, ObjectArt>();\n            this.objectArt.set(type, artMap);\n            names.forEach((name: string) => {\n                const section = this.artIni.getSection(name);\n                if (section) {\n                    const rules = new ObjectRules(type, new IniSection(name));\n                    const art = new ObjectArt(type, rules as any, section);\n                    artMap.set(name, art);\n                }\n                else {\n                    this.logger?.debug(`${ObjectType[type]} \"${name}\" has no art section`);\n                }\n            });\n        });\n    }\n    private applyUnitMapOverrides(rule: any, mapFile: any, nameSection: any, imageSection: any): any {\n        if ([ObjectType.Infantry, ObjectType.Vehicle, ObjectType.Aircraft].includes(rule.type) &&\n            mapFile?.getSection(rule.name)?.getString(\"Image\") &&\n            nameSection) {\n            const mergedSection = nameSection.clone();\n            imageSection?.entries.forEach((value: any, key: string) => {\n                mergedSection.set(key, value);\n            });\n            this.logger?.debug(`${ObjectType[rule.type]} \"${rule.name}\": ` +\n                `Using merged art sections ${rule.name} and ${rule.imageName}`);\n            return mergedSection;\n        }\n        return imageSection;\n    }\n}\n"
  },
  {
    "path": "src/game/art/FlhCoords.ts",
    "content": "export class FlhCoords {\n    public forward: number;\n    public lateral: number;\n    public vertical: number;\n    constructor(coords?: number[]) {\n        this.forward = 0;\n        this.lateral = 0;\n        this.vertical = 0;\n        if (coords && coords.length === 3) {\n            this.fromArray(coords);\n        }\n    }\n    fromArray(coords: number[]): FlhCoords {\n        this.forward = coords[0];\n        this.lateral = coords[1];\n        this.vertical = coords[2];\n        return this;\n    }\n    clone(): FlhCoords {\n        return new FlhCoords([this.forward, this.lateral, this.vertical]);\n    }\n}\n"
  },
  {
    "path": "src/game/art/ObjectArt.ts",
    "content": "import { PaletteType } from \"@/engine/type/PaletteType\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { Coords } from \"@/game/Coords\";\nimport { SequenceReader } from \"@/game/art/SequenceReader\";\nimport { LightingType } from \"@/engine/type/LightingType\";\nimport { LandType } from \"@/game/type/LandType\";\nimport { OverlayRules } from \"@/game/rules/OverlayRules\";\nimport { TechnoRules } from \"@/game/rules/TechnoRules\";\nimport { TerrainRules } from \"@/game/rules/TerrainRules\";\nimport { ProjectileRules } from \"@/game/rules/ProjectileRules\";\nimport { FlhCoords } from \"@/game/art/FlhCoords\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { Vector3 } from \"@/game/math/Vector3\";\nimport { SequenceType } from \"@/game/art/SequenceType\";\ninterface ArtSection {\n    getString(key: string, defaultValue?: string): string | undefined;\n    getBool(key: string, defaultValue?: boolean): boolean;\n    getNumber(key: string, defaultValue?: number): number;\n    getNumberArray(key: string, separator?: RegExp, defaultValue?: number[]): number[];\n    getArray(key: string): string[];\n    has(key: string): boolean;\n}\ninterface RulesBase {\n    imageName: string;\n    alternateArcticArt?: boolean;\n    noShadow?: boolean;\n}\ninterface BuildingRules extends RulesBase {\n    numberOfDocks: number;\n}\ninterface IniSection {\n}\ninterface Rotor {\n    name: string;\n    axis: Vector3;\n    speed?: number;\n    idleSpeed?: number;\n}\ninterface MuzzleFlash {\n    x: number;\n    y: number;\n}\ninterface Foundation {\n    width: number;\n    height: number;\n}\nexport class ObjectArt {\n    public static readonly DEFAULT_LINE_TRAIL_DEC = 16;\n    private static readonly MISSING_CAMEO = \"xxicon\";\n    public sequences: Map<SequenceType, any> = new Map();\n    public dockingOffsets: Vector3[] = [];\n    public type: ObjectType;\n    public rules: RulesBase;\n    public art: ArtSection;\n    public image: string = \"\";\n    public report?: string;\n    public rotors?: Rotor[];\n    public noHva: boolean = false;\n    public startSound?: string;\n    public muzzleFlash?: MuzzleFlash[];\n    public paletteType: PaletteType = PaletteType.Default;\n    public lightingType: LightingType = LightingType.Default;\n    public customPaletteName?: string;\n    public remapable: boolean = false;\n    public flat: boolean = false;\n    public queueingCell?: Vector2;\n    public demandLoad: boolean = false;\n    public useLineTrail: boolean = false;\n    public lineTrailColor: number[] = [];\n    public lineTrailColorDecrement: number = ObjectArt.DEFAULT_LINE_TRAIL_DEC;\n    public crater: boolean = false;\n    public forceBigCraters: boolean = false;\n    public scorch: boolean = false;\n    public height: number = 0;\n    public isVoxel: boolean = false;\n    public occupyHeight: number = 0;\n    public canHideThings: boolean = false;\n    public canBeHidden: boolean = true;\n    public addOccupy: Vector2[] = [];\n    public removeOccupy: Vector2[] = [];\n    public rotates: boolean = false;\n    static getDefaultPalette(objectType: ObjectType): PaletteType {\n        switch (objectType) {\n            case ObjectType.Building:\n            case ObjectType.Aircraft:\n            case ObjectType.Infantry:\n            case ObjectType.Vehicle:\n            case ObjectType.Projectile:\n            case ObjectType.VoxelAnim:\n                return PaletteType.Unit;\n            case ObjectType.Overlay:\n                return PaletteType.Overlay;\n            case ObjectType.Smudge:\n            case ObjectType.Terrain:\n                return PaletteType.Iso;\n            default:\n                ObjectType.Animation;\n                return PaletteType.Anim;\n        }\n    }\n    static getDefaultLighting(objectType: ObjectType): LightingType {\n        switch (objectType) {\n            case ObjectType.Animation:\n                return LightingType.None;\n            case ObjectType.Aircraft:\n            case ObjectType.Building:\n            case ObjectType.Infantry:\n            case ObjectType.Vehicle:\n                return LightingType.Ambient;\n            case ObjectType.Projectile:\n            case ObjectType.VoxelAnim:\n                return LightingType.Global;\n            case ObjectType.Overlay:\n            case ObjectType.Smudge:\n            case ObjectType.Terrain:\n            default:\n                return LightingType.Full;\n        }\n    }\n    static getDefaultRemapability(objectType: ObjectType): boolean {\n        switch (objectType) {\n            case ObjectType.Aircraft:\n            case ObjectType.Building:\n            case ObjectType.Infantry:\n            case ObjectType.Vehicle:\n                return true;\n            case ObjectType.Overlay:\n            case ObjectType.Smudge:\n            case ObjectType.Terrain:\n            case ObjectType.Animation:\n            case ObjectType.Projectile:\n            case ObjectType.VoxelAnim:\n                return false;\n            default:\n                throw new Error(\"Unknown object type \" + objectType);\n        }\n    }\n    static getDefaultDrawOffset(objectType: ObjectType): Vector2 {\n        switch (objectType) {\n            case ObjectType.Animation:\n            case ObjectType.Building:\n            case ObjectType.Vehicle:\n            case ObjectType.Infantry:\n            case ObjectType.Overlay:\n            case ObjectType.Smudge:\n            case ObjectType.Projectile:\n            case ObjectType.VoxelAnim:\n                return new Vector2(0, 0);\n            case ObjectType.Terrain:\n            case ObjectType.Aircraft:\n                return new Vector2(0, (Coords.ISO_TILE_SIZE + 1) / 2);\n            default:\n                throw new Error(\"Unknown object type \" + objectType);\n        }\n    }\n    static getDefaultShadow(objectType: ObjectType): boolean {\n        switch (objectType) {\n            case ObjectType.Overlay:\n            case ObjectType.Building:\n            case ObjectType.Infantry:\n            case ObjectType.Terrain:\n            case ObjectType.Vehicle:\n            case ObjectType.Aircraft:\n                return true;\n            default:\n            case ObjectType.Smudge:\n            case ObjectType.Animation:\n            case ObjectType.Projectile:\n            case ObjectType.VoxelAnim:\n                return false;\n        }\n    }\n    static getDefaultHeight(objectType: ObjectType): number {\n        switch (objectType) {\n            case ObjectType.Building:\n                return 2;\n            case ObjectType.Infantry:\n            case ObjectType.Vehicle:\n            case ObjectType.Aircraft:\n                return 1;\n            default:\n                return 0;\n        }\n    }\n    static factory(objectType: ObjectType, rules: RulesBase, iniData: any, art: ArtSection): ObjectArt {\n        const result = new this(objectType, rules, art);\n        if (objectType === ObjectType.Infantry) {\n            const sequenceName = art.getString(\"Sequence\");\n            if (sequenceName) {\n                const sequenceSection = iniData.getSection(sequenceName);\n                if (sequenceSection) {\n                    result.sequences = new SequenceReader().readIni(sequenceSection);\n                }\n            }\n        }\n        return result;\n    }\n    constructor(objectType: ObjectType, rules: RulesBase, art: ArtSection) {\n        this.type = objectType;\n        this.rules = rules;\n        this.art = art;\n        this.init();\n    }\n    private init(): void {\n        this.image = [ObjectType.Infantry, ObjectType.Vehicle, ObjectType.Aircraft].includes(this.type)\n            ? \"\"\n            : this.art.getString(\"Image\") || \"\";\n        this.report = this.art.getString(\"Report\");\n        this.readRotors();\n        this.noHva = this.art.getBool(\"NoHVA\", false);\n        this.startSound = this.art.getString(\"StartSound\");\n        this.readMuzzleFlash();\n        this.readPaletteAndLightingTypes();\n        this.readRemapability();\n        this.readFlatness();\n        this.readDockingOffsets();\n        const queueingCellArray = this.art.getNumberArray(\"QueueingCell\");\n        this.queueingCell = queueingCellArray.length\n            ? new Vector2(queueingCellArray[0], queueingCellArray[1])\n            : undefined;\n        this.demandLoad = this.art.getBool(\"DemandLoad\", false);\n        const useLineTrail = this.art.getBool(\"UseLineTrail\", false);\n        const lineTrailColorArray = this.art.getNumberArray(\"LineTrailColor\");\n        const lineTrailColorDecrement = this.art.getNumber(\"LineTrailColorDecrement\", ObjectArt.DEFAULT_LINE_TRAIL_DEC);\n        if (useLineTrail && lineTrailColorArray.length) {\n            this.useLineTrail = true;\n            this.lineTrailColor = lineTrailColorArray;\n            this.lineTrailColorDecrement = lineTrailColorDecrement;\n        }\n        else {\n            this.useLineTrail = false;\n        }\n        this.crater = this.art.getBool(\"Crater\", false);\n        this.forceBigCraters = this.art.getBool(\"ForceBigCraters\", false);\n        this.scorch = this.art.getBool(\"Scorch\", false);\n        this.height = this.art.getNumber(\"Height\", ObjectArt.getDefaultHeight(this.type));\n        this.isVoxel = this.art.getBool(\"Voxel\", false);\n        this.occupyHeight = this.art.getNumber(\"OccupyHeight\", this.height);\n        if (this.type === ObjectType.Building) {\n            this.canHideThings = this.art.getBool(\"CanHideThings\", true);\n        }\n        else {\n            this.canHideThings = false;\n        }\n        this.canBeHidden = this.art.getBool(\"CanBeHidden\", true);\n        this.addOccupy = this.readAddRemoveOccupy(\"AddOccupy\");\n        this.removeOccupy = this.readAddRemoveOccupy(\"RemoveOccupy\");\n        this.rotates = this.art.getBool(\"Rotates\", false);\n    }\n    get imageName(): string {\n        return (this.image || this.rules.imageName) + (this.rules.alternateArcticArt ? \"A\" : \"\");\n    }\n    get cameo(): string {\n        const cameo = this.art.getString(\"Cameo\") || ObjectArt.MISSING_CAMEO;\n        return cameo.toLowerCase();\n    }\n    get altCameo(): string {\n        const altCameo = this.art.getString(\"AltCameo\") || this.cameo;\n        return altCameo.toLowerCase();\n    }\n    get useTheaterExtension(): boolean {\n        return this.art.getBool(\"Theater\", false);\n    }\n    private readPaletteAndLightingTypes(): void {\n        this.paletteType = PaletteType.Default;\n        this.lightingType = LightingType.Default;\n        if (this.rules instanceof OverlayRules && (this.rules as any).noUseTileLandType) {\n            this.paletteType = PaletteType.Iso;\n            this.lightingType = LightingType.Full;\n        }\n        if (this.art.getBool(\"TerrainPalette\", false) || this.art.getBool(\"ShouldUseCellDrawer\", false)) {\n            this.paletteType = PaletteType.Iso;\n        }\n        else if (this.art.getBool(\"AnimPalette\", false)) {\n            this.paletteType = PaletteType.Anim;\n            this.lightingType = LightingType.None;\n        }\n        else if (this.art.getString(\"Palette\")) {\n            this.paletteType = PaletteType.Custom;\n            this.customPaletteName = this.art.getString(\"Palette\");\n        }\n        if (this.art.getBool(\"AltPalette\", false)) {\n            this.paletteType = PaletteType.Unit;\n        }\n        if ((this.rules instanceof OverlayRules || this.rules instanceof TechnoRules) && (this.rules as any).wall) {\n            this.paletteType = PaletteType.Unit;\n            this.lightingType = LightingType.Ambient;\n        }\n        if ((this.rules instanceof TerrainRules || this.rules instanceof TechnoRules) && (this.rules as any).gate) {\n            this.paletteType = PaletteType.Unit;\n        }\n        if (this.rules instanceof TerrainRules && (this.rules as any).spawnsTiberium) {\n            this.paletteType = PaletteType.Unit;\n            this.lightingType = LightingType.None;\n        }\n        if (this.rules instanceof OverlayRules) {\n            const overlayRules = this.rules as any;\n            if (overlayRules.isVeins) {\n                this.paletteType = PaletteType.Unit;\n                this.lightingType = LightingType.None;\n            }\n            if (overlayRules.isVeinholeMonster) {\n                this.paletteType = PaletteType.Unit;\n                this.lightingType = LightingType.None;\n            }\n            if (overlayRules.tiberium) {\n                this.lightingType = LightingType.None;\n            }\n            if (overlayRules.land === LandType.Railroad) {\n                this.paletteType = PaletteType.Iso;\n                this.lightingType = LightingType.Full;\n            }\n            if (overlayRules.crate) {\n                this.paletteType = PaletteType.Iso;\n                this.lightingType = LightingType.Full;\n            }\n        }\n        if (this.paletteType === PaletteType.Default) {\n            this.paletteType = ObjectArt.getDefaultPalette(this.type);\n        }\n        if (this.lightingType === LightingType.Default) {\n            this.lightingType = ObjectArt.getDefaultLighting(this.type);\n        }\n    }\n    private readRemapability(): void {\n        this.remapable = ObjectArt.getDefaultRemapability(this.type);\n        if (this.art.getBool(\"TerrainPalette\", false) || this.art.getBool(\"AnimPalette\", false)) {\n            this.remapable = false;\n        }\n        else if (this.rules instanceof ProjectileRules && (this.rules as any).firersPalette) {\n            this.remapable = true;\n        }\n    }\n    private readFlatness(): void {\n        let flat = false;\n        if (this.type === ObjectType.Building || this.type === ObjectType.Animation) {\n            flat = this.art.getBool(\"Flat\", false);\n        }\n        else if (this.type === ObjectType.Smudge) {\n            flat = true;\n        }\n        if (this.rules instanceof OverlayRules) {\n            const overlayRules = this.rules as any;\n            if (overlayRules.wall || overlayRules.crate || overlayRules.isARock) {\n                flat = true;\n            }\n        }\n        this.flat = flat;\n    }\n    private readRotors(): void {\n        const rotorNames = this.art.getArray(\"Rotors\");\n        if (rotorNames.length) {\n            const rotors: Rotor[] = [];\n            for (let i = 0; i < rotorNames.length; ++i) {\n                const axisArray = this.art.getNumberArray(`Rotor${i + 1}Axis`, undefined, [0, 1, 0]);\n                const axis = new Vector3(-axisArray[2], -axisArray[0], axisArray[1]).normalize();\n                rotors.push({\n                    name: rotorNames[i],\n                    axis: axis,\n                    speed: this.art.getNumber(`Rotor${i + 1}Rate`),\n                    idleSpeed: this.art.getNumber(`Rotor${i + 1}IdleRate`)\n                });\n            }\n            if (rotors.length) {\n                this.rotors = rotors;\n            }\n        }\n    }\n    private readMuzzleFlash(): void {\n        let index = 0;\n        let key = \"MuzzleFlash\" + index;\n        const muzzleFlashes: MuzzleFlash[] = [];\n        while (this.art.has(key)) {\n            const [x, y] = this.art.getNumberArray(key);\n            muzzleFlashes.push({ x, y });\n            index++;\n            key = \"MuzzleFlash\" + index;\n        }\n        this.muzzleFlash = muzzleFlashes.length ? muzzleFlashes : undefined;\n    }\n    private readDockingOffsets(): void {\n        if (this.type === ObjectType.Building) {\n            const numberOfDocks = (this.rules as BuildingRules).numberOfDocks;\n            for (let i = 0; i < numberOfDocks; i++) {\n                const [x, y, z] = this.art.getNumberArray(\"DockingOffset\" + i, /,\\s*/, [0, 0, 0]);\n                this.dockingOffsets.push(new Vector3(x, z, y));\n            }\n        }\n    }\n    private readAddRemoveOccupy(prefix: string): Vector2[] {\n        let index = 0;\n        const result: Vector2[] = [];\n        while (true) {\n            const coords = this.art.getNumberArray(prefix + (++index));\n            if (!coords.length)\n                break;\n            result.push(new Vector2(coords[0], coords[1]));\n        }\n        return result;\n    }\n    get bibShape(): string | undefined {\n        return this.art.getString(\"BibShape\");\n    }\n    get foundation(): Foundation {\n        const foundationStr = this.art.getString(\"Foundation\", \"1x1\")!;\n        const [widthStr, heightStr] = foundationStr.split(\"x\");\n        return {\n            width: parseInt(widthStr, 10),\n            height: parseInt(heightStr, 10)\n        };\n    }\n    get foundationCenter(): Vector2 {\n        return new Vector2(Math.floor(this.foundation.width / 2 - 0.5), Math.floor(this.foundation.height / 2 - 0.5));\n    }\n    getDrawOffset(): Vector2 {\n        if (this.rules instanceof TerrainRules && (this.rules as any).spawnsTiberium) {\n            return new Vector2(0, 0);\n        }\n        const defaultOffset = ObjectArt.getDefaultDrawOffset(this.type);\n        if (this.rules instanceof OverlayRules && (this.rules as any).isARock) {\n            defaultOffset.y += (Coords.ISO_TILE_SIZE + 1) / 2;\n        }\n        return defaultOffset;\n    }\n    get hasShadow(): boolean {\n        return this.art.getBool(\"Shadow\", ObjectArt.getDefaultShadow(this.type)) && !this.rules.noShadow;\n    }\n    get turretOffset(): number {\n        return this.art.getNumber(\"TurretOffset\", 0);\n    }\n    get facings(): number {\n        return this.art.getNumber(\"Facings\", 8);\n    }\n    get walkFrames(): number {\n        return this.art.getNumber(\"WalkFrames\", 0);\n    }\n    get firingFrames(): number {\n        return this.art.getNumber(\"FiringFrames\", 0);\n    }\n    get standingFrames(): number {\n        return this.art.getNumber(\"StandingFrames\", 1);\n    }\n    get startWalkFrame(): number {\n        return this.art.getNumber(\"StartWalkFrame\", 0);\n    }\n    get startStandFrame(): number {\n        return this.art.getNumber(\"StartStandFrame\", this.walkFrames * this.facings);\n    }\n    get startFiringFrame(): number {\n        return this.art.getNumber(\"StartFiringFrame\", (this.walkFrames + this.standingFrames) * this.facings);\n    }\n    get isFlamingGuy(): boolean {\n        return this.art.getBool(\"IsFlamingGuy\", false);\n    }\n    get runningFrames(): number {\n        return this.art.getNumber(\"RunningFrames\", 0);\n    }\n    get crawls(): boolean {\n        return this.art.getBool(\"Crawls\", true);\n    }\n    get primaryFireFlh(): FlhCoords {\n        return new FlhCoords(this.art.getNumberArray(\"PrimaryFireFLH\"));\n    }\n    get elitePrimaryFireFlh(): FlhCoords {\n        const eliteArray = this.art.getNumberArray(\"ElitePrimaryFireFLH\");\n        return eliteArray.length ? new FlhCoords(eliteArray) : this.primaryFireFlh;\n    }\n    get primaryFirePixelOffset(): number[] {\n        return this.art.getNumberArray(\"PrimaryFirePixelOffset\");\n    }\n    get secondaryFirePixelOffset(): number[] {\n        return this.art.getNumberArray(\"SecondaryFirePixelOffset\");\n    }\n    get secondaryFireFlh(): FlhCoords {\n        return new FlhCoords(this.art.getNumberArray(\"SecondaryFireFLH\"));\n    }\n    get eliteSecondaryFireFlh(): FlhCoords {\n        const eliteArray = this.art.getNumberArray(\"EliteSecondaryFireFLH\");\n        return eliteArray.length ? new FlhCoords(eliteArray) : this.secondaryFireFlh;\n    }\n    getSpecialWeaponFlh(weaponIndex: number): FlhCoords {\n        return new FlhCoords(this.art.getNumberArray(`Weapon${weaponIndex + 1}FLH`));\n    }\n    get fireUp(): number {\n        return this.art.getNumber(\"FireUp\", 0) || this.art.getNumber(\"DelayedFireDelay\", 0);\n    }\n    get isAnimDelayedFire(): boolean {\n        return this.art.getBool(\"IsAnimDelayedFire\", false);\n    }\n    get zShapePointMove(): number[] {\n        return this.art.getNumberArray(\"ZShapePointMove\");\n    }\n    get zAdjust(): number {\n        return this.art.getNumber(\"ZAdjust\", 0);\n    }\n    get trailer(): string | undefined {\n        return this.art.getString(\"Trailer\");\n    }\n    get spawnDelay(): number {\n        return this.art.getNumber(\"SpawnDelay\", 1);\n    }\n    get translucent(): boolean {\n        return this.art.getBool(\"Translucent\", false);\n    }\n    get translucency(): number {\n        let translucency = this.art.getNumber(\"Translucency\", 0);\n        translucency = (Math.floor(translucency / 25) * 25) / 100;\n        return translucency;\n    }\n}\n"
  },
  {
    "path": "src/game/art/RotorData.ts",
    "content": "export class RotorData {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/game/art/SequenceReader.ts",
    "content": "import { SequenceType } from './SequenceType';\nimport { IniSection } from '@/data/IniSection';\nconst FACING_MAP = new Map([\n    ['E', 5],\n    ['S', 3],\n    ['W', 1],\n    ['N', 7]\n]);\nexport class SequenceReader {\n    readIni(section: IniSection | Map<string, string>): Map<SequenceType, any> {\n        const entries: Map<string, string> = section instanceof IniSection ? (section.entries as Map<string, string>) : section;\n        const sequences = new Map<SequenceType, any>();\n        for (const [key, value] of entries) {\n            const type = SequenceType[key];\n            if (type !== undefined && typeof value === 'string') {\n                const parts = value.split(',');\n                const sequence = {\n                    type,\n                    startFrame: Number(parts[0]),\n                    frameCount: Number(parts[1]),\n                    facingMult: Number(parts[2]),\n                    onlyFacing: parts[3] ? FACING_MAP.get(parts[3]) : undefined\n                };\n                sequences.set(type, sequence);\n            }\n        }\n        return sequences;\n    }\n}\n"
  },
  {
    "path": "src/game/art/SequenceType.ts",
    "content": "export enum SequenceType {\n    Ready = 0,\n    Guard = 1,\n    Prone = 2,\n    Walk = 3,\n    FireUp = 4,\n    Down = 5,\n    Crawl = 6,\n    Up = 7,\n    FireProne = 8,\n    Idle1 = 9,\n    Idle2 = 10,\n    Die1 = 11,\n    Die2 = 12,\n    Hover = 13,\n    Fly = 14,\n    FireFly = 15,\n    Tumble = 16,\n    AirDeathStart = 17,\n    AirDeathFalling = 18,\n    AirDeathFinish = 19,\n    Tread = 20,\n    Swim = 21,\n    WetAttack = 22,\n    WetIdle1 = 23,\n    WetIdle2 = 24,\n    WetDie1 = 25,\n    WetDie2 = 26,\n    Deploy = 27,\n    Deployed = 28,\n    DeployedFire = 29,\n    DeployedIdle = 30,\n    Undeploy = 31,\n    Paradrop = 32,\n    Cheer = 33,\n    Panic = 34\n}\n"
  },
  {
    "path": "src/game/bot/Bot.ts",
    "content": "export class Bot {\n    public name: string;\n    public country: string;\n    public gameApi: any;\n    public actionsApi: any;\n    public productionApi: any;\n    public logger: any;\n    public debugMode: boolean = false;\n    constructor(name: string, country: string) {\n        this.name = name;\n        this.country = country;\n    }\n    get context() {\n        return {\n            game: this.gameApi,\n            player: {\n                name: this.name,\n                actions: this.actionsApi,\n                production: this.productionApi,\n            },\n        };\n    }\n    setGameApi(api: any): void {\n        this.gameApi = api;\n    }\n    setActionsApi(api: any): void {\n        this.actionsApi = api;\n    }\n    setProductionApi(api: any): void {\n        this.productionApi = api;\n    }\n    setLogger(logger: any): void {\n        this.logger = logger;\n        this.logger.setDebugLevel(this.debugMode);\n    }\n    setDebugMode(debug: boolean): Bot {\n        this.debugMode = debug;\n        this.logger?.setDebugLevel(debug);\n        return this;\n    }\n    getDebugMode(): boolean {\n        return this.debugMode;\n    }\n    onGameStart(_event: any): void { }\n    onGameTick(_event: any): void { }\n    onGameEvent(_event: any, _data: any): void { }\n}\n"
  },
  {
    "path": "src/game/bot/BotFactory.ts",
    "content": "import { AiDifficulty } from '../gameopts/GameOpts';\nimport { Bot } from './Bot';\nimport { DummyBot } from './DummyBot';\nimport { BuiltInBotAdapter } from '../ai/thirdpartbot/builtIn/BuiltInBotAdapter';\nimport { BotRegistry } from '../ai/thirdpartbot/BotRegistry';\nimport { ThirdPartyBotAdapter } from '../ai/thirdpartbot/ThirdPartyBotAdapter';\nexport class BotFactory {\n    private botsLib: any;\n    constructor(botsLib: any) {\n        this.botsLib = botsLib;\n    }\n    create(player: {\n        isAi: boolean;\n        name: string;\n        aiDifficulty: AiDifficulty;\n        country: {\n            name: string;\n        };\n        customBotId?: string;\n    }): Bot {\n        if (!player.isAi) {\n            throw new Error(`Player \"${player.name}\" is not an AI`);\n        }\n\n        if (player.aiDifficulty === AiDifficulty.Custom) {\n            const registry = BotRegistry.getInstance();\n            if (player.customBotId) {\n                const meta = registry.get(player.customBotId);\n                if (meta) {\n                    console.info(`[BotFactory] Using bot \"${meta.displayName}\" for \"${player.name}\"`);\n                    return new ThirdPartyBotAdapter(player.name, player.country.name, meta);\n                }\n                console.warn(`[BotFactory] Custom bot \"${player.customBotId}\" not found, trying fallback`);\n            }\n            const uploadedBots = registry.getUploadedBots();\n            if (uploadedBots.length > 0) {\n                const meta = uploadedBots[0];\n                console.info(`[BotFactory] Using uploaded bot \"${meta.displayName}\" for \"${player.name}\"`);\n                return new ThirdPartyBotAdapter(player.name, player.country.name, meta);\n            }\n            console.warn(`[BotFactory] Custom AI selected but no uploaded bot found, falling back to BuiltInBotAdapter`);\n            return new BuiltInBotAdapter(player.name, player.country.name);\n        }\n        if (player.aiDifficulty === AiDifficulty.Normal) {\n            return new BuiltInBotAdapter(player.name, player.country.name);\n        }\n        if (player.aiDifficulty === AiDifficulty.Easy ||\n            player.aiDifficulty === AiDifficulty.Medium ||\n            player.aiDifficulty === AiDifficulty.MediumSea ||\n            player.aiDifficulty === AiDifficulty.Brutal) {\n            return new DummyBot(player.name, player.country.name);\n        }\n        throw new Error(`Unsupported AI difficulty \"${player.aiDifficulty}\"`);\n    }\n}\n"
  },
  {
    "path": "src/game/bot/BotsLib.ts",
    "content": "export class BotsLib {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/game/bot/DummyBot.ts",
    "content": "import { Bot } from './Bot';\nimport { OrderType } from '../order/OrderType';\nexport enum BotState {\n    Initial = 0,\n    Deployed = 1,\n    Attacking = 2,\n    Defeated = 3\n}\nexport class DummyBot extends Bot {\n    private botState: BotState = BotState.Initial;\n    private tickRatio: number = 0;\n    private enemyPlayers: string[] = [];\n    constructor(name: string, country: string) {\n        super(name, country);\n    }\n    onGameStart(event: any): void {\n        const tickRate = event.getTickRate();\n        this.tickRatio = Math.ceil(tickRate / 5);\n        this.enemyPlayers = event.getPlayers().filter((player: string) => player !== this.name && !event.areAlliedPlayers(this.name, player));\n    }\n    onGameTick(event: any): void {\n        if (event.getCurrentTick() % this.tickRatio === 0) {\n            switch (this.botState) {\n                case BotState.Initial: {\n                    const baseUnit = event.getGeneralRules().baseUnit;\n                    if (event.getVisibleUnits(this.name, \"self\", (unit: any) => unit.constructionYard).length) {\n                        this.botState = BotState.Deployed;\n                        break;\n                    }\n                    const units = event.getVisibleUnits(this.name, \"self\", (unit: any) => baseUnit.includes(unit.name));\n                    if (units.length) {\n                        this.actionsApi.orderUnits([units[0]], OrderType.DeploySelected);\n                    }\n                    break;\n                }\n                case BotState.Deployed:\n                    break;\n                case BotState.Attacking:\n                    if (!event.getVisibleUnits(this.name, \"self\", (unit: any) => unit.isSelectableCombatant).length) {\n                        this.botState = BotState.Defeated;\n                        this.actionsApi.quitGame();\n                    }\n                    break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/event/AllianceChangeEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport enum AllianceEventType {\n    Requested = 0,\n    Formed = 1,\n    Broken = 2\n}\nexport class AllianceChangeEvent {\n    public readonly type: EventType;\n    constructor(public readonly alliance: any, public readonly changeType: AllianceEventType, public readonly from: any) {\n        this.type = EventType.AllianceChange;\n    }\n}\n"
  },
  {
    "path": "src/game/event/BridgeRepairEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class BridgeRepairEvent {\n    public readonly type: EventType;\n    constructor(public readonly source: any, public readonly tile: any) {\n        this.type = EventType.BridgeRepair;\n    }\n}\n"
  },
  {
    "path": "src/game/event/BuildStatusChangeEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class BuildStatusChangeEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly status: any) {\n        this.type = EventType.BuildStatusChange;\n    }\n}\n"
  },
  {
    "path": "src/game/event/BuildingCaptureEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class BuildingCaptureEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.BuildingCapture;\n    }\n}\n"
  },
  {
    "path": "src/game/event/BuildingEvacuateEvent.ts",
    "content": "import { EventType } from './EventType';\nexport class BuildingEvacuateEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly player: any) {\n        this.type = EventType.BuildingEvacuate;\n    }\n}\n"
  },
  {
    "path": "src/game/event/BuildingFailedPlaceEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class BuildingFailedPlaceEvent {\n    public readonly type: EventType;\n    constructor(public readonly name: string, public readonly player: any, public readonly tile: any) {\n        this.type = EventType.BuildingFailedPlace;\n    }\n}\n"
  },
  {
    "path": "src/game/event/BuildingGarrisonEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class BuildingGarrisonEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.BuildingGarrison;\n    }\n}\n"
  },
  {
    "path": "src/game/event/BuildingInfiltrationEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class BuildingInfiltrationEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly source: any) {\n        this.type = EventType.BuildingInfiltration;\n    }\n}\n"
  },
  {
    "path": "src/game/event/BuildingPlaceEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class BuildingPlaceEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.BuildingPlace;\n    }\n}\n"
  },
  {
    "path": "src/game/event/BuildingRepairFullEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class BuildingRepairFullEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly source: any) {\n        this.type = EventType.BuildingRepairFull;\n    }\n}\n"
  },
  {
    "path": "src/game/event/BuildingRepairStartEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class BuildingRepairStartEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.BuildingRepairStart;\n    }\n}\n"
  },
  {
    "path": "src/game/event/CheerEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class CheerEvent {\n    public readonly type: EventType;\n    constructor(public readonly player: any) {\n        this.type = EventType.Cheer;\n    }\n}\n"
  },
  {
    "path": "src/game/event/CratePickupEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class CratePickupEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly player: any, public readonly source: any, public readonly tile: any) {\n        this.type = EventType.CratePickup;\n    }\n}\n"
  },
  {
    "path": "src/game/event/DeployNotAllowedEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class DeployNotAllowedEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.DeployNotAllowed;\n    }\n}\n"
  },
  {
    "path": "src/game/event/EnterObjectEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class EnterObjectEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly source: any) {\n        this.type = EventType.EnterObject;\n    }\n}\n"
  },
  {
    "path": "src/game/event/EnterTileEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class EnterTileEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly source: any) {\n        this.type = EventType.EnterTile;\n    }\n}\n"
  },
  {
    "path": "src/game/event/EnterTransportEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class EnterTransportEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.EnterTransport;\n    }\n}\n"
  },
  {
    "path": "src/game/event/EventMap.ts",
    "content": "export const EventMap = {};\n"
  },
  {
    "path": "src/game/event/EventType.ts",
    "content": "export enum EventType {\n    Cheer = 0,\n    UnitDeployUndeploy = 1,\n    WeaponFire = 2,\n    ObjectDestroy = 3,\n    ObjectSpawn = 4,\n    ObjectUnspawn = 5,\n    ObjectMorph = 6,\n    ObjectLiftOff = 7,\n    ObjectLand = 8,\n    ObjectCrashing = 9,\n    ObjectDisguiseChange = 10,\n    ObjectCloakChange = 11,\n    ObjectAttacked = 12,\n    ShipSubmergeChange = 13,\n    BridgeRepair = 14,\n    BuildStatusChange = 15,\n    BuildingPlace = 16,\n    BuildingFailedPlace = 17,\n    ObjectSell = 18,\n    BuildingRepairFull = 19,\n    BuildingCapture = 20,\n    BuildingInfiltration = 21,\n    BuildingGarrison = 22,\n    BuildingEvacuate = 23,\n    BuildingRepairStart = 24,\n    UnitRepairStart = 25,\n    UnitRepairFinish = 26,\n    UnitRecycle = 27,\n    InflictDamage = 28,\n    HealthChange = 29,\n    WarheadDetonate = 30,\n    PlayerDefeated = 31,\n    PlayerResigned = 32,\n    PlayerDropped = 33,\n    DeployNotAllowed = 34,\n    PowerChange = 35,\n    PowerLow = 36,\n    PowerRestore = 37,\n    RadarOnOff = 38,\n    ObjectOwnerChange = 39,\n    RadarEvent = 40,\n    InsufficientFunds = 41,\n    RallyPointChange = 42,\n    PrimaryFactoryChange = 43,\n    FactoryProduceUnit = 44,\n    ObjectTeleport = 45,\n    AllianceChange = 46,\n    UnitPromote = 47,\n    EnterTransport = 48,\n    LeaveTransport = 49,\n    EnterObject = 50,\n    EnterTile = 51,\n    SuperWeaponReady = 52,\n    SuperWeaponActivate = 53,\n    LightningStormManifest = 54,\n    LightningStormCloud = 55,\n    CratePickup = 56,\n    PingLocation = 57,\n    StalemateDetect = 58,\n    TriggerSoundFx = 59,\n    TriggerStopSoundFx = 60,\n    TriggerEva = 61,\n    TriggerAnim = 62,\n    TriggerText = 63,\n    TimerExpire = 64\n}\n"
  },
  {
    "path": "src/game/event/FactoryProduceUnitEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class FactoryProduceUnitEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.FactoryProduceUnit;\n    }\n}\n"
  },
  {
    "path": "src/game/event/GameEvent.ts",
    "content": "export {};\n"
  },
  {
    "path": "src/game/event/HealthChangeEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class HealthChangeEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly currentHealth: any, public readonly prevHealth: any) {\n        this.type = EventType.HealthChange;\n    }\n}\n"
  },
  {
    "path": "src/game/event/InflictDamageEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class InflictDamageEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly attacker: any, public readonly damageHitPoints: any, public readonly currentHealth: any, public readonly prevHealth: any) {\n        this.type = EventType.InflictDamage;\n    }\n}\n"
  },
  {
    "path": "src/game/event/InsufficientFundsEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class InsufficientFundsEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.InsufficientFunds;\n    }\n}\n"
  },
  {
    "path": "src/game/event/LeaveTransportEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class LeaveTransportEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.LeaveTransport;\n    }\n}\n"
  },
  {
    "path": "src/game/event/LightningStormCloudEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class LightningStormCloudEvent {\n    public readonly type: EventType;\n    constructor(public readonly position: any) {\n        this.type = EventType.LightningStormCloud;\n    }\n}\n"
  },
  {
    "path": "src/game/event/LightningStormManifestEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class LightningStormManifestEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.LightningStormManifest;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectAttackedEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectAttackedEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly attacker: any, public readonly incidental: any) {\n        this.type = EventType.ObjectAttacked;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectCloakChangeEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectCloakChangeEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.ObjectCloakChange;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectCrashingEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectCrashingEvent {\n    public readonly type: EventType;\n    constructor(public readonly gameObject: any) {\n        this.type = EventType.ObjectCrashing;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectDestroyEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectDestroyEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly attackerInfo: any, public readonly incidental: any) {\n        this.type = EventType.ObjectDestroy;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectDisguiseChangeEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectDisguiseChangeEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.ObjectDisguiseChange;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectLandEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectLandEvent {\n    public readonly type: EventType;\n    constructor(public readonly gameObject: any) {\n        this.type = EventType.ObjectLand;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectLiftOffEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectLiftOffEvent {\n    public readonly type: EventType;\n    constructor(public readonly gameObject: any) {\n        this.type = EventType.ObjectLiftOff;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectMorphEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectMorphEvent {\n    public readonly type: EventType;\n    constructor(public readonly from: any, public readonly to: any) {\n        this.type = EventType.ObjectMorph;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectOwnerChangeEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectOwnerChangeEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly prevOwner: any) {\n        this.type = EventType.ObjectOwnerChange;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectSellEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectSellEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.ObjectSell;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectSpawnEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectSpawnEvent {\n    public readonly type: EventType;\n    constructor(public readonly gameObject: any) {\n        this.type = EventType.ObjectSpawn;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectTeleportEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectTeleportEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly isChronoshift: any, public readonly prevTile: any) {\n        this.type = EventType.ObjectTeleport;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ObjectUnspawnEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ObjectUnspawnEvent {\n    public readonly type: EventType;\n    constructor(public readonly gameObject: any) {\n        this.type = EventType.ObjectUnspawn;\n    }\n}\n"
  },
  {
    "path": "src/game/event/PingLocationEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class PingLocationEvent {\n    public readonly type: EventType;\n    constructor(public readonly tile: any, public readonly player: any) {\n        this.type = EventType.PingLocation;\n    }\n}\n"
  },
  {
    "path": "src/game/event/PlayerDefeatedEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class PlayerDefeatedEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.PlayerDefeated;\n    }\n}\n"
  },
  {
    "path": "src/game/event/PlayerDroppedEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class PlayerDroppedEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly assetsRedistributed: any) {\n        this.type = EventType.PlayerDropped;\n    }\n}\n"
  },
  {
    "path": "src/game/event/PlayerResignedEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class PlayerResignedEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly assetsRedistributed?: any) {\n        this.type = EventType.PlayerResigned;\n    }\n}\n"
  },
  {
    "path": "src/game/event/PowerChangeEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class PowerChangeEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly power: any, public readonly drain: any) {\n        this.type = EventType.PowerChange;\n    }\n}\n"
  },
  {
    "path": "src/game/event/PowerLowEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class PowerLowEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.PowerLow;\n    }\n}\n"
  },
  {
    "path": "src/game/event/PowerRestoreEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class PowerRestoreEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.PowerRestore;\n    }\n}\n"
  },
  {
    "path": "src/game/event/PrimaryFactoryChangeEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class PrimaryFactoryChangeEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.PrimaryFactoryChange;\n    }\n}\n"
  },
  {
    "path": "src/game/event/RadarEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class RadarEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly radarEventType: any, public readonly tile: any) {\n        this.type = EventType.RadarEvent;\n    }\n}\n"
  },
  {
    "path": "src/game/event/RadarOnOffEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class RadarOnOffEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly radarEnabled: boolean) {\n        this.type = EventType.RadarOnOff;\n    }\n}\n"
  },
  {
    "path": "src/game/event/RallyPointChangeEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class RallyPointChangeEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.RallyPointChange;\n    }\n}\n"
  },
  {
    "path": "src/game/event/ShipSubmergeChangeEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class ShipSubmergeChangeEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.ShipSubmergeChange;\n    }\n}\n"
  },
  {
    "path": "src/game/event/StalemateDetectEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class StalemateDetectEvent {\n    public readonly type: EventType;\n    constructor() {\n        this.type = EventType.StalemateDetect;\n    }\n}\n"
  },
  {
    "path": "src/game/event/SuperWeaponActivateEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class SuperWeaponActivateEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly owner: any, public readonly atTile: any, public readonly atTile2: any, public readonly noSfxWarning: boolean) {\n        this.type = EventType.SuperWeaponActivate;\n    }\n}\n"
  },
  {
    "path": "src/game/event/SuperWeaponReadyEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class SuperWeaponReadyEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.SuperWeaponReady;\n    }\n}\n"
  },
  {
    "path": "src/game/event/TimerExpireEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class TimerExpireEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.TimerExpire;\n    }\n}\n"
  },
  {
    "path": "src/game/event/TriggerAnimEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class TriggerAnimEvent {\n    public readonly type: EventType;\n    constructor(public readonly name: string, public readonly tile: any) {\n        this.type = EventType.TriggerAnim;\n    }\n}\n"
  },
  {
    "path": "src/game/event/TriggerEvaEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class TriggerEvaEvent {\n    public readonly type: EventType;\n    constructor(public readonly soundId: string) {\n        this.type = EventType.TriggerEva;\n    }\n}\n"
  },
  {
    "path": "src/game/event/TriggerSoundFxEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class TriggerSoundFxEvent {\n    public readonly type: EventType;\n    constructor(public readonly soundId: string, public readonly tile?: any) {\n        this.type = EventType.TriggerSoundFx;\n    }\n}\n"
  },
  {
    "path": "src/game/event/TriggerStopSoundFxEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class TriggerStopSoundFxEvent {\n    public readonly type: EventType;\n    constructor(public readonly tile: any) {\n        this.type = EventType.TriggerStopSoundFx;\n    }\n}\n"
  },
  {
    "path": "src/game/event/TriggerTextEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class TriggerTextEvent {\n    public readonly type: EventType;\n    constructor(public readonly label: any) {\n        this.type = EventType.TriggerText;\n    }\n}\n"
  },
  {
    "path": "src/game/event/UnitDeployUndeployEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class UnitDeployUndeployEvent {\n    public readonly type: EventType;\n    constructor(public readonly unit: any, public readonly deployType: any) {\n        this.type = EventType.UnitDeployUndeploy;\n    }\n}\n"
  },
  {
    "path": "src/game/event/UnitPromoteEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class UnitPromoteEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.UnitPromote;\n    }\n}\n"
  },
  {
    "path": "src/game/event/UnitRecycleEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class UnitRecycleEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.UnitRecycle;\n    }\n}\n"
  },
  {
    "path": "src/game/event/UnitRepairFinishEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class UnitRepairFinishEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly from: any) {\n        this.type = EventType.UnitRepairFinish;\n    }\n}\n"
  },
  {
    "path": "src/game/event/UnitRepairStartEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class UnitRepairStartEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any) {\n        this.type = EventType.UnitRepairStart;\n    }\n}\n"
  },
  {
    "path": "src/game/event/WarheadDetonateEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class WarheadDetonateEvent {\n    public readonly type: EventType;\n    constructor(public readonly target: any, public readonly position: any, public readonly explodeAnim: any, public readonly isLightningStrike: boolean) {\n        this.type = EventType.WarheadDetonate;\n    }\n}\n"
  },
  {
    "path": "src/game/event/WeaponFireEvent.ts",
    "content": "import { EventType } from \"./EventType\";\nexport class WeaponFireEvent {\n    public readonly type: EventType;\n    constructor(public readonly weapon: any, public readonly gameObject: any) {\n        this.type = EventType.WeaponFire;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Aircraft.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { MoveTrait } from '@/game/gameobject/trait/MoveTrait';\nimport { ZoneType } from '@/game/gameobject/unit/ZoneType';\nimport { DockableTrait } from '@/game/gameobject/trait/DockableTrait';\nimport { Techno } from '@/game/gameobject/Techno';\nimport { ParasiteableTrait } from '@/game/gameobject/trait/ParasiteableTrait';\nimport { CrashableTrait } from '@/game/gameobject/trait/CrashableTrait';\nimport { AirportBoundTrait } from '@/game/gameobject/trait/AirportBoundTrait';\nimport { SpawnLinkTrait } from '@/game/gameobject/trait/SpawnLinkTrait';\nimport { MissileSpawnTrait } from '@/game/gameobject/trait/MissileSpawnTrait';\nimport { CrateBonuses } from '@/game/gameobject/unit/CrateBonuses';\nimport { UnlandableTrait } from '@/game/gameobject/trait/UnlandableTrait';\nexport class Aircraft extends Techno {\n    pitch: number;\n    yaw: number;\n    roll: number;\n    onBridge: boolean;\n    zone: ZoneType;\n    crateBonuses: CrateBonuses;\n    moveTrait: MoveTrait;\n    airportBoundTrait?: AirportBoundTrait;\n    crashableTrait?: CrashableTrait;\n    missileSpawnTrait?: MissileSpawnTrait;\n    spawnLinkTrait?: SpawnLinkTrait;\n    parasiteableTrait?: ParasiteableTrait;\n    get direction() {\n        return this.yaw;\n    }\n    set direction(value: number) {\n        this.yaw = value;\n    }\n    get isMoving() {\n        return this.moveTrait.isMoving();\n    }\n    static factory(id: string, rules: any, owner: any, gameRules: any, map: any) {\n        const aircraft = new this(id, rules, owner);\n        if (aircraft.rules.airportBound && aircraft.rules.dock.length) {\n            aircraft.airportBoundTrait = new AirportBoundTrait(aircraft.rules.dock);\n            aircraft.traits.add(aircraft.airportBoundTrait);\n        }\n        if (!aircraft.rules.missileSpawn) {\n            aircraft.crashableTrait = new CrashableTrait(aircraft);\n            aircraft.traits.add(aircraft.crashableTrait);\n        }\n        if (aircraft.rules.spawned) {\n            if (aircraft.rules.missileSpawn) {\n                aircraft.missileSpawnTrait = new MissileSpawnTrait();\n                aircraft.traits.add(aircraft.missileSpawnTrait);\n            }\n            else {\n                aircraft.spawnLinkTrait = new SpawnLinkTrait();\n                aircraft.traits.add(aircraft.spawnLinkTrait);\n            }\n        }\n        aircraft.moveTrait = new MoveTrait(aircraft as any, map);\n        aircraft.traits.add(aircraft.moveTrait);\n        if (rules.dock.length) {\n            aircraft.traits.add(new DockableTrait());\n        }\n        if (!(rules.landable && id !== gameRules.general.paradrop.paradropPlane)) {\n            aircraft.traits.add(new UnlandableTrait());\n        }\n        if (rules.parasiteable) {\n            aircraft.parasiteableTrait = new ParasiteableTrait(aircraft);\n            aircraft.traits.add(aircraft.parasiteableTrait);\n        }\n        return aircraft;\n    }\n    constructor(id: string, rules: any, owner: any) {\n        super(ObjectType.Aircraft as any, id, rules, owner);\n        this.pitch = 0;\n        this.yaw = 0;\n        this.roll = 0;\n        this.onBridge = false;\n        this.zone = ZoneType.Ground;\n        this.crateBonuses = new CrateBonuses();\n    }\n    isUnit(): boolean {\n        return true;\n    }\n    isAircraft(): boolean {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Bridge.ts",
    "content": "export interface Bridge {\n    [key: string]: any;\n}\n"
  },
  {
    "path": "src/game/gameobject/Building.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { GarrisonTrait } from \"@/game/gameobject/trait/GarrisonTrait\";\nimport { TurretTrait } from \"@/game/gameobject/trait/TurretTrait\";\nimport { TechnoRules, FactoryType } from \"@/game/rules/TechnoRules\";\nimport { PoweredTrait } from \"@/game/gameobject/trait/PoweredTrait\";\nimport { FactoryTrait } from \"@/game/gameobject/trait/FactoryTrait\";\nimport { DockTrait } from \"@/game/gameobject/trait/DockTrait\";\nimport { FreeUnitTrait } from \"@/game/gameobject/trait/FreeUnitTrait\";\nimport { Techno } from \"@/game/gameobject/Techno\";\nimport { CrewedTrait } from \"@/game/gameobject/trait/CrewedTrait\";\nimport { CabHutTrait } from \"@/game/gameobject/trait/CabHutTrait\";\nimport { OilDerrickTrait } from \"@/game/gameobject/trait/OilDerrickTrait\";\nimport { WallTrait } from \"@/game/gameobject/trait/WallTrait\";\nimport { Coords } from \"@/game/Coords\";\nimport { OverpoweredTrait } from \"@/game/gameobject/trait/OverpoweredTrait\";\nimport { UnitRepairTrait } from \"@/game/gameobject/trait/UnitRepairTrait\";\nimport { RallyTrait } from \"@/game/gameobject/trait/RallyTrait\";\nimport { C4ChargeTrait } from \"@/game/gameobject/trait/C4ChargeTrait\";\nimport { HelipadTrait } from \"@/game/gameobject/trait/HelipadTrait\";\nimport { UnitReloadTrait } from \"@/game/gameobject/trait/UnitReloadTrait\";\nimport { WaitForBuildUpTask } from \"@/game/gameobject/task/WaitForBuildUpTask\";\nimport { SuperWeaponTrait } from \"@/game/gameobject/trait/SuperWeaponTrait\";\nimport { GapGeneratorTrait } from \"@/game/gameobject/trait/GapGeneratorTrait\";\nimport { PsychicDetectorTrait } from \"@/game/gameobject/trait/PsychicDetectorTrait\";\nimport { HospitalTrait } from \"@/game/gameobject/trait/HospitalTrait\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { DelayedKillTrait } from \"@/game/gameobject/trait/DelayedKillTrait\";\nimport { BuildStatusChangeEvent } from \"@/game/event/BuildStatusChangeEvent\";\nimport { NotifyBuildStatus } from \"@/game/gameobject/trait/interface/NotifyBuildStatus\";\nexport enum BuildStatus {\n    BuildUp = 0,\n    Ready = 1,\n    BuildDown = 2\n}\nexport class Building extends Techno {\n    public showWeaponRange: boolean = false;\n    public direction: number = 0;\n    private _buildStatus: BuildStatus;\n    public lastBuildStatus: BuildStatus;\n    public garrisonTrait?: GarrisonTrait;\n    public c4ChargeTrait?: C4ChargeTrait;\n    public delayedKillTrait?: DelayedKillTrait;\n    public cabHutTrait?: CabHutTrait;\n    public crewedTrait?: CrewedTrait;\n    public turretTrait?: TurretTrait;\n    public overpoweredTrait?: OverpoweredTrait;\n    public poweredTrait?: PoweredTrait;\n    public factoryTrait?: FactoryTrait;\n    public superWeaponTrait?: SuperWeaponTrait;\n    public dockTrait?: DockTrait;\n    public helipadTrait?: HelipadTrait;\n    public unitRepairTrait?: UnitRepairTrait;\n    public unitReloadTrait?: UnitReloadTrait;\n    public hospitalTrait?: HospitalTrait;\n    public rallyTrait?: RallyTrait;\n    public wallTrait?: WallTrait;\n    public gapGeneratorTrait?: GapGeneratorTrait;\n    public psychicDetectorTrait?: PsychicDetectorTrait;\n    static factory(owner: any, rules: TechnoRules, gameRules: any, art: any, world: any, coords: any): Building {\n        const building = new this(owner, rules, art);\n        if (rules.canBeOccupied) {\n            building.garrisonTrait = new GarrisonTrait(building, gameRules.audioVisual.conditionRed, rules.maxNumberOccupants);\n            building.traits.add(building.garrisonTrait);\n        }\n        if (rules.canC4 && !rules.wall) {\n            building.c4ChargeTrait = new C4ChargeTrait();\n            building.traits.add(building.c4ChargeTrait);\n        }\n        if (rules.eligibleForDelayKill) {\n            building.delayedKillTrait = new DelayedKillTrait();\n            building.traits.add(building.delayedKillTrait);\n        }\n        if (rules.bridgeRepairHut) {\n            building.cabHutTrait = new CabHutTrait(building, coords);\n            building.traits.add(building.cabHutTrait);\n        }\n        if (rules.crewed) {\n            building.crewedTrait = new CrewedTrait();\n            building.traits.add(building.crewedTrait);\n        }\n        if (rules.turret) {\n            building.turretTrait = new TurretTrait();\n            building.traits.add(building.turretTrait);\n        }\n        if (rules.overpowerable) {\n            building.overpoweredTrait = new OverpoweredTrait(building);\n            building.traits.add(building.overpoweredTrait);\n        }\n        if ((rules.powered && rules.power !== 0) || rules.needsEngineer) {\n            building.poweredTrait = new PoweredTrait(building);\n            building.traits.add(building.poweredTrait);\n        }\n        if (rules.factory || rules.cloning) {\n            building.factoryTrait = new FactoryTrait(rules.cloning ? FactoryType.InfantryType : rules.factory, rules.cloning);\n            building.traits.add(building.factoryTrait);\n        }\n        if (rules.superWeapon) {\n            building.superWeaponTrait = new SuperWeaponTrait(rules.superWeapon);\n            building.traits.add(building.superWeaponTrait);\n        }\n        if (rules.numberOfDocks) {\n            building.dockTrait = new DockTrait(building as any, world, rules.numberOfDocks, art.dockingOffsets);\n            building.traits.add(building.dockTrait);\n            if (rules.helipad) {\n                building.helipadTrait = new HelipadTrait();\n                building.traits.add(building.helipadTrait);\n            }\n            if (rules.unitRepair || rules.unitReload) {\n                building.unitRepairTrait = new UnitRepairTrait();\n                building.traits.add(building.unitRepairTrait);\n            }\n            if (rules.unitReload) {\n                building.unitReloadTrait = new UnitReloadTrait();\n                building.traits.add(building.unitReloadTrait);\n            }\n        }\n        if (rules.hospital) {\n            building.hospitalTrait = new HospitalTrait();\n            building.traits.add(building.hospitalTrait);\n        }\n        if (rules.factory || rules.cloning || rules.numberOfDocks) {\n            building.rallyTrait = new RallyTrait();\n            building.traits.add(building.rallyTrait);\n        }\n        if (rules.freeUnit) {\n            building.traits.add(new FreeUnitTrait());\n        }\n        if (rules.produceCashStartup) {\n            building.traits.add(new OilDerrickTrait());\n        }\n        if (rules.wall) {\n            building.wallTrait = new WallTrait();\n            building.traits.add(building.wallTrait);\n        }\n        if (rules.gapGenerator) {\n            building.gapGeneratorTrait = new GapGeneratorTrait(rules.gapRadiusInCells);\n            building.traits.add(building.gapGeneratorTrait);\n        }\n        if (rules.psychicDetectionRadius) {\n            building.psychicDetectorTrait = new PsychicDetectorTrait(rules.psychicDetectionRadius);\n            building.traits.add(building.psychicDetectorTrait);\n        }\n        return building;\n    }\n    constructor(owner: any, rules: TechnoRules, art: any) {\n        super(ObjectType.Building as any, owner, rules, art);\n        this._buildStatus = BuildStatus.BuildUp;\n        this.lastBuildStatus = this.buildStatus;\n    }\n    isBuilding(): boolean {\n        return true;\n    }\n    get buildStatus(): BuildStatus {\n        return this._buildStatus;\n    }\n    getFoundation(): any {\n        return this.art.foundation;\n    }\n    getFoundationCenterOffset(): Vector2 {\n        const foundation = this.getFoundation();\n        return new Vector2((foundation.width / 2) * Coords.LEPTONS_PER_TILE, (foundation.height / 2) * Coords.LEPTONS_PER_TILE);\n    }\n    update(context: any): void {\n        if (this.buildStatus === BuildStatus.BuildUp &&\n            !this.unitOrderTrait.hasTasks()) {\n            this.unitOrderTrait.addTask(new WaitForBuildUpTask(context.rules.general.buildupTime, context));\n        }\n        this.attackTrait?.setDisabled(this.buildStatus !== BuildStatus.Ready ||\n            (!!this.poweredTrait && !this.poweredTrait.isPoweredOn()));\n        super.update(context);\n    }\n    setBuildStatus(status: BuildStatus, context: any): void {\n        this._buildStatus = status;\n        const oldStatus = this.lastBuildStatus;\n        if (this.buildStatus !== oldStatus) {\n            this.lastBuildStatus = this.buildStatus;\n            this.traits.filter(NotifyBuildStatus).forEach((trait: any) => {\n                trait[NotifyBuildStatus.onStatusChange](oldStatus, this, context);\n            });\n            context.events.dispatch(new BuildStatusChangeEvent(this, this.buildStatus));\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Debris.ts",
    "content": "import { GameObject } from './GameObject';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { ZoneType } from './unit/ZoneType';\nimport { Warhead } from '../Warhead';\nimport { FacingUtil } from './unit/FacingUtil';\nimport { AnimTerrainEffect } from './common/AnimTerrainEffect';\nimport { CollisionHelper } from './unit/CollisionHelper';\nimport { CollisionType } from './unit/CollisionType';\nimport { Vector3 } from '../math/Vector3';\nimport { lerp } from '@/util/math';\nexport class Debris extends GameObject {\n    private age: number = 0;\n    private direction: number = 0;\n    private rotationAxis: Vector3 = new Vector3();\n    private angularVelocity: number = 0;\n    private zone: ZoneType = ZoneType.Air;\n    private velocity: Vector3 = new Vector3();\n    private collisionHelper: CollisionHelper;\n    private xySpeed: number = 0;\n    private zSpeed: number = 0;\n    private explodeAnim?: string;\n    static factory(rules: any, position: any, tile: any, collisionRules: any): Debris {\n        return new this(rules, position, tile, collisionRules);\n    }\n    constructor(rules: any, position: any, tile: any, collisionRules: any) {\n        super(ObjectType.Debris, rules, position, tile);\n        this.collisionHelper = new CollisionHelper(collisionRules);\n    }\n    onSpawn(gameEngine: any): void {\n        super.onSpawn(gameEngine);\n        this.direction = gameEngine.generateRandomInt(0, 359);\n        this.xySpeed = lerp(0, this.rules.maxXYVel, gameEngine.generateRandom());\n        this.zSpeed = lerp(this.rules.minZVel, this.rules.maxZVel || 1.5 * this.rules.minZVel, gameEngine.generateRandom());\n        this.rotationAxis\n            .set(gameEngine.generateRandom(), gameEngine.generateRandom(), gameEngine.generateRandom())\n            .normalize();\n        this.angularVelocity = lerp(this.rules.minAngularVelocity, this.rules.maxAngularVelocity, gameEngine.generateRandom());\n    }\n    update(gameContext: any): void {\n        super.update(gameContext);\n        this.age++;\n        if (this.rules.duration && this.age > this.rules.duration) {\n            this.velocity.set(0, 0, 0);\n            this.detonate(gameContext);\n            return;\n        }\n        this.zSpeed--;\n        const xyMovement = FacingUtil.toMapCoords(this.direction).setLength(this.xySpeed);\n        const movementVector = new Vector3(xyMovement.x, this.zSpeed, xyMovement.y);\n        const previousPosition = this.position.clone();\n        const nextWorldPosition = movementVector.clone().add(this.position.worldPosition);\n        if (!gameContext.map.isWithinHardBounds(nextWorldPosition)) {\n            gameContext.unspawnObject(this);\n            return;\n        }\n        this.position.moveByLeptons3(movementVector);\n        let shouldDetonate = false;\n        const { type: collisionType, target: collisionTarget } = this.collisionHelper.checkCollisions(this.position, previousPosition, {\n            cliffs: true,\n            ground: true,\n            shore: false,\n            walls: true,\n            units: () => false,\n        });\n        if (collisionType) {\n            const isGroundCollision = [\n                CollisionType.Ground,\n                CollisionType.OnBridge\n            ].includes(collisionType);\n            const canBounce = isGroundCollision &&\n                this.rules.elasticity > 0 &&\n                gameContext.map.getTileZone(this.tile) !== ZoneType.Water;\n            if (!canBounce || Math.abs(this.zSpeed) < 1) {\n                shouldDetonate = true;\n            }\n            else {\n                this.zSpeed = -this.zSpeed * this.rules.elasticity;\n                this.velocity.y = -this.velocity.y * this.rules.elasticity;\n                this.rotationAxis.negate();\n            }\n        }\n        if (shouldDetonate) {\n            this.velocity.set(0, 0, 0);\n            if (collisionTarget && collisionType === CollisionType.Wall) {\n                const targetWorldPosition = collisionTarget.position.worldPosition;\n                this.position.moveByLeptons3(targetWorldPosition.clone().sub(this.position.worldPosition));\n            }\n            this.detonate(gameContext, collisionType);\n        }\n        else {\n            this.velocity.copy(movementVector);\n        }\n    }\n    private detonate(gameContext: any, collisionType: CollisionType = CollisionType.None): void {\n        const warhead = this.rules.warhead ?\n            gameContext.rules.getWarhead(this.rules.warhead) : undefined;\n        this.zone = this.collisionHelper.computeDetonationZone(this.tile, this.tileElevation, collisionType);\n        let animationName: string | undefined;\n        if (this.zone === ZoneType.Water) {\n            const splashList = gameContext.rules.combatDamage.splashList;\n            animationName = splashList[0];\n        }\n        else if (this.rules.expireAnim && gameContext.rules.animationNames.has(this.rules.expireAnim)) {\n            animationName = this.rules.expireAnim;\n        }\n        this.explodeAnim = animationName;\n        const terrainEffect = new AnimTerrainEffect();\n        if (animationName) {\n            terrainEffect.spawnSmudges(animationName, this.tile, gameContext);\n        }\n        gameContext.destroyObject(this);\n        if (warhead) {\n            const warheadInstance = new Warhead(warhead);\n            warheadInstance.detonate(gameContext, this.rules.damage, this.tile, this.tileElevation, this.position.worldPosition, this.zone, collisionType, gameContext.createTarget(undefined, this.tile), undefined, false, undefined, this.rules.damageRadius || undefined, true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/GameObject.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { Traits } from '@/game/Traits';\nimport { NotifyTick } from '@/game/gameobject/trait/interface/NotifyTick';\nimport { NotifyDestroy } from '@/game/gameobject/trait/interface/NotifyDestroy';\nimport { fnv32a } from '@/util/math';\nimport { NotifyOwnerChange } from '@/game/gameobject/trait/interface/NotifyOwnerChange';\nimport { NotifySpawn } from '@/game/gameobject/trait/interface/NotifySpawn';\nimport { NotifyUnspawn } from '@/game/gameobject/trait/interface/NotifyUnspawn';\nimport { NotifyAttack } from '@/game/gameobject/trait/interface/NotifyAttack';\nimport { DeathType } from '@/game/gameobject/common/DeathType';\nexport class GameObject {\n    public traits: Traits;\n    public cachedTraits: {\n        tick: any[];\n    };\n    public isCrashing: boolean;\n    public isDestroyed: boolean;\n    public deathType: DeathType;\n    public isDisposed: boolean;\n    public isSpawned: boolean;\n    public type: ObjectType;\n    public name: string;\n    public rules: any;\n    public art: any;\n    public id: number;\n    public position: any;\n    [key: string]: any;\n    get tile() {\n        return this.position.tile;\n    }\n    get tileElevation() {\n        return this.position.tileElevation;\n    }\n    constructor(type: ObjectType, name: string, rules: any, art: any) {\n        this.traits = new Traits();\n        this.cachedTraits = { tick: [] };\n        this.isCrashing = false;\n        this.isDestroyed = false;\n        this.deathType = DeathType.Normal;\n        this.isDisposed = false;\n        this.isSpawned = false;\n        this.type = type;\n        this.name = name;\n        this.rules = rules;\n        this.art = art;\n    }\n    getFoundation() {\n        return { width: 1, height: 1 };\n    }\n    isSmudge() {\n        return this.type === ObjectType.Smudge;\n    }\n    isOverlay() {\n        return this.type === ObjectType.Overlay;\n    }\n    isTerrain() {\n        return this.type === ObjectType.Terrain;\n    }\n    isProjectile() {\n        return this.type === ObjectType.Projectile;\n    }\n    isDebris() {\n        return this.type === ObjectType.Debris;\n    }\n    isBuilding() {\n        return false;\n    }\n    isInfantry() {\n        return false;\n    }\n    isVehicle() {\n        return false;\n    }\n    isAircraft() {\n        return false;\n    }\n    isUnit() {\n        return false;\n    }\n    isTechno() {\n        return false;\n    }\n    update(deltaTime: number) {\n        for (const trait of this.cachedTraits.tick) {\n            trait[NotifyTick.onTick](this, deltaTime);\n        }\n    }\n    onSpawn(data: any) {\n        this.isSpawned = true;\n        this.traits.filter(NotifySpawn).forEach((trait) => {\n            trait[NotifySpawn.onSpawn](this, data);\n        });\n    }\n    onUnspawn(data: any) {\n        this.isSpawned = false;\n        this.traits.filter(NotifyUnspawn).forEach((trait) => {\n            trait[NotifyUnspawn.onUnspawn](this, data);\n        });\n    }\n    onDestroy(data: any, type: any, reason: any) {\n        this.traits.filter(NotifyDestroy).forEach((trait) => {\n            trait[NotifyDestroy.onDestroy](this, data, type, reason);\n        });\n    }\n    onOwnerChange(data: any, owner: any) {\n        this.traits.filter(NotifyOwnerChange).forEach((trait) => {\n            trait[NotifyOwnerChange.onChange](this, data, owner);\n        });\n    }\n    onAttack(data: any, target: any) {\n        this.traits.filter(NotifyAttack).forEach((trait) => {\n            trait[NotifyAttack.onAttack](this, target, data);\n        });\n    }\n    addTrait(trait: any) {\n        this.traits.add(trait);\n        if (trait[NotifyTick.onTick]) {\n            this.cachedTraits.tick.push(trait);\n        }\n    }\n    getUiName() {\n        return this.rules.uiName;\n    }\n    getHash() {\n        const pos = this.position.worldPosition;\n        return fnv32a([\n            this.id,\n            ...new Uint8Array(new Float64Array([pos.x, pos.y, pos.z]).buffer),\n            ...this.traits.getAll().map((trait) => trait.getHash?.() ?? 0),\n        ]);\n    }\n    debugGetState() {\n        return {\n            id: this.id,\n            position: this.position.worldPosition.toArray(),\n            traits: this.traits.getAll().reduce((acc, trait) => {\n                const state = trait.debugGetState?.();\n                if (state !== undefined) {\n                    acc[trait.constructor.name] = state;\n                }\n                return acc;\n            }, {}),\n        };\n    }\n    dispose() {\n        this.isDisposed = true;\n        this.traits.dispose();\n        this.cachedTraits.tick.length = 0;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Infantry.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { ZoneType } from '@/game/gameobject/unit/ZoneType';\nimport { StanceType } from '@/game/gameobject/infantry/StanceType';\nimport { InfDeathType } from '@/game/gameobject/infantry/InfDeathType';\nimport { MoveTrait } from '@/game/gameobject/trait/MoveTrait';\nimport { SuppressionTrait } from '@/game/gameobject/trait/SuppressionTrait';\nimport { Techno } from '@/game/gameobject/Techno';\nimport { IdleActionTrait } from '@/game/gameobject/trait/IdleActionTrait';\nimport { CrashableTrait } from '@/game/gameobject/trait/CrashableTrait';\nimport { AgentTrait } from '@/game/gameobject/trait/AgentTrait';\nimport { CrateBonuses } from '@/game/gameobject/unit/CrateBonuses';\nexport class Infantry extends Techno {\n    static SUB_CELLS = [2, 4, 3];\n    direction: number;\n    onBridge: boolean;\n    zone: ZoneType;\n    private _stance: StanceType;\n    isFiring: boolean;\n    isPanicked: boolean;\n    infDeathType: InfDeathType;\n    crateBonuses: CrateBonuses;\n    moveTrait: MoveTrait;\n    crashableTrait?: CrashableTrait;\n    suppressionTrait?: SuppressionTrait;\n    agentTrait?: AgentTrait;\n    idleActionTrait: IdleActionTrait;\n    get isMoving(): boolean {\n        return this.moveTrait.isMoving();\n    }\n    static factory(id: string, rules: any, owner: any, general: any): Infantry {\n        const infantry = new this(id, rules, owner);\n        infantry.moveTrait = new MoveTrait(infantry as any, general);\n        infantry.traits.add(infantry.moveTrait);\n        if (infantry.rules.crashable) {\n            infantry.crashableTrait = new CrashableTrait(infantry);\n            infantry.traits.add(infantry.crashableTrait);\n        }\n        if (!infantry.rules.fearless) {\n            infantry.suppressionTrait = new SuppressionTrait();\n            infantry.traits.add(infantry.suppressionTrait);\n        }\n        if (infantry.rules.agent) {\n            infantry.agentTrait = new AgentTrait();\n            infantry.traits.add(infantry.agentTrait);\n        }\n        infantry.idleActionTrait = new IdleActionTrait();\n        infantry.traits.add(infantry.idleActionTrait);\n        return infantry;\n    }\n    constructor(id: string, rules: any, owner: any) {\n        super(ObjectType.Infantry as any, id, rules, owner);\n        this.direction = 0;\n        this.onBridge = false;\n        this.zone = ZoneType.Ground;\n        this._stance = StanceType.None;\n        this.isFiring = false;\n        this.isPanicked = false;\n        this.infDeathType = InfDeathType.Gunfire;\n        this.crateBonuses = new CrateBonuses();\n    }\n    get stance(): StanceType {\n        return this._stance === StanceType.None &&\n            this.suppressionTrait?.isSuppressed()\n            ? StanceType.Prone\n            : this._stance;\n    }\n    set stance(value: StanceType) {\n        this._stance = value;\n        this.moveTrait.setDisabled([StanceType.Deployed, StanceType.Cheer].includes(value));\n        this.attackTrait?.setDisabled([StanceType.Paradrop, StanceType.Cheer].includes(value));\n    }\n    isUnit(): boolean {\n        return true;\n    }\n    isInfantry(): boolean {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/ObjectFactory.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { Building } from \"@/game/gameobject/Building\";\nimport { Terrain } from \"@/game/gameobject/Terrain\";\nimport { Overlay } from \"@/game/gameobject/Overlay\";\nimport { Smudge } from \"@/game/gameobject/Smudge\";\nimport { Infantry } from \"@/game/gameobject/Infantry\";\nimport { Vehicle } from \"@/game/gameobject/Vehicle\";\nimport { Aircraft } from \"@/game/gameobject/Aircraft\";\nimport { ObjectArt } from \"@/game/art/ObjectArt\";\nimport { IniSection } from \"@/data/IniSection\";\nimport { UnitOrderTrait } from \"@/game/gameobject/trait/UnitOrderTrait\";\nimport { ObjectPosition } from \"@/game/gameobject/ObjectPosition\";\nimport { AttackTrait } from \"@/game/gameobject/trait/AttackTrait\";\nimport { Projectile } from \"@/game/gameobject/Projectile\";\nimport { DeployerTrait } from \"@/game/gameobject/trait/DeployerTrait\";\nimport { HealthTrait } from \"@/game/gameobject/trait/HealthTrait\";\nimport { BridgeTrait } from \"@/game/gameobject/trait/BridgeTrait\";\nimport { BridgeOverlayTypes, OverlayBridgeType } from \"@/game/map/BridgeOverlayTypes\";\nimport { OreOverlayTypes } from \"@/game/map/OreOverlayTypes\";\nimport { OverlayTibType } from \"@/engine/type/OverlayTibType\";\nimport { TiberiumTrait } from \"@/game/gameobject/trait/TiberiumTrait\";\nimport { TiberiumTreeTrait } from \"@/game/gameobject/trait/TiberiumTreeTrait\";\nimport { AutoRepairTrait } from \"@/game/gameobject/trait/AutoRepairTrait\";\nimport { VeteranTrait } from \"@/game/gameobject/trait/VeteranTrait\";\nimport { ArmedTrait } from \"@/game/gameobject/trait/ArmedTrait\";\nimport { SelfHealingTrait } from \"@/game/gameobject/trait/SelfHealingTrait\";\nimport { AmmoTrait } from \"@/game/gameobject/trait/AmmoTrait\";\nimport { DisguiseTrait } from \"@/game/gameobject/trait/DisguiseTrait\";\nimport { InvulnerableTrait } from \"@/game/gameobject/trait/InvulnerableTrait\";\nimport { WarpedOutTrait } from \"@/game/gameobject/trait/WarpedOutTrait\";\nimport { TntChargeTrait } from \"@/game/gameobject/trait/TntChargeTrait\";\nimport { MindControllableTrait } from \"@/game/gameobject/trait/MindControllableTrait\";\nimport { MindControllerTrait } from \"@/game/gameobject/trait/MindControllerTrait\";\nimport { TemporalTrait } from \"@/game/gameobject/trait/TemporalTrait\";\nimport { CloakableTrait } from \"@/game/gameobject/trait/CloakableTrait\";\nimport { AirSpawnTrait } from \"@/game/gameobject/trait/AirSpawnTrait\";\nimport { SpawnDebrisTrait } from \"@/game/gameobject/trait/SpawnDebrisTrait\";\nimport { Debris } from \"@/game/gameobject/Debris\";\nimport { DebrisRules } from \"@/game/rules/DebrisRules\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { SensorsTrait } from \"@/game/gameobject/trait/SensorsTrait\";\nexport class ObjectFactory {\n    private tiles: any;\n    private tileOccupation: any;\n    private bridges: any;\n    private nextObjectId: any;\n    constructor(tiles: any, tileOccupation: any, bridges: any, nextObjectId: any) {\n        this.tiles = tiles;\n        this.tileOccupation = tileOccupation;\n        this.bridges = bridges;\n        this.nextObjectId = nextObjectId;\n    }\n    create(objectType: any, name: string, rulesIni: any, artIni: any): any {\n        let rules: any;\n        let art: any;\n        if (objectType === ObjectType.Debris) {\n            if (rulesIni.hasObject(name, ObjectType.VoxelAnim)) {\n                art = artIni.getObject(name, ObjectType.VoxelAnim);\n                rules = rulesIni.getObject(name, ObjectType.VoxelAnim);\n            }\n            else {\n                art = artIni.getAnimation(name);\n                rules = new DebrisRules(ObjectType.Debris, artIni.getIni().getOrCreateSection(name));\n            }\n        }\n        else {\n            if (objectType === ObjectType.Projectile) {\n                rules = rulesIni.getProjectile(name);\n                if (rules.inviso) {\n                    art = new ObjectArt(ObjectType.Projectile, rules, new IniSection(name));\n                }\n                else {\n                    art = artIni.getProjectile(name);\n                }\n            }\n            else {\n                rules = rulesIni.getObject(name, objectType);\n                art = artIni.getObject(name, objectType);\n            }\n        }\n        let gameObject: any;\n        switch (objectType) {\n            case ObjectType.Building:\n                gameObject = Building.factory(name, rules, rulesIni, art, this.tiles, this.bridges);\n                break;\n            case ObjectType.Infantry:\n                gameObject = Infantry.factory(name, rules, art, this.tileOccupation);\n                break;\n            case ObjectType.Vehicle:\n                gameObject = Vehicle.factory(name, rules, art, rulesIni, this.tileOccupation);\n                break;\n            case ObjectType.Aircraft:\n                gameObject = Aircraft.factory(name, rules, art, rulesIni, this.tileOccupation);\n                break;\n            case ObjectType.Terrain:\n                gameObject = Terrain.factory(name, rules, art);\n                break;\n            case ObjectType.Overlay:\n                gameObject = Overlay.factory(name, rules, art);\n                break;\n            case ObjectType.Smudge:\n                gameObject = Smudge.factory(name, rules, art);\n                break;\n            case ObjectType.Projectile:\n                gameObject = Projectile.factory(name, rules, art, this.tileOccupation);\n                break;\n            case ObjectType.Debris:\n                gameObject = Debris.factory(name, rules, art, this.tileOccupation);\n                break;\n            default:\n                throw new Error(\"Not implemented\");\n        }\n        gameObject.id = this.nextObjectId.value++;\n        gameObject.position = new ObjectPosition(this.tiles, this.tileOccupation);\n        if (gameObject.isUnit()) {\n            gameObject.position.subCell = 0;\n        }\n        else if (gameObject.isBuilding()) {\n            gameObject.position.setCenterOffset(gameObject.getFoundationCenterOffset());\n        }\n        if (gameObject.isTechno()) {\n            if (gameObject.rules.primary ||\n                gameObject.rules.secondary ||\n                gameObject.rules.weaponCount ||\n                gameObject.rules.explodes) {\n                gameObject.armedTrait = new ArmedTrait(gameObject, rulesIni);\n                gameObject.traits.add(gameObject.armedTrait);\n            }\n            if (gameObject.rules.ammo !== -1) {\n                const initialAmmo = gameObject.rules.initialAmmo;\n                gameObject.ammoTrait = new AmmoTrait(gameObject.rules.ammo, initialAmmo !== -1 ? initialAmmo : undefined);\n                gameObject.traits.add(gameObject.ammoTrait);\n            }\n            gameObject.unitOrderTrait = new UnitOrderTrait(gameObject);\n            gameObject.traits.addToFront(gameObject.unitOrderTrait);\n            if (gameObject.primaryWeapon || gameObject.secondaryWeapon) {\n                gameObject.attackTrait = new AttackTrait(this.tiles, this.tileOccupation);\n                gameObject.traits.add(gameObject.attackTrait);\n            }\n            if ((gameObject.isInfantry() || gameObject.isVehicle()) && gameObject.rules.deployer) {\n                gameObject.deployerTrait = new DeployerTrait(gameObject);\n                gameObject.traits.add(gameObject.deployerTrait);\n            }\n            if ((gameObject.isInfantry() || gameObject.isVehicle()) && gameObject.rules.canDisguise) {\n                gameObject.disguiseTrait = new DisguiseTrait();\n                gameObject.traits.add(gameObject.disguiseTrait);\n            }\n            if (gameObject.rules.cloakable) {\n                gameObject.cloakableTrait = new CloakableTrait(gameObject, rulesIni.general.cloakDelay);\n                gameObject.traits.add(gameObject.cloakableTrait);\n            }\n            if (gameObject.rules.sensors) {\n                gameObject.sensorsTrait = new SensorsTrait();\n                gameObject.traits.add(gameObject.sensorsTrait);\n            }\n            gameObject.autoRepairTrait = new AutoRepairTrait(!gameObject.isBuilding());\n            gameObject.traits.add(gameObject.autoRepairTrait);\n            if (gameObject.rules.trainable) {\n                gameObject.veteranTrait = new VeteranTrait(gameObject, rulesIni.general.veteran);\n                gameObject.traits.add(gameObject.veteranTrait);\n            }\n            if (gameObject.rules.selfHealing) {\n                gameObject.traits.add(new SelfHealingTrait());\n            }\n            gameObject.invulnerableTrait = new InvulnerableTrait();\n            gameObject.traits.add(gameObject.invulnerableTrait);\n            gameObject.warpedOutTrait = new WarpedOutTrait(gameObject);\n            gameObject.traits.add(gameObject.warpedOutTrait);\n            gameObject.temporalTrait = new TemporalTrait(gameObject);\n            gameObject.traits.add(gameObject.temporalTrait);\n            if (gameObject.rules.bombable) {\n                gameObject.tntChargeTrait = new TntChargeTrait();\n                gameObject.traits.add(gameObject.tntChargeTrait);\n            }\n            if (!gameObject.rules.immuneToPsionics && !gameObject.isBuilding()) {\n                gameObject.mindControllableTrait = new MindControllableTrait(gameObject);\n                gameObject.traits.add(gameObject.mindControllableTrait);\n            }\n            const weapons = [gameObject.primaryWeapon, gameObject.secondaryWeapon];\n            if (weapons.some(weapon => weapon?.warhead.rules.mindControl)) {\n                gameObject.mindControllerTrait = new MindControllerTrait(gameObject);\n                gameObject.traits.add(gameObject.mindControllerTrait);\n            }\n            if (gameObject.rules.spawns) {\n                gameObject.airSpawnTrait = new AirSpawnTrait();\n                gameObject.traits.add(gameObject.airSpawnTrait);\n            }\n            if (gameObject.rules.maxDebris) {\n                gameObject.traits.add(new SpawnDebrisTrait());\n            }\n        }\n        if (gameObject.isTechno() || gameObject.isOverlay() || gameObject.isTerrain()) {\n            const isBridgeOverlay = gameObject.isOverlay() &&\n                BridgeOverlayTypes.isBridge(rulesIni.getOverlayId(gameObject.name));\n            let strength = gameObject.rules.strength;\n            if (!strength && gameObject.isTerrain()) {\n                strength = rulesIni.general.treeStrength;\n            }\n            if (isBridgeOverlay) {\n                strength = rulesIni.combatDamage.bridgeStrength;\n            }\n            const hitPointsRaw = strength;\n            let hitPoints = typeof hitPointsRaw === \"number\" && Number.isFinite(hitPointsRaw)\n                ? Math.floor(hitPointsRaw)\n                : 0;\n            if (hitPoints <= 0) {\n                hitPoints = 1;\n            }\n            if (hitPoints || gameObject.isTechno()) {\n                gameObject.healthTrait = new HealthTrait(hitPoints, gameObject, rulesIni.audioVisual.conditionYellow, rulesIni.audioVisual.conditionRed);\n                gameObject.traits.add(gameObject.healthTrait);\n            }\n            if (gameObject.isOverlay() && isBridgeOverlay) {\n                gameObject.bridgeTrait = new BridgeTrait(this.bridges);\n                gameObject.traits.add(gameObject.bridgeTrait);\n                if (BridgeOverlayTypes.getOverlayBridgeType(rulesIni.getOverlayId(gameObject.name)) === OverlayBridgeType.Concrete) {\n                    gameObject.traits.add(new SpawnDebrisTrait());\n                }\n            }\n        }\n        if (gameObject.isOverlay() &&\n            OreOverlayTypes.getOverlayTibType(rulesIni.getOverlayId(gameObject.name)) !== OverlayTibType.NotSpecial) {\n            gameObject.traits.add(new TiberiumTrait(gameObject));\n        }\n        if (gameObject.isTerrain() && gameObject.rules.spawnsTiberium) {\n            gameObject.traits.add(new TiberiumTreeTrait(gameObject.rules));\n        }\n        gameObject.cachedTraits.tick.push(...gameObject.traits.filter(NotifyTick));\n        return gameObject;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/ObjectPosition.ts",
    "content": "import { Coords } from '../Coords';\nimport { EventDispatcher } from '../../util/event';\nimport { rampHeights } from '@/game/theater/rampHeights';\nimport { Vector3 } from '../math/Vector3';\nimport { Vector2 } from '../math/Vector2';\nimport { roundToDecimals } from '../../util/math';\ninterface Tile {\n    rx: number;\n    ry: number;\n    z: number;\n    rampType: number;\n    onBridgeLandType?: boolean;\n}\ninterface Tiles {\n    getByMapCoords(rx: number, ry: number): Tile | undefined;\n    getPlaceholderTile(rx: number, ry: number): Tile;\n}\ninterface TileOccupation {\n    getBridgeOnTile(tile: Tile): any;\n}\ninterface PositionChangeEvent {\n    tileChanged: boolean;\n}\nexport class ObjectPosition {\n    private tiles: Tiles;\n    private tileOccupation: TileOccupation;\n    private _worldPosition: Vector3;\n    private _tile?: Tile;\n    private _tileOffset: Vector2;\n    private _centerOffset: Vector2;\n    private desiredSubCell: number;\n    private _tileElevation?: number;\n    private _absoluteElevation?: number;\n    private _computedTileElevation?: number;\n    private _onPositionChange: EventDispatcher<ObjectPosition, PositionChangeEvent>;\n    constructor(tiles: Tiles, tileOccupation: TileOccupation) {\n        this.tiles = tiles;\n        this.tileOccupation = tileOccupation;\n        this._worldPosition = new Vector3();\n        this._tileOffset = new Vector2();\n        this._centerOffset = new Vector2();\n        this.desiredSubCell = 0;\n        this._tileElevation = 0;\n        this._onPositionChange = new EventDispatcher<ObjectPosition, PositionChangeEvent>();\n    }\n    get onPositionChange() {\n        return this._onPositionChange.asEvent();\n    }\n    get worldPosition(): Vector3 {\n        return this._worldPosition;\n    }\n    get tile(): Tile | undefined {\n        return this._tile;\n    }\n    set tile(tile: Tile | undefined) {\n        const tileChanged = !!this._tile && tile !== this._tile;\n        this._tile = tile;\n        if (tile) {\n            this.updateWorldPosition(tile, this._tileOffset);\n            this._onPositionChange.dispatch(this, { tileChanged });\n        }\n    }\n    get tileElevation(): number {\n        if (this._tileElevation === undefined) {\n            if (this._computedTileElevation === undefined) {\n                this._computedTileElevation = this.computeTileElevationFromWorldPos();\n            }\n            return this._computedTileElevation;\n        }\n        return this._tileElevation;\n    }\n    set tileElevation(elevation: number) {\n        this._absoluteElevation = undefined;\n        this._tileElevation = elevation;\n        if (this._tile) {\n            this.updateWorldPosition(this._tile, this._tileOffset);\n            this._onPositionChange.dispatch(this, { tileChanged: false });\n        }\n    }\n    get subCell(): number {\n        if (!this._tileOffset.x && !this._tileOffset.y)\n            return 0;\n        const signX = Math.sign(this._tileOffset.x / Coords.LEPTONS_PER_TILE - 0.5);\n        const signY = Math.sign(this._tileOffset.y / Coords.LEPTONS_PER_TILE - 0.5);\n        return signX && signY ? signY + 1 + (signX + 1) / 2 + 1 : 0;\n    }\n    set subCell(subCell: number) {\n        this._tileOffset = this.computeSubCellOffset(subCell);\n        this.desiredSubCell = subCell;\n        if (this._tile) {\n            this.updateWorldPosition(this._tile, this._tileOffset);\n            this._onPositionChange.dispatch(this, { tileChanged: false });\n        }\n    }\n    getTileOffset(): Vector2 {\n        return this._tileOffset.clone();\n    }\n    setTileOffset(offset: Vector2): void {\n        this._tileOffset.copy(offset);\n        if (this._tile) {\n            this.updateWorldPosition(this._tile, this._tileOffset);\n            this._onPositionChange.dispatch(this, { tileChanged: false });\n        }\n    }\n    setCenterOffset(offset: Vector2): void {\n        this._centerOffset.copy(offset);\n        if (this._tile) {\n            this.updateWorldPosition(this._tile, this._tileOffset);\n            this._onPositionChange.dispatch(this, { tileChanged: false });\n        }\n    }\n    getMapPosition(): Vector2 | undefined {\n        if (this._tile) {\n            return new Vector2(this._tile.rx * Coords.LEPTONS_PER_TILE + this._tileOffset.x + this._centerOffset.x, this._tile.ry * Coords.LEPTONS_PER_TILE + this._tileOffset.y + this._centerOffset.y);\n        }\n    }\n    getBridgeBelow(): any {\n        return this._tile?.onBridgeLandType\n            ? this.tileOccupation.getBridgeOnTile(this._tile)\n            : undefined;\n    }\n    moveToTileCell(tile: Tile, subCell: number = 0): void {\n        if (!this._tile)\n            throw new Error(\"Tile is not set\");\n        const tileChanged = tile !== this._tile;\n        this._tile = tile;\n        this._tileOffset = this.computeSubCellOffset(subCell);\n        this.desiredSubCell = subCell;\n        this.updateWorldPosition(tile, this._tileOffset);\n        this._onPositionChange.dispatch(this, { tileChanged });\n    }\n    moveToTileCoords(x: number, y: number, allowPlaceholder: boolean = false): void {\n        const rx = Math.floor(x);\n        const ry = Math.floor(y);\n        const tileChanged = !this._tile || this._tile.rx !== rx || this._tile.ry !== ry;\n        if (tileChanged) {\n            let tile = this.tiles.getByMapCoords(rx, ry);\n            if (!tile) {\n                if (!allowPlaceholder) {\n                    throw new RangeError(`Attempted move to a non-existent tile: [${rx},${ry}]`);\n                }\n                tile = this.tiles.getPlaceholderTile(rx, ry);\n            }\n            this._tile = tile;\n        }\n        this._tileOffset.set((x - rx) * Coords.LEPTONS_PER_TILE, (y - ry) * Coords.LEPTONS_PER_TILE);\n        this.updateWorldPosition(this._tile!, this._tileOffset);\n        this._onPositionChange.dispatch(this, { tileChanged });\n    }\n    moveToLeptons(leptons: Vector2, allowPlaceholder: boolean = false): void {\n        this.moveToTileCoords(leptons.x / Coords.LEPTONS_PER_TILE, leptons.y / Coords.LEPTONS_PER_TILE, allowPlaceholder);\n    }\n    moveByLeptons(deltaX: number, deltaY: number, allowPlaceholder: boolean = false): void {\n        if (!this._tile)\n            throw new Error(\"Tile is not set\");\n        this.moveToTileCoords(this._tile.rx + (this._tileOffset.x + deltaX) / Coords.LEPTONS_PER_TILE, this._tile.ry + (this._tileOffset.y + deltaY) / Coords.LEPTONS_PER_TILE, allowPlaceholder);\n    }\n    moveByLeptons3(delta: Vector3, allowPlaceholder: boolean = false): void {\n        const currentY = this._worldPosition.y;\n        this.moveByLeptons(delta.x, delta.z, allowPlaceholder);\n        this.setAbsoluteElevationWorld(currentY + delta.y);\n    }\n    setAbsoluteElevationWorld(elevation: number): void {\n        this._absoluteElevation = elevation;\n        this._tileElevation = undefined;\n        if (this._tile) {\n            this.updateWorldPosition(this._tile, this._tileOffset);\n            this._onPositionChange.dispatch(this, { tileChanged: false });\n        }\n    }\n    computeSubCellOffset(subCell: number): Vector2 {\n        let offset = { width: 0, height: 0 };\n        if (subCell) {\n            const signX = ((subCell - 1) % 2) * 2 - 1;\n            const signY = 2 * Math.floor((subCell - 1) / 2) - 1;\n            offset = {\n                width: (signX * Coords.LEPTONS_PER_TILE) / 4,\n                height: (signY * Coords.LEPTONS_PER_TILE) / 4\n            };\n        }\n        const half = Coords.LEPTONS_PER_TILE / 2;\n        return new Vector2(half + offset.width, half + offset.height);\n    }\n    interpolateRampHeight(x: number, y: number, rampType: number): number {\n        const heights = rampHeights[rampType];\n        const h1 = heights[1];\n        const h0 = heights[0];\n        return (h1 * (1 - x) * (1 - y) +\n            heights[2] * x * (1 - y) +\n            h0 * (1 - x) * y +\n            heights[3] * x * y);\n    }\n    updateWorldPosition(tile: Tile, offset: Vector2): void {\n        const x = offset.x + this._centerOffset.x;\n        const y = offset.y + this._centerOffset.y;\n        const normalizedX = x / Coords.LEPTONS_PER_TILE;\n        const normalizedY = y / Coords.LEPTONS_PER_TILE;\n        let worldY: number;\n        if (this._tileElevation !== undefined) {\n            let rampHeight = 0;\n            if (tile.rampType !== 0) {\n                rampHeight = this.interpolateRampHeight(normalizedX, normalizedY, tile.rampType);\n            }\n            worldY = Coords.tileHeightToWorld(tile.z + rampHeight + this._tileElevation);\n        }\n        else {\n            worldY = this._absoluteElevation!;\n        }\n        this._worldPosition.set(tile.rx * Coords.LEPTONS_PER_TILE + x, worldY, tile.ry * Coords.LEPTONS_PER_TILE + y);\n        if (this._tileElevation === undefined) {\n            this._computedTileElevation = this.computeTileElevationFromWorldPos();\n        }\n    }\n    computeTileElevationFromWorldPos(): number {\n        if (!this._tile)\n            return 0;\n        const tileHeight = roundToDecimals(Coords.worldToTileHeight(this._worldPosition.y), 14);\n        const normalizedX = (this._tileOffset.x + this._centerOffset.x) / Coords.LEPTONS_PER_TILE;\n        const normalizedY = (this._tileOffset.y + this._centerOffset.y) / Coords.LEPTONS_PER_TILE;\n        let rampHeight = 0;\n        if (this._tile.rampType !== 0) {\n            rampHeight = this.interpolateRampHeight(normalizedX, normalizedY, this._tile.rampType);\n        }\n        return tileHeight - this._tile.z - rampHeight;\n    }\n    clone(): ObjectPosition {\n        const cloned = new ObjectPosition(this.tiles, this.tileOccupation);\n        cloned._worldPosition = this._worldPosition.clone();\n        cloned._tile = this._tile;\n        cloned._tileOffset = this._tileOffset.clone();\n        cloned._centerOffset = this._centerOffset.clone();\n        cloned._tileElevation = this._tileElevation;\n        cloned._absoluteElevation = this._absoluteElevation;\n        cloned._computedTileElevation = this._computedTileElevation;\n        return cloned;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Overlay.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { BridgeOverlayTypes } from '@/game/map/BridgeOverlayTypes';\nimport { OreOverlayTypes } from '@/game/map/OreOverlayTypes';\nimport { OverlayTibType } from '@/engine/type/OverlayTibType';\nimport { WallTrait } from '@/game/gameobject/trait/WallTrait';\nexport class Overlay extends GameObject {\n    radarInvisible: boolean;\n    wallTrait?: WallTrait;\n    static factory(id: string, rules: any, owner: any): Overlay {\n        const overlay = new this(id, rules, owner);\n        if (rules.wall) {\n            overlay.wallTrait = new WallTrait();\n            overlay.traits.add(overlay.wallTrait);\n        }\n        return overlay;\n    }\n    constructor(id: string, rules: any, owner: any) {\n        super(ObjectType.Overlay, id, rules, owner);\n        this.radarInvisible = this.rules.radarInvisible;\n    }\n    isTiberium(): boolean {\n        return OreOverlayTypes.getOverlayTibType(this.overlayId) !== OverlayTibType.NotSpecial;\n    }\n    isBridge(): boolean {\n        return BridgeOverlayTypes.isBridge(this.overlayId);\n    }\n    isXBridge(): boolean {\n        return BridgeOverlayTypes.isXBridge(this.overlayId);\n    }\n    isHighBridge(): boolean {\n        return BridgeOverlayTypes.isHighBridge(this.overlayId);\n    }\n    isLowBridge(): boolean {\n        return BridgeOverlayTypes.isLowBridge(this.overlayId);\n    }\n    isBridgePlaceholder(): boolean {\n        return BridgeOverlayTypes.isBridgePlaceholder(this.overlayId);\n    }\n    getFoundation(): {\n        width: number;\n        height: number;\n    } {\n        const foundation = { width: 1, height: 1 };\n        if (this.isBridge()) {\n            if (this.isXBridge()) {\n                foundation.height += 2;\n            }\n            else {\n                foundation.width += 2;\n            }\n        }\n        return foundation;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Projectile.ts",
    "content": "import { GameObject } from './GameObject';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { Weapon } from '@/game/Weapon';\nimport { WeaponType } from '@/game/WeaponType';\nimport { FacingUtil } from './unit/FacingUtil';\nimport { Coords } from '@/game/Coords';\nimport { ZoneType } from './unit/ZoneType';\nimport { TileOccupation, LayerType } from '@/game/map/TileOccupation';\nimport { RadialTileFinder } from '@/game/map/tileFinder/RadialTileFinder';\nimport { RangeHelper } from './unit/RangeHelper';\nimport { TargetUtil } from './unit/TargetUtil';\nimport * as geometry from '@/game/math/geometry';\nimport { RandomTileFinder } from '@/game/map/tileFinder/RandomTileFinder';\nimport { clamp } from '@/util/math';\nimport { MovementZone } from '@/game/type/MovementZone';\nimport { StanceType } from './infantry/StanceType';\nimport { MovePositionHelper } from './unit/MovePositionHelper';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { VeteranLevel } from './unit/VeteranLevel';\nimport { ScatterTask } from './task/ScatterTask';\nimport { Warhead } from '@/game/Warhead';\nimport { ObjectRules } from '@/game/rules/ObjectRules';\nimport { CollisionHelper } from './unit/CollisionHelper';\nimport { CollisionType } from './unit/CollisionType';\nimport { Vector2 } from '@/game/math/Vector2';\nimport { Vector3 } from '@/game/math/Vector3';\nexport enum ProjectileState {\n    Travel = 0,\n    Impact = 1,\n    Detonation = 2\n}\nexport class Projectile extends GameObject {\n    private _fromObject?: any;\n    private tileOccupation: TileOccupation;\n    private state: ProjectileState;\n    private detonationTimer: number;\n    private collisionType: CollisionType;\n    private direction: number;\n    private zone: ZoneType;\n    private isShrapnel: boolean;\n    private isNuke: boolean;\n    private baseDamageMultiplier: number;\n    private veteranDamageMult: number;\n    private snapToTarget: boolean;\n    private targetLockLost: boolean;\n    private limboTravelTicks: number;\n    private homingTravelDistance: number;\n    private homingTravelTicks: number;\n    private velocity: Vector3;\n    private sonicVisitedObjects: Map<any, Set<any>>;\n    private collisionHelper: CollisionHelper;\n    private initialSelfPosition?: Vector3;\n    private target: any;\n    private fromWeapon: any;\n    private fromPlayer: any;\n    private maxSpeed?: number;\n    private initialTileDistToTarget?: number;\n    private homingMoveDir?: Vector3;\n    private aimPoint?: Vector3;\n    private overshootTiles?: number;\n    private lastTargetLockPosition?: Vector3;\n    private speed?: number;\n    private impactAnim?: string;\n    get fromObject() {\n        return this._fromObject;\n    }\n    set fromObject(value: any) {\n        this._fromObject = value;\n        if (value && value.veteranTrait && !value.isDestroyed) {\n            this.veteranDamageMult = value.veteranTrait.getVeteranDamageMultiplier();\n        }\n    }\n    get rot(): number {\n        return this.fromWeapon.rules.isSonic\n            ? ObjectRules.iniRotToDegsPerTick(this.iniRot)\n            : this.rules.rot;\n    }\n    get iniRot(): number {\n        return this.fromWeapon.rules.isSonic ? 10 : this.rules.iniRot;\n    }\n    static factory(name: string, rules: any, art: any, tileOccupation: TileOccupation): Projectile {\n        return new this(name, rules, art, tileOccupation);\n    }\n    constructor(name: string, rules: any, art: any, tileOccupation: TileOccupation) {\n        super(ObjectType.Projectile, name, rules, art);\n        this.tileOccupation = tileOccupation;\n        this.state = ProjectileState.Travel;\n        this.detonationTimer = 0;\n        this.collisionType = CollisionType.None;\n        this.direction = 0;\n        this.zone = ZoneType.Air;\n        this.isShrapnel = false;\n        this.isNuke = false;\n        this.baseDamageMultiplier = 1;\n        this.veteranDamageMult = 1;\n        this.snapToTarget = false;\n        this.targetLockLost = false;\n        this.limboTravelTicks = 0;\n        this.homingTravelDistance = 0;\n        this.homingTravelTicks = 0;\n        this.velocity = new Vector3();\n        this.sonicVisitedObjects = new Map();\n        this.collisionHelper = new CollisionHelper(tileOccupation);\n    }\n    onSpawn(game: any): void {\n        super.onSpawn(game);\n        this.initialSelfPosition = this.position.worldPosition.clone();\n        if (!this.target.obj ||\n            this.fromWeapon.type === WeaponType.DeathWeapon ||\n            this.fromWeapon.rules.limboLaunch ||\n            (!this.isHoming() && this.fromWeapon.speed === Number.POSITIVE_INFINITY) ||\n            this.rules.inaccurate ||\n            this.rules.arcing ||\n            this.rules.flakScatter ||\n            this.isPrismSupportBeam(game)) {\n        }\n        else {\n            let damage = this.computeBaseDamage(game);\n            if (damage > 0) {\n                damage = this.fromWeapon.warhead.computeDamage(damage, this.target.obj, game);\n                this.target.obj.healthTrait?.projectDamage(damage);\n            }\n        }\n        game.afterTick(() => {\n            const rangeHelper = new RangeHelper(this.tileOccupation);\n            const tileDistance = rangeHelper.distance2(this.target.getWorldCoords(), this as any) / Coords.LEPTONS_PER_TILE;\n            this.initialTileDistToTarget = tileDistance;\n            this.maxSpeed = this.computeMaxSpeed(this.fromWeapon.speed, tileDistance, game.rules.audioVisual.gravity);\n        });\n        if (this.isHoming()) {\n            if (this.iniRot === 1) {\n                this.homingMoveDir = this.target\n                    .getWorldCoords()\n                    .clone()\n                    .sub(this.position.worldPosition);\n            }\n            if (this.fromObject?.isAircraft() &&\n                this.rules.isAntiGround &&\n                !this.rules.isAntiAir) {\n                const targetObj = this.target.obj;\n                if (targetObj?.isVehicle() &&\n                    !targetObj.isDestroyed &&\n                    targetObj.veteranLevel === VeteranLevel.Elite &&\n                    !targetObj.unitOrderTrait.hasTasks()) {\n                    targetObj.unitOrderTrait.addTask(new ScatterTask(game as any, undefined as any, undefined as any));\n                }\n            }\n        }\n        else if (this.rules.vertical) {\n            const pos = this.position.clone();\n            pos.tileElevation = this.fromWeapon.warhead.rules.nukeMaker\n                ? Coords.worldToTileHeight(this.fromWeapon.projectileRules.detonationAltitude)\n                : 0;\n            this.aimPoint = pos.worldPosition.clone();\n        }\n        else {\n            const initialTargetPos = this.target.getWorldCoords().clone();\n            game.afterTick(() => {\n                const targetMovement = this.target.getWorldCoords().clone().sub(initialTargetPos);\n                const targetMoved = targetMovement.length() > Coords.LEPTONS_PER_TILE;\n                let aimPoint = targetMoved\n                    ? initialTargetPos\n                    : this.target.obj?.isUnit() &&\n                        this.target.obj.moveTrait.velocity.length() &&\n                        isFinite(this.maxSpeed!)\n                        ? this.computeAimPointVersusMovingTarget(this.target.obj, this.maxSpeed!, this.position.worldPosition, game.map)\n                        : this.target.getWorldCoords().clone();\n                this.aimPoint = aimPoint;\n                this.snapToTarget = !targetMoved &&\n                    isFinite(this.maxSpeed!) &&\n                    !this.fromWeapon.warhead.rules.sonic;\n                if (this.rules.inaccurate || this.rules.flakScatter) {\n                    this.adjustAimForBallisticScatter(game, aimPoint);\n                    this.snapToTarget = false;\n                }\n                if (!targetMoved && this.rules.arcing) {\n                    if (this.rules.inaccurate) {\n                        this.overshootTiles = this.calculateInaccurateBallisticOvershoot(game);\n                        this.snapToTarget = false;\n                    }\n                    else if (this.target.obj?.isVehicle() &&\n                        this.target.obj.moveTrait.isMoving()) {\n                        this.overshootTiles = this.calculateBallisticOvershootVsMoving(game, this.target.obj);\n                        if (this.overshootTiles) {\n                            this.snapToTarget = false;\n                        }\n                    }\n                }\n                const toTarget = aimPoint.clone().sub(this.position.worldPosition);\n                if (toTarget.length() < this.fromWeapon.speed) {\n                    this.update(game);\n                }\n            });\n        }\n    }\n    private adjustAimForBallisticScatter(game: any, aimPoint: Vector3): void {\n        let scatter = game.rules.combatDamage.ballisticScatter;\n        let scatterAmount: number;\n        if (this.rules.flakScatter) {\n            if (this.rules.inviso) {\n                scatter *= 2;\n            }\n            scatterAmount = game.generateRandom() * scatter;\n        }\n        else {\n            scatterAmount = scatter / 2 + game.generateRandom() * (scatter / 2);\n        }\n        let scatterDistance = scatterAmount * Coords.LEPTONS_PER_TILE;\n        if (this.rules.flakScatter) {\n            const distanceToTarget = aimPoint.clone().sub(this.initialSelfPosition!).length();\n            scatterDistance *= distanceToTarget / (this.fromWeapon.range * Coords.LEPTONS_PER_TILE);\n        }\n        const scatterVec = geometry.rotateVec2(new Vector2(scatterDistance, 0), game.generateRandomInt(0, 360));\n        const scatterTile = Coords.vecWorldToGround(aimPoint)\n            .add(scatterVec)\n            .multiplyScalar(1 / Coords.LEPTONS_PER_TILE)\n            .floor();\n        if (game.map.tiles.getByMapCoords(scatterTile.x, scatterTile.y)) {\n            aimPoint.add(new Vector3(scatterVec.x, 0, scatterVec.y));\n        }\n    }\n    private calculateBallisticOvershootVsMoving(game: any, target: any): number {\n        const toTarget = this.target\n            .getWorldCoords()\n            .clone()\n            .sub(this.initialSelfPosition!);\n        const toTargetGround = Coords.vecWorldToGround(toTarget);\n        const velocityGround = Coords.vecWorldToGround(target.moveTrait.velocity);\n        const angle = geometry.angleDegBetweenVec2(toTargetGround, velocityGround);\n        const angleFactor = (angle > 90 ? 180 - angle : angle) / 90;\n        const distance = toTarget.length() / Coords.LEPTONS_PER_TILE;\n        const overshootChance = (angleFactor * distance) / 5;\n        return game.generateRandom() <= overshootChance ? 2 * Math.min(1, distance / 5) : 0;\n    }\n    private calculateInaccurateBallisticOvershoot(game: any): number {\n        return game.generateRandom() <= 0.5 ? 2 : 0;\n    }\n    update(game: any): void {\n        if (this.maxSpeed === undefined)\n            return;\n        super.update(game);\n        if (this.state === ProjectileState.Impact) {\n            if (this.detonationTimer > 0) {\n                this.detonationTimer--;\n            }\n            else {\n                this.detonate(game, this.collisionType);\n            }\n            return;\n        }\n        const oldVelocity = this.velocity.clone();\n        const oldPosition = this.position.clone();\n        this.velocity.set(0, 0, 0);\n        if (this.fromWeapon.rules.limboLaunch) {\n            if (!this.fromObject) {\n                throw new Error(\"Limbo launch projectile must be fired from a unit\");\n            }\n            if (this.fromObject.isDestroyed) {\n                game.destroyObject(this);\n                return;\n            }\n        }\n        const currentSpeed = this.updateSpeed(this.maxSpeed);\n        this.speed = currentSpeed;\n        let targetPos = this.target.getWorldCoords();\n        if (this.lastTargetLockPosition &&\n            (this.targetLockLost ||\n                targetPos.clone().sub(this.lastTargetLockPosition).length() >= Coords.LEPTONS_PER_TILE)) {\n            targetPos = this.lastTargetLockPosition;\n            this.targetLockLost = true;\n        }\n        else {\n            this.lastTargetLockPosition = targetPos.clone();\n        }\n        if (this.isHoming()) {\n            if (this.target.obj?.isUnit() &&\n                (this.target.obj.isDestroyed ||\n                    this.target.obj.isCrashing ||\n                    !this.target.obj.isSpawned) &&\n                (this.fromWeapon.rules.limboLaunch ||\n                    this.homingTravelDistance >= 2 * Coords.LEPTONS_PER_TILE)) {\n                this.detonate(game);\n                return;\n            }\n            if (!this.homingMoveDir) {\n                const facingCoords = FacingUtil.toMapCoords(this.direction);\n                this.homingMoveDir = new Vector3(facingCoords.x, 0, facingCoords.y);\n                if (this.fromObject?.isAircraft()) {\n                    this.homingMoveDir.y = -9999999;\n                    this.homingMoveDir.normalize();\n                }\n            }\n            if (this.fromWeapon.rules.limboLaunch) {\n                if (!this.targetLockLost) {\n                    if (this.limboTravelTicks > 10) {\n                        this.position.moveToLeptons(this.target.obj.position.getMapPosition());\n                        this.position.tileElevation = this.target.obj.position.tileElevation;\n                        this.detonate(game);\n                        return;\n                    }\n                    this.limboTravelTicks++;\n                }\n            }\n            else if (!this.isInHomingRange(targetPos, game)) {\n                this.detonate(game);\n                return;\n            }\n            const rangeHelper = new RangeHelper(this.tileOccupation);\n            const tileDistance = Math.floor(rangeHelper.distance2(targetPos, this as any) / Coords.LEPTONS_PER_TILE);\n            const shouldTurn = tileDistance > 2 && this.iniRot > 1;\n            const toTarget = targetPos.clone().sub(this.position.worldPosition);\n            let verticalAdjustment = 0;\n            if (this.homingTravelTicks >= this.rules.courseLockDuration) {\n                if (shouldTurn) {\n                    geometry.rotateVec3Towards(this.homingMoveDir, new Vector3(toTarget.x, this.homingMoveDir.y, toTarget.z), this.rot);\n                    if (!this.rules.level) {\n                        const targetElevation = clamp(Math.floor(this.initialTileDistToTarget!) - 1, 0, 2) + clamp(tileDistance - 2, 0, 3);\n                        const bridgeElevation = this.tileOccupation.getBridgeOnTile(this.tile)?.tileElevation ?? 0;\n                        const elevationDiff = targetElevation - (this.position.tileElevation - bridgeElevation);\n                        if (elevationDiff) {\n                            const maxElevationChange = 0.25 + (6 / this.iniRot) * 0.1;\n                            verticalAdjustment = Coords.tileHeightToWorld(Math.sign(elevationDiff) * Math.min(Math.abs(elevationDiff), maxElevationChange));\n                        }\n                    }\n                }\n                else {\n                    geometry.rotateVec3Towards(this.homingMoveDir, toTarget, this.rot);\n                }\n            }\n            this.direction = FacingUtil.fromMapCoords(new Vector2(this.homingMoveDir.x, this.homingMoveDir.z));\n            const distanceToTarget = toTarget.length();\n            const moveDistance = Math.min(distanceToTarget, currentSpeed);\n            this.homingTravelDistance += moveDistance;\n            this.homingTravelTicks++;\n            let shouldDetonate = false;\n            let collisionType = CollisionType.None;\n            let collisionTarget: any;\n            if (moveDistance >= 1) {\n                const moveVector = this.homingMoveDir.clone().setLength(moveDistance);\n                if (verticalAdjustment) {\n                    moveVector.y += verticalAdjustment;\n                }\n                if (moveDistance === currentSpeed) {\n                    this.velocity.copy(moveVector);\n                }\n                const newPos = moveVector.clone().add(this.position.worldPosition);\n                if (game.map.mapBounds.isWithinHardBounds(newPos)) {\n                    this.position.moveByLeptons3(moveVector);\n                }\n                else {\n                    shouldDetonate = true;\n                }\n                const collision = this.checkObstacles(oldPosition, game);\n                collisionType = collision.type;\n                collisionTarget = collision.target;\n                if (collisionType || moveDistance < currentSpeed) {\n                    shouldDetonate = true;\n                }\n            }\n            else {\n                this.position.moveByLeptons3(toTarget);\n                shouldDetonate = true;\n            }\n            if (shouldDetonate) {\n                if (collisionTarget && collisionType === CollisionType.Wall) {\n                    const wallPos = collisionTarget.position.worldPosition;\n                    this.position.moveByLeptons3(wallPos.clone().sub(this.position.worldPosition));\n                }\n                this.collisionType = collisionType;\n                this.detonate(game, collisionType);\n            }\n        }\n        else {\n            const toAimPoint = this.aimPoint!\n                .clone()\n                .sub(this.position.worldPosition);\n            if (!this.rules.vertical) {\n                this.direction = FacingUtil.fromMapCoords(new Vector2(toAimPoint.x, toAimPoint.z));\n            }\n            if (this.rules.arcing) {\n                toAimPoint.y = 0;\n            }\n            const moveDistance = Math.min(toAimPoint.length(), currentSpeed);\n            toAimPoint.setLength(moveDistance);\n            if (this.rules.arcing) {\n                const currentOffset = Coords.vecWorldToGround(this.position.worldPosition\n                    .clone()\n                    .sub(this.initialSelfPosition!)\n                    .add(toAimPoint));\n                const totalOffset = this.aimPoint!\n                    .clone()\n                    .sub(this.initialSelfPosition!);\n                const currentDistance = currentOffset.length();\n                const totalDistance = Coords.vecWorldToGround(totalOffset).length();\n                const targetHeight = totalOffset.y;\n                const gravity = game.rules.audioVisual.gravity;\n                toAimPoint.y =\n                    (((targetHeight / totalDistance) * currentSpeed + ((gravity / 2) * totalDistance) / currentSpeed) * currentDistance) / currentSpeed -\n                        (gravity * (currentDistance / currentSpeed) * (currentDistance / currentSpeed)) / 2 +\n                        this.initialSelfPosition!.y -\n                        this.position.worldPosition.y;\n            }\n            let shouldDetonate = false;\n            const newPos = toAimPoint.clone().add(this.position.worldPosition);\n            if (game.map.isWithinHardBounds(newPos)) {\n                this.position.moveByLeptons3(toAimPoint);\n            }\n            else {\n                shouldDetonate = true;\n            }\n            let collisionType = CollisionType.None;\n            let collisionTarget: any;\n            if (moveDistance >= 1) {\n                if (moveDistance !== currentSpeed && !this.overshootTiles) {\n                }\n                else {\n                    this.velocity.copy(toAimPoint);\n                }\n                const collision = this.checkObstacles(oldPosition, game);\n                collisionType = collision.type;\n                collisionTarget = collision.target;\n                if (collisionType || moveDistance < currentSpeed) {\n                    shouldDetonate = true;\n                }\n            }\n            else {\n                shouldDetonate = true;\n            }\n            if (shouldDetonate) {\n                if (collisionType) {\n                    if (collisionTarget && collisionType === CollisionType.Wall) {\n                        const wallPos = collisionTarget.isBuilding()\n                            ? Coords.tile3dToWorld(collisionTarget.tile.rx + 0.5, collisionTarget.tile.ry + 0.5, collisionTarget.tile.z)\n                            : collisionTarget.position.worldPosition;\n                        this.position.moveByLeptons3(wallPos.clone().sub(this.position.worldPosition));\n                    }\n                }\n                else if (this.overshootTiles) {\n                    const overshootVec = Coords.vecWorldToGround(oldVelocity).setLength(this.overshootTiles * Coords.LEPTONS_PER_TILE);\n                    geometry.rotateVec2(overshootVec, game.generateRandomInt(-45, 45));\n                    const overshootPos = Coords.vecGroundToWorld(overshootVec).add(this.position.worldPosition);\n                    if (!game.map.isWithinHardBounds(overshootPos)) {\n                        game.unspawnObject(this);\n                        return;\n                    }\n                    this.position.moveByLeptons(overshootVec.x, overshootVec.y);\n                }\n                else if (this.snapToTarget && !this.targetLockLost) {\n                    if (!game.map.isWithinHardBounds(targetPos)) {\n                        game.unspawnObject(this);\n                        return;\n                    }\n                    this.position.moveByLeptons3(targetPos.clone().sub(this.position.worldPosition));\n                }\n                this.collisionType = collisionType;\n                if (this.isNuke) {\n                    this.state = ProjectileState.Impact;\n                    this.detonationTimer = 2.5 * GameSpeed.BASE_TICKS_PER_SECOND;\n                }\n                else {\n                    this.detonate(game, collisionType);\n                }\n            }\n        }\n        const warhead = this.fromWeapon.warhead;\n        if (warhead.rules.sonic) {\n            const sonicRadius = (11 / 30) * Coords.LEPTONS_PER_TILE;\n            const sonicPos = this.position.worldPosition\n                .clone()\n                .add(this.velocity.clone().setLength(sonicRadius));\n            const sonicTile = Coords.vecWorldToGround(sonicPos)\n                .multiplyScalar(1 / Coords.LEPTONS_PER_TILE)\n                .floor();\n            const tile = game.map.tiles.getByMapCoords(sonicTile.x, sonicTile.y);\n            if (tile && tile !== this.fromObject?.tile) {\n                const tileZone = game.map.getTileZone(tile);\n                for (const obj of game.map.getGroundObjectsOnTile(tile)) {\n                    if ((!obj.isUnit() || !obj.onBridge) &&\n                        (!obj.isTechno() ||\n                            !obj.rules.typeImmune ||\n                            obj.owner !== this.fromPlayer ||\n                            obj.name !== this.fromObject?.name) &&\n                        (!obj.isAircraft() || !obj.rules.spawned) &&\n                        warhead.canDamage(obj, tile, tileZone)) {\n                        let visitedTiles = this.sonicVisitedObjects.get(obj) ?? new Set();\n                        visitedTiles.add(tile);\n                        this.sonicVisitedObjects.set(obj, visitedTiles);\n                    }\n                }\n            }\n            for (const [obj, tiles] of this.sonicVisitedObjects) {\n                for (const visitedTile of tiles) {\n                    if (game.map.tileOccupation.isTileOccupiedBy(visitedTile, obj) &&\n                        obj.isSpawned) {\n                        let damage = this.fromWeapon.rules.ambientDamage *\n                            this.veteranDamageMult *\n                            this.baseDamageMultiplier;\n                        damage = warhead.computeDamage(damage, obj, game);\n                        warhead.inflictDamage(damage, obj, {\n                            player: this.fromPlayer,\n                            weapon: this.fromWeapon,\n                            obj: this.fromObject,\n                        }, game, obj !== this.target.obj);\n                    }\n                }\n            }\n        }\n    }\n    private isHoming(): boolean {\n        return !!this.rot && !this.rules.arcing;\n    }\n    private isInHomingRange(targetPos: Vector3, game: any): boolean {\n        let inRange = true;\n        const targetObj = this.target.obj;\n        if (targetObj?.isUnit() && this.fromObject) {\n            const rangeHelper = new RangeHelper(this.tileOccupation);\n            const weaponRange = rangeHelper.computeWeaponRangeVsTarget(this.fromObject, targetObj, this.fromWeapon, game.rules).range;\n            if (this.fromWeapon.rules.limboLaunch) {\n                inRange = rangeHelper.isInRange3(this.initialSelfPosition!, targetPos, 0, weaponRange + 0.5);\n            }\n            else {\n                const targetSpeed = targetObj.moveTrait.velocity.length();\n                if (targetSpeed) {\n                    if (this.fromObject.rules.movementZone === MovementZone.Fly) {\n                        if (this.speed! / targetSpeed > 5) {\n                            inRange = rangeHelper.isInRange2(this.initialSelfPosition!, this.position.worldPosition, 0, weaponRange);\n                        }\n                    }\n                    else if (isFinite(this.fromWeapon.speed) &&\n                        this.fromWeapon.speed / targetObj.rules.speed > 3.5) {\n                        inRange = rangeHelper.isInRange3(this.initialSelfPosition!, this.position.worldPosition, 0, weaponRange);\n                    }\n                }\n            }\n        }\n        return inRange;\n    }\n    private updateSpeed(maxSpeed: number): number {\n        let speed: number;\n        if (this.isHoming() || this.rules.vertical) {\n            if (this.speed === undefined) {\n                speed = Math.min(maxSpeed, this.rules.acceleration);\n            }\n            else {\n                speed = Math.min(maxSpeed, this.speed + this.rules.acceleration);\n            }\n        }\n        else {\n            speed = maxSpeed;\n        }\n        return speed;\n    }\n    private computeMaxSpeed(weaponSpeed: number, tileDistance: number, gravity: number): number {\n        let maxSpeed = weaponSpeed;\n        if (this.rules.arcing) {\n            maxSpeed *= (1 + gravity / 6) / 2;\n            const floorDistance = Math.floor(tileDistance);\n            maxSpeed *= floorDistance <= 8 ? 1 : 1 + (floorDistance / 8) * 0.5;\n        }\n        if (this.fromWeapon.warhead.rules.sonic) {\n            maxSpeed = Math.ceil((tileDistance * Coords.LEPTONS_PER_TILE) / 21);\n        }\n        return maxSpeed;\n    }\n    private checkObstacles(oldPosition: any, game: any): {\n        type: CollisionType;\n        target?: any;\n    } {\n        if (this.fromWeapon.rules.limboLaunch) {\n            return { type: CollisionType.None };\n        }\n        return this.collisionHelper.checkCollisions(this.position, oldPosition, {\n            cliffs: this.rules.subjectToCliffs,\n            ground: this.isHoming(),\n            shore: this.rules.level,\n            walls: this.rules.subjectToWalls,\n            units: !this.rules.inaccurate &&\n                ((owner: any) => this.fromPlayer !== owner &&\n                    !game.alliances.areAllied(this.fromPlayer, owner)),\n        });\n    }\n    private isPrismSupportBeam(game: any): boolean {\n        const prismType = game.rules.general.prism.type;\n        return !!prismType &&\n            this.fromWeapon.type === WeaponType.Secondary &&\n            !!this.fromObject?.isBuilding() &&\n            this.fromObject.name === prismType;\n    }\n\n    private computeBaseDamage(game: any): number {\n        const weapon = this.fromWeapon;\n        const warhead = weapon.warhead;\n        let damage = weapon.rules.damage;\n        if (weapon.type === WeaponType.DeathWeapon && warhead.rules.ivanBomb) {\n            damage = game.rules.combatDamage.ivanDamage;\n        }\n        let totalDamage = damage * this.baseDamageMultiplier;\n        if (weapon.type === WeaponType.DeathWeapon && this.fromObject) {\n            totalDamage *= this.fromObject.rules.deathWeaponDamageModifier;\n        }\n        totalDamage *= this.veteranDamageMult;\n        return totalDamage;\n    }\n    private detonate(game: any, collisionType: CollisionType = CollisionType.None): void {\n        const weapon = this.fromWeapon;\n        let warhead = weapon.warhead;\n        const detonationZone = this.zone = this.collisionHelper.computeDetonationZone(this.tile, this.tileElevation, collisionType);\n        const detonationTile = this.tile;\n        if (weapon.type === WeaponType.DeathWeapon && warhead.rules.ivanBomb) {\n            warhead = new Warhead(game.rules.getWarhead(game.rules.combatDamage.ivanWarhead));\n        }\n        const damage = this.computeBaseDamage(game);\n        game.destroyObject(this);\n        this.state = ProjectileState.Detonation;\n        const targetObj = this.target.obj;\n        let parasiteSuccess = false;\n        // 寄生弹头秒杀步兵的标志（与载具寄生区分，步兵击杀后攻击单位需要返回地图）\n        let parasiteInfantryKill = false;\n        if (warhead.rules.parasite &&\n            targetObj?.isUnit() &&\n            detonationTile === targetObj.tile &&\n            warhead.canDamage(targetObj, detonationTile, detonationZone)) {\n            if (targetObj.isInfantry()) {\n                // 警犬和恐怖机器人的寄生弹头对步兵造成无限伤害，实现秒杀效果\n                const infiniteDamage = Number.POSITIVE_INFINITY;\n                warhead.inflictDamage(infiniteDamage, targetObj, {\n                    player: this.fromPlayer,\n                    weapon: weapon,\n                    obj: this.fromObject,\n                }, game, true);\n                // 不设置 parasiteSuccess，以便攻击单位能从 limbo 状态返回地图\n                parasiteInfantryKill = true;\n            }\n            else if (targetObj.parasiteableTrait && this.fromObject?.isUnit()) {\n                if (!(this.fromWeapon instanceof Weapon)) {\n                    throw new Error(\"Projectile with parasite warhead must have a weapon reference\");\n                }\n                targetObj.parasiteableTrait.infest(this.fromObject, this.fromWeapon);\n                parasiteSuccess = true;\n            }\n        }\n        let shouldDetonate = true;\n        // 寄生载具成功或秒杀步兵后，跳过普通弹头爆炸逻辑\n        if (parasiteSuccess || parasiteInfantryKill) {\n            shouldDetonate = false;\n        }\n        if (warhead.rules.sonic) {\n            shouldDetonate = false;\n        }\n        if (warhead.rules.ivanBomb) {\n            shouldDetonate = false;\n            if (targetObj?.isTechno() &&\n                targetObj.tntChargeTrait &&\n                !targetObj.tntChargeTrait.hasCharge() &&\n                !targetObj.isDestroyed &&\n                !targetObj.warpedOutTrait.isInvulnerable()) {\n                const delay = game.rules.combatDamage.ivanTimedDelay;\n                targetObj.tntChargeTrait.setCharge(delay, game.currentTick, {\n                    player: this.fromPlayer,\n                });\n            }\n        }\n        if (warhead.rules.bombDisarm) {\n            shouldDetonate = false;\n            if (targetObj?.isTechno() &&\n                targetObj.tntChargeTrait?.hasCharge() &&\n                !targetObj.isDestroyed) {\n                targetObj.tntChargeTrait.removeCharge();\n            }\n        }\n        if (warhead.rules.mindControl) {\n            shouldDetonate = false;\n            if (this.fromObject &&\n                !this.fromObject.isDestroyed &&\n                targetObj?.isTechno() &&\n                targetObj.mindControllableTrait &&\n                !targetObj.mindControllableTrait?.isActive() &&\n                !game.areFriendly(targetObj, this.fromObject) &&\n                warhead.canDamage(targetObj, detonationTile, detonationZone) &&\n                !targetObj.invulnerableTrait.isActive()) {\n                this.fromObject.mindControllerTrait.control(targetObj, game);\n            }\n        }\n        if (warhead.rules.temporal) {\n            shouldDetonate = false;\n            if (this.fromObject &&\n                !this.fromObject.isDestroyed &&\n                targetObj?.isTechno() &&\n                warhead.canDamage(targetObj, detonationTile, detonationZone) &&\n                !targetObj.invulnerableTrait.isActive()) {\n                warhead.inflictDamage(0, targetObj, {\n                    player: this.fromPlayer,\n                    weapon: weapon,\n                    obj: this.fromObject,\n                }, game);\n                this.fromObject.temporalTrait.updateTarget(targetObj, weapon, game);\n            }\n        }\n        if (warhead.rules.makesDisguise) {\n            shouldDetonate = false;\n            if (this.fromObject &&\n                !this.fromObject.isDestroyed &&\n                (this.fromObject.isInfantry() || this.fromObject.isVehicle()) &&\n                targetObj?.isUnit() &&\n                targetObj.type === this.fromObject.type) {\n                this.fromObject.disguiseTrait?.disguiseAs(targetObj, this.fromObject, game);\n            }\n        }\n        if (warhead.rules.electricAssault) {\n            if (this.fromObject?.isUnit() &&\n                !this.fromObject.isDestroyed &&\n                targetObj?.isBuilding() &&\n                !targetObj.isDestroyed &&\n                targetObj.overpoweredTrait &&\n                targetObj.owner === this.fromPlayer) {\n                targetObj.overpoweredTrait.chargeFrom(this.fromObject);\n            }\n            shouldDetonate = false;\n        }\n        if (this.isPrismSupportBeam(game)) {\n            shouldDetonate = false;\n        }\n        if (shouldDetonate) {\n            warhead.detonate(game, damage, detonationTile, this.tileElevation, this.position.worldPosition, detonationZone, collisionType, this.target, {\n                player: this.fromPlayer,\n                weapon: weapon,\n                obj: this.fromObject,\n            }, this.isShrapnel, this.impactAnim, undefined);\n        }\n        if (warhead.rules.nukeMaker) {\n            let nukeProjectile: Projectile;\n            if (this.fromObject) {\n                const nukeWeapon = Weapon.factory(Weapon.NUKE_PAYLOAD_NAME, WeaponType.Primary, this.fromObject, game.rules);\n                nukeProjectile = game.createProjectile(nukeWeapon.projectileRules.name, this.fromObject, nukeWeapon, this.target, false);\n            }\n            else {\n                nukeProjectile = game.createLooseProjectile(Weapon.NUKE_PAYLOAD_NAME, this.fromPlayer, this.target);\n            }\n            nukeProjectile.isNuke = true;\n            nukeProjectile.impactAnim = \"NUKEBALL\";\n            const nukeTile = this.target.tile;\n            nukeProjectile.position.moveToTileCoords(nukeTile.rx + 0.5, nukeTile.ry + 0.5);\n            nukeProjectile.position.tileElevation = this.position.tileElevation;\n            game.spawnObject(nukeProjectile, nukeTile);\n        }\n        if (this.rules.shrapnelCount &&\n            this.rules.shrapnelWeapon &&\n            ((this.target.obj\n                ? !this.target.obj.isBuilding()\n                : game.map\n                    .getGroundObjectsOnTile(this.target.tile)\n                    .some((obj: any) => obj.isTerrain()) &&\n                    !weapon.projectileRules.isAntiAir) ||\n                this.isShrapnel)) {\n            const shrapnelWeapon = game.rules.getWeapon(this.rules.shrapnelWeapon);\n            const shrapnelProjectile = game.rules.getProjectile(shrapnelWeapon.projectile);\n            let shrapnelCount = this.rules.shrapnelCount;\n            const rangeHelper = new RangeHelper(game.map.tileOccupation);\n            const tileFinder = new RadialTileFinder(game.map.tiles, game.map.mapBounds, detonationTile, { width: 1, height: 1 }, 1, shrapnelWeapon.range, (tile: any) => rangeHelper.isInTileRange(detonationTile, tile, shrapnelWeapon.minimumRange, shrapnelWeapon.range));\n            const shrapnelTargets = new Set<any>();\n            while (Math.floor(shrapnelCount) > 0) {\n                const tile = tileFinder.getNextTile();\n                if (!tile)\n                    break;\n                const objects = game.map.tileOccupation\n                    .getObjectsOnTileByLayer(tile, shrapnelProjectile.isAntiAir ? LayerType.Air : LayerType.Ground)\n                    .filter((obj: any) => game.isValidTarget(obj) &&\n                    (obj.isTerrain() ||\n                        (obj.isTechno() &&\n                            obj.owner !== this.fromPlayer &&\n                            !game.alliances.areAllied(obj.owner, this.fromPlayer) &&\n                            !(obj.isInfantry() && obj.stance === StanceType.Paradrop))));\n                for (const obj of objects) {\n                    if (!shrapnelTargets.has(obj)) {\n                        shrapnelTargets.add(obj);\n                        shrapnelCount = Math.max(0, shrapnelCount - 1 - (obj.isTechno() ? 0.5 : 0));\n                        if (Math.floor(shrapnelCount) <= 0)\n                            break;\n                    }\n                }\n            }\n            for (const target of shrapnelTargets) {\n                const shrapnelTarget = game.createTarget(target.isTerrain() ? undefined : target, target.tile);\n                this.createShrapnel(game, shrapnelTarget, shrapnelWeapon.name);\n            }\n            shrapnelCount = Math.floor(shrapnelCount);\n            const randomTileFinder = new RandomTileFinder(game.map.tiles, game.map.mapBounds, detonationTile, shrapnelWeapon.range, game, (tile: any) => rangeHelper.isInTileRange(detonationTile, tile, shrapnelWeapon.minimumRange, shrapnelWeapon.range));\n            for (let i = 0; i < shrapnelCount; i++) {\n                const tile = randomTileFinder.getNextTile();\n                if (!tile)\n                    break;\n                const target = game.createTarget(undefined, tile);\n                this.createShrapnel(game, target, shrapnelWeapon.name);\n            }\n        }\n        if (weapon.rules.limboLaunch && !parasiteSuccess && this.fromObject?.isUnit()) {\n            const unit = this.fromObject;\n            if (warhead.rules.parasite &&\n                (this.target.obj.isVehicle() || this.target.obj?.isAircraft()) &&\n                this.target.obj.parasiteableTrait) {\n                this.target.obj.parasiteableTrait.beingBoarded = false;\n            }\n            let returnTile: any;\n            let onBridge: boolean;\n            const isAircraft = unit.rules.movementZone === MovementZone.Fly;\n            if (isAircraft) {\n                returnTile = detonationTile;\n                onBridge = false;\n            }\n            else {\n                const targetBridge = this.target.obj.isUnit() &&\n                    this.target.obj.tile.onBridgeLandType &&\n                    !this.target.obj.onBridge\n                    ? undefined\n                    : game.map.tileOccupation.getBridgeOnTile(detonationTile);\n                const moveHelper = new MovePositionHelper(game.map);\n                const returnTileFinder = new RadialTileFinder(game.map.tiles, game.map.mapBounds, detonationTile, { width: 1, height: 1 }, 0, 1, (tile: any) => {\n                    const tileBridge = game.map.tileOccupation.getBridgeOnTile(tile);\n                    return (game.map.terrain.getPassableSpeed(tile, unit.rules.speedType, unit.isInfantry(), !!tileBridge) > 0 &&\n                        moveHelper.isEligibleTile(tile, tileBridge, targetBridge, detonationTile) &&\n                        (tile === detonationTile ||\n                            !game.map.terrain.findObstacles({ tile: tile, onBridge: targetBridge }, unit).length));\n                });\n                returnTile = returnTileFinder.getNextTile();\n                onBridge = !!returnTile?.onBridgeLandType;\n            }\n            if (returnTile) {\n                if (!isAircraft && this.target.obj.isUnit()) {\n                    unit.onBridge = onBridge;\n                    unit.position.tileElevation = onBridge\n                        ? (game.map.tileOccupation.getBridgeOnTile(returnTile)?.tileElevation ?? 0)\n                        : 0;\n                }\n                game.unlimboObject(unit, returnTile);\n                if (unit.isInfantry()) {\n                    unit.position.subCell = this.target.obj.position.subCell;\n                }\n                unit.direction = this.direction;\n            }\n            else {\n                unit.owner.removeOwnedObject(unit);\n            }\n        }\n    }\n    private createShrapnel(game: any, target: any, weaponName: string): void {\n        const shrapnel = game.createLooseProjectile(weaponName, this.fromPlayer, target);\n        shrapnel.isShrapnel = true;\n        shrapnel.veteranDamageMult = this.veteranDamageMult;\n        shrapnel.position.moveToLeptons(this.position.getMapPosition());\n        shrapnel.position.tileElevation = this.position.tileElevation;\n        game.spawnObject(shrapnel, shrapnel.position.tile);\n    }\n    private computeAimPointVersusMovingTarget(target: any, projectileSpeed: number, projectilePos: Vector3, map: any): Vector3 {\n        const targetPos = target.position.worldPosition;\n        const aimPoint = targetPos.clone();\n        const targetSpeed = target.moveTrait.velocity.length();\n        if (projectileSpeed < 3 * targetSpeed) {\n            return targetPos.clone();\n        }\n        const interceptPoint = TargetUtil.computeInterceptPoint(projectilePos, projectileSpeed, targetPos, target.moveTrait.velocity);\n        if (interceptPoint.length()) {\n            const toIntercept = interceptPoint.clone().sub(targetPos);\n            const distance = toIntercept.length();\n            const travelTime = targetSpeed ? Math.ceil(distance / targetSpeed) : 0;\n            const finalIntercept = targetPos.clone().add(toIntercept.setLength(travelTime * targetSpeed));\n            if (map.isWithinHardBounds(finalIntercept)) {\n                if (target.zone !== ZoneType.Air) {\n                    finalIntercept.multiplyScalar(1 / Coords.LEPTONS_PER_TILE);\n                    const pos = target.position.clone();\n                    pos.moveToTileCoords(finalIntercept.x, finalIntercept.z);\n                    return pos.worldPosition;\n                }\n                else {\n                    return finalIntercept;\n                }\n            }\n            else {\n                return targetPos;\n            }\n        }\n        return aimPoint.clone();\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Smudge.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { GameObject } from '@/game/gameobject/GameObject';\nexport class Smudge extends GameObject {\n    static factory(id: string, rules: any, owner: any): Smudge {\n        return new this(id, rules, owner);\n    }\n    constructor(id: string, rules: any, owner: any) {\n        super(ObjectType.Smudge, id, rules, owner);\n    }\n    getFoundation(): {\n        width: number;\n        height: number;\n    } {\n        return { width: this.rules.width, height: this.rules.height };\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Techno.ts",
    "content": "import { GameObject } from '@/game/gameobject/GameObject';\nimport { TechnoRules } from '@/game/rules/TechnoRules';\nimport { VeteranLevel } from '@/game/gameobject/unit/VeteranLevel';\nimport { NotifyTick } from '@/game/gameobject/trait/interface/NotifyTick';\nexport class Techno extends GameObject {\n    explodes: boolean;\n    radarInvisible: boolean;\n    c4: boolean;\n    crusher: boolean;\n    defaultToGuardArea: boolean;\n    guardMode: boolean;\n    purchaseValue: number;\n    guardArea?: any;\n    [key: string]: any;\n    get primaryWeapon() {\n        return this.armedTrait?.primaryWeapon;\n    }\n    get secondaryWeapon() {\n        return this.armedTrait?.secondaryWeapon;\n    }\n    get ammo() {\n        return this.ammoTrait?.ammo;\n    }\n    get sight() {\n        return Math.min(TechnoRules.MAX_SIGHT, this.rules.sight * (this.veteranTrait?.getVeteranSightMultiplier() ?? 1));\n    }\n    get veteranLevel() {\n        return this.veteranTrait?.veteranLevel ?? VeteranLevel.None;\n    }\n    constructor(id: string, rules: any, owner: any, general: any) {\n        super(id as any, rules, owner, general);\n        this.explodes = this.rules.explodes;\n        this.radarInvisible = this.rules.radarInvisible;\n        this.c4 = this.rules.c4;\n        this.crusher = this.rules.crusher;\n        this.defaultToGuardArea = this.rules.defaultToGuardArea;\n        this.guardMode = this.rules.defaultToGuardArea;\n        this.purchaseValue = this.rules.cost;\n    }\n    resetGuardModeToIdle() {\n        this.guardMode = this.defaultToGuardArea;\n        this.guardArea = undefined;\n    }\n    update(delta: number) {\n        if (this.warpedOutTrait.isActive()) {\n            for (const trait of this.cachedTraits.tick) {\n                if (trait.ticksWhenWarpedOut) {\n                    trait[NotifyTick.onTick](this, delta);\n                }\n            }\n        }\n        else {\n            super.update(delta);\n        }\n    }\n    isTechno(): boolean {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Terrain.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { GameObject } from '@/game/gameobject/GameObject';\nexport class Terrain extends GameObject {\n    radarInvisible: boolean;\n    static factory(id: string, rules: any, owner: any): Terrain {\n        return new this(id, rules, owner);\n    }\n    constructor(id: string, rules: any, owner: any) {\n        super(ObjectType.Terrain, id, rules, owner);\n        this.radarInvisible = this.rules.radarInvisible;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Unit.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { Techno } from '@/game/gameobject/Techno';\nexport class Unit extends Techno {\n    static factory(id: string, rules: any, owner: any, general?: any): Unit {\n        return new this(id, rules, owner, general);\n    }\n    constructor(id: string, rules: any, owner: any, general?: any) {\n        super(ObjectType.Vehicle as any, id, rules, owner);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Vehicle.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { HarvesterTrait } from \"@/game/gameobject/trait/HarvesterTrait\";\nimport { TransportTrait } from \"@/game/gameobject/trait/TransportTrait\";\nimport { MoveTrait } from \"@/game/gameobject/trait/MoveTrait\";\nimport { TurretTrait } from \"@/game/gameobject/trait/TurretTrait\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { DockableTrait } from \"@/game/gameobject/trait/DockableTrait\";\nimport { Techno } from \"@/game/gameobject/Techno\";\nimport { CrewedTrait } from \"@/game/gameobject/trait/CrewedTrait\";\nimport { GunnerTrait } from \"@/game/gameobject/trait/GunnerTrait\";\nimport { ParasiteableTrait } from \"@/game/gameobject/trait/ParasiteableTrait\";\nimport { CrashableTrait } from \"@/game/gameobject/trait/CrashableTrait\";\nimport { SubmergibleTrait } from \"@/game/gameobject/trait/SubmergibleTrait\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\nimport { HoverBobTrait } from \"@/game/gameobject/trait/HoverBobTrait\";\nimport { CrateBonuses } from \"@/game/gameobject/unit/CrateBonuses\";\nimport { TilterTrait } from \"@/game/gameobject/trait/TilterTrait\";\nexport const ROCKING_TICKS = 34;\ninterface RockingState {\n    ticksLeft: number;\n    facing: number;\n    factor: number;\n}\ninterface VehicleRules {\n    underwater: boolean;\n    weight: number;\n    naval: boolean;\n    crashable: boolean;\n    crewed: boolean;\n    harvester: boolean;\n    storage?: any;\n    passengers: boolean;\n    gunner: boolean;\n    turret: boolean;\n    consideredAircraft: boolean;\n    landable: boolean;\n    parasiteable: boolean;\n    locomotor: LocomotorType;\n}\ninterface GameRules {\n    general: {\n        shipSinkingWeight: number;\n    };\n}\ninterface TerrainInfo {\n    isVoxel: boolean;\n}\nexport class Vehicle extends Techno {\n    public direction: number = 0;\n    public spinVelocity: number = 0;\n    public crateBonuses: CrateBonuses = new CrateBonuses();\n    public turretNo: number = 0;\n    public onBridge: boolean = false;\n    public isSinker: boolean = false;\n    public isFiring: boolean = false;\n    public zone: ZoneType;\n    public rocking?: RockingState;\n    public moveTrait!: MoveTrait;\n    public crashableTrait?: CrashableTrait;\n    public crewedTrait?: CrewedTrait;\n    public harvesterTrait?: HarvesterTrait;\n    public transportTrait?: TransportTrait;\n    public gunnerTrait?: GunnerTrait;\n    public turretTrait?: TurretTrait;\n    public parasiteableTrait?: ParasiteableTrait;\n    public submergibleTrait?: SubmergibleTrait;\n    public tilterTrait?: TilterTrait;\n    get isMoving(): boolean {\n        return this.moveTrait.isMoving();\n    }\n    static factory(owner: any, rules: VehicleRules, terrain: TerrainInfo, gameRules: GameRules, moveRules: any): Vehicle {\n        const vehicle = new this(owner, rules, terrain);\n        vehicle.isSinker = !rules.underwater &&\n            (rules.weight >= gameRules.general.shipSinkingWeight || !rules.naval);\n        vehicle.moveTrait = new MoveTrait(vehicle as any, moveRules);\n        vehicle.traits.add(vehicle.moveTrait);\n        if (rules.crashable) {\n            vehicle.crashableTrait = new CrashableTrait(vehicle);\n            vehicle.traits.add(vehicle.crashableTrait);\n        }\n        if (rules.crewed) {\n            vehicle.crewedTrait = new CrewedTrait();\n            vehicle.traits.add(vehicle.crewedTrait);\n        }\n        if (rules.harvester) {\n            vehicle.harvesterTrait = new HarvesterTrait(rules.storage);\n            vehicle.traits.add(vehicle.harvesterTrait);\n        }\n        if (rules.passengers) {\n            vehicle.transportTrait = new TransportTrait(vehicle);\n            vehicle.traits.add(vehicle.transportTrait);\n            if (rules.gunner) {\n                vehicle.gunnerTrait = new GunnerTrait();\n                vehicle.traits.add(vehicle.gunnerTrait);\n            }\n        }\n        if (rules.turret) {\n            vehicle.turretTrait = new TurretTrait();\n            vehicle.traits.add(vehicle.turretTrait);\n        }\n        if (!(rules.consideredAircraft && !rules.landable)) {\n            vehicle.traits.add(new DockableTrait());\n        }\n        if (rules.parasiteable) {\n            vehicle.parasiteableTrait = new ParasiteableTrait(vehicle);\n            vehicle.traits.add(vehicle.parasiteableTrait);\n        }\n        if (rules.naval && rules.underwater) {\n            vehicle.submergibleTrait = new SubmergibleTrait();\n            vehicle.traits.add(vehicle.submergibleTrait);\n        }\n        if (rules.locomotor === LocomotorType.Hover) {\n            vehicle.traits.add(new HoverBobTrait());\n        }\n        if ([LocomotorType.Vehicle, LocomotorType.Chrono].includes(rules.locomotor) &&\n            terrain.isVoxel) {\n            vehicle.tilterTrait = new TilterTrait();\n            vehicle.traits.add(vehicle.tilterTrait);\n        }\n        return vehicle;\n    }\n    constructor(owner: any, rules: VehicleRules, terrain: TerrainInfo) {\n        super(ObjectType.Vehicle as any, owner, rules, terrain);\n        this.zone = rules.naval ? ZoneType.Water : ZoneType.Ground;\n    }\n    isUnit(): boolean {\n        return true;\n    }\n    isVehicle(): boolean {\n        return true;\n    }\n    getUiName(): string {\n        if (this.gunnerTrait) {\n            const specialWeaponIndex = this.armedTrait.getSpecialWeaponIndex();\n            const ifvModeName = this.gunnerTrait.getUiNameForIfvMode(specialWeaponIndex, this.transportTrait?.units[0]?.name);\n            const baseName = \"name:\" + this.name;\n            return ifvModeName ? `{${ifvModeName}} {${baseName}}` : baseName;\n        }\n        return super.getUiName();\n    }\n    update(deltaTime: number): void {\n        if (this.rocking) {\n            this.rocking.ticksLeft--;\n            if (!this.rocking.ticksLeft) {\n                this.rocking = undefined;\n            }\n        }\n        super.update(deltaTime);\n    }\n    applyRocking(facing: number, factor: number): void {\n        if (!this.rules.consideredAircraft) {\n            this.rocking = {\n                ticksLeft: this.rocking?.ticksLeft ?? ROCKING_TICKS,\n                facing: facing,\n                factor: factor\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/Weapon.ts",
    "content": "export { Weapon } from '@/game/Weapon';\n"
  },
  {
    "path": "src/game/gameobject/common/AnimTerrainEffect.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { TileCollection, TileDirection } from \"@/game/map/TileCollection\";\nimport { LandType } from \"@/game/type/LandType\";\nimport { TiberiumTrait } from \"@/game/gameobject/trait/TiberiumTrait\";\nexport class AnimTerrainEffect {\n    destroyOre(animationId: string, tile: any, game: any): void {\n        if (tile.landType === LandType.Tiberium &&\n            (game.art.hasObject(animationId, ObjectType.Animation)\n                ? game.art.getAnimation(animationId)\n                : undefined)?.crater) {\n            const tiberiumObject = game.map\n                .getObjectsOnTile(tile)\n                .find((obj: any) => obj.isOverlay() && obj.isTiberium());\n            if (tiberiumObject) {\n                let bailCount = Math.ceil(TiberiumTrait.maxBails / 2);\n                bailCount = animationId.startsWith(\"S_CLSN\")\n                    ? bailCount\n                    : game.generateRandomInt(1, bailCount);\n                const tiberiumTrait = tiberiumObject.traits.get(TiberiumTrait);\n                tiberiumTrait.removeBails(bailCount);\n                if (!tiberiumTrait.getBailCount()) {\n                    game.unspawnObject(tiberiumObject);\n                }\n            }\n        }\n    }\n    spawnSmudges(animationId: string, tile: any, game: any): void {\n        if (tile.landType === LandType.Clear &&\n            tile.rampType === 0 &&\n            game.map.mapBounds.isWithinBounds(tile) &&\n            !game.map.getObjectsOnTile(tile).find((obj: any) => !obj.isUnit())) {\n            const animation = game.art.hasObject(animationId, ObjectType.Animation)\n                ? game.art.getAnimation(animationId)\n                : undefined;\n            if (animation?.crater) {\n                const craterSize = animation?.forceBigCraters ? 2 : 1;\n                const isScorch = animation?.scorch;\n                const hasNeighbors = [\n                    TileDirection.Bottom,\n                    TileDirection.BottomLeft,\n                    TileDirection.BottomRight,\n                ].every((dir) => game.map.tiles.getNeighbourTile(tile, dir));\n                const validSmudges = [...game.rules.smudgeRules.values()].filter((rule) => ((rule.crater && rule.width === craterSize && rule.height === craterSize) ||\n                    (isScorch && rule.burn)) &&\n                    !((rule.width > 1 || rule.height > 1) && !hasNeighbors));\n                if (validSmudges.length) {\n                    const selectedSmudge = validSmudges[game.generateRandomInt(0, validSmudges.length - 1)].name;\n                    const smudgeObject = game.createObject(ObjectType.Smudge, selectedSmudge);\n                    game.spawnObject(smudgeObject, tile);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/common/DeathType.ts",
    "content": "export enum DeathType {\n    None = 0,\n    Normal = 1,\n    Demolish = 2,\n    Crush = 3,\n    Temporal = 4,\n    Sink = 5\n}\n"
  },
  {
    "path": "src/game/gameobject/infantry/InfDeathType.ts",
    "content": "export enum InfDeathType {\n    None = 0,\n    Gunfire = 1,\n    Explode = 2,\n    ExplodeAlt = 3,\n    Fire = 4,\n    Electro = 5,\n    HeadExplode = 6,\n    Nuke = 7\n}\n"
  },
  {
    "path": "src/game/gameobject/infantry/StanceType.ts",
    "content": "export enum StanceType {\n    None = 0,\n    Guard = 1,\n    Prone = 2,\n    Deployed = 3,\n    Paradrop = 4,\n    Cheer = 5\n}\n"
  },
  {
    "path": "src/game/gameobject/infantry/sequenceMap.ts",
    "content": "import { ZoneType } from '@/game/gameobject/unit/ZoneType';\nimport { SequenceType } from '../../art/SequenceType';\nimport { StanceType } from './StanceType';\nimport { InfDeathType } from './InfDeathType';\nexport const getFireSequenceBy = (zone: ZoneType, stance: StanceType = StanceType.None): SequenceType => {\n    if (stance === StanceType.Deployed) {\n        return SequenceType.DeployedFire;\n    }\n    if (zone === ZoneType.Water) {\n        return SequenceType.WetAttack;\n    }\n    if (zone === ZoneType.Air) {\n        return SequenceType.FireFly;\n    }\n    if (stance === StanceType.Prone) {\n        return SequenceType.FireProne;\n    }\n    return SequenceType.FireUp;\n};\nexport const getMoveSequenceBy = (zone: ZoneType, stance: StanceType, isPanic: boolean): SequenceType => {\n    if (zone === ZoneType.Air) {\n        return SequenceType.Fly;\n    }\n    if (zone === ZoneType.Water) {\n        return SequenceType.Swim;\n    }\n    if (stance === StanceType.Prone) {\n        return SequenceType.Crawl;\n    }\n    return isPanic ? SequenceType.Panic : SequenceType.Walk;\n};\nexport const getIdleSequenceBy = (zone: ZoneType, stance: StanceType = StanceType.None): SequenceType[] | undefined => {\n    if (stance === StanceType.Deployed) {\n        return [SequenceType.DeployedIdle];\n    }\n    if (zone === ZoneType.Water) {\n        return [SequenceType.WetIdle1, SequenceType.WetIdle2];\n    }\n    if (zone !== ZoneType.Air) {\n        return [SequenceType.Idle1, SequenceType.Idle2];\n    }\n    return undefined;\n};\nexport const getStillSequenceBy = (zone: ZoneType, stance: StanceType = StanceType.None): SequenceType => {\n    if (stance === StanceType.Deployed) {\n        return SequenceType.Deployed;\n    }\n    if (zone === ZoneType.Water) {\n        return SequenceType.Tread;\n    }\n    if (zone === ZoneType.Air) {\n        return SequenceType.Hover;\n    }\n    if (stance === StanceType.Prone) {\n        return SequenceType.Prone;\n    }\n    if (stance === StanceType.Guard) {\n        return SequenceType.Guard;\n    }\n    if (stance === StanceType.Paradrop) {\n        return SequenceType.Paradrop;\n    }\n    return SequenceType.Ready;\n};\nexport const getStanceTransitionSequenceBy = (fromStance: StanceType, toStance: StanceType): SequenceType | undefined => {\n    if (fromStance === StanceType.Prone) {\n        return SequenceType.Up;\n    }\n    if (toStance === StanceType.Prone) {\n        return SequenceType.Down;\n    }\n    if (fromStance === StanceType.Deployed) {\n        return SequenceType.Undeploy;\n    }\n    if (toStance === StanceType.Deployed) {\n        return SequenceType.Deploy;\n    }\n    if (toStance === StanceType.Cheer) {\n        return SequenceType.Cheer;\n    }\n    return undefined;\n};\nexport const getCrashingSequences = (unit: {\n    art: {\n        sequences: Map<SequenceType, any>;\n    };\n}): SequenceType[] | undefined => {\n    const availableSequences = [...unit.art.sequences.keys()];\n    const sequences = [\n        SequenceType.AirDeathStart,\n        SequenceType.AirDeathFalling,\n    ].filter(seq => availableSequences.includes(seq));\n    return sequences.length ? sequences : undefined;\n};\nexport const getDeathSequence = (unit: {\n    zone: ZoneType;\n    rules: {\n        isHuman: boolean;\n    };\n    art: {\n        sequences: Map<SequenceType, any>;\n    };\n    isCrashing: boolean;\n}, deathType: InfDeathType): SequenceType[] | undefined => {\n    const zone = unit.zone;\n    const isHuman = unit.rules.isHuman;\n    const availableSequences = [...unit.art.sequences.keys()];\n    let sequences: SequenceType[] | undefined;\n    if (unit.isCrashing) {\n        sequences = [SequenceType.AirDeathFinish];\n    }\n    else if (zone === ZoneType.Air) {\n        sequences = [SequenceType.Tumble];\n    }\n    else if (zone === ZoneType.Water) {\n        if (![InfDeathType.Gunfire, InfDeathType.Explode].includes(deathType) && isHuman) {\n            sequences = [SequenceType.WetDie1, SequenceType.WetDie2];\n        }\n    }\n    else if (deathType !== InfDeathType.Gunfire && isHuman) {\n        if (deathType === InfDeathType.Explode) {\n            sequences = [SequenceType.Die2];\n        }\n    }\n    else {\n        sequences = [SequenceType.Die1];\n    }\n    if (sequences) {\n        sequences = sequences.filter(seq => availableSequences.includes(seq));\n        if (!sequences.length) {\n            sequences = undefined;\n        }\n    }\n    return sequences;\n};\nexport const getDeathAnim = (unit: {\n    audioVisual: {\n        infantryExplode: any;\n        flamingInfantry: any;\n        infantryHeadPop: any;\n        infantryNuked: any;\n    };\n    animationNames: string[];\n}, deathType: InfDeathType): any => {\n    switch (deathType) {\n        case InfDeathType.ExplodeAlt:\n            return unit.audioVisual.infantryExplode;\n        case InfDeathType.Fire:\n            return unit.audioVisual.flamingInfantry;\n        case InfDeathType.Electro:\n            return [...unit.animationNames][1];\n        case InfDeathType.HeadExplode:\n            return unit.audioVisual.infantryHeadPop;\n        case InfDeathType.Nuke:\n            return unit.audioVisual.infantryNuked;\n        default:\n            return undefined;\n    }\n};\nexport const findSequence = (zone: ZoneType, stance: StanceType, isMoving: boolean, isFiring: boolean, isPanic: boolean, availableSequences: SequenceType[]): SequenceType | undefined => {\n    const isAvailable = (seq: SequenceType) => availableSequences.indexOf(seq) !== -1;\n    let sequence: SequenceType | undefined;\n    if (isFiring) {\n        sequence = getFireSequenceBy(zone, stance);\n        if (!isAvailable(sequence)) {\n            sequence = getFireSequenceBy(zone);\n            if (!isAvailable(sequence)) {\n                sequence = undefined;\n            }\n        }\n    }\n    if (sequence === undefined && isMoving) {\n        sequence = getMoveSequenceBy(zone, stance, isPanic);\n        if (!isAvailable(sequence)) {\n            sequence = getMoveSequenceBy(zone, StanceType.None, isPanic);\n            if (!isAvailable(sequence)) {\n                sequence = undefined;\n            }\n        }\n    }\n    if (sequence === undefined) {\n        sequence = getStillSequenceBy(zone, stance);\n        if (!isAvailable(sequence)) {\n            sequence = getStillSequenceBy(zone);\n            if (!isAvailable(sequence)) {\n                sequence = getStillSequenceBy(ZoneType.Ground);\n            }\n        }\n    }\n    return sequence;\n};\n"
  },
  {
    "path": "src/game/gameobject/locomotor/ChronoLocomotor.ts",
    "content": "import { Vector2 } from '@/game/math/Vector2';\nimport { Vector3 } from '@/game/math/Vector3';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { Game } from '@/game/Game';\nexport class ChronoLocomotor {\n    private game: Game;\n    private ignoresTerrain: boolean;\n    private distanceToWaypoint: Vector2;\n    constructor(game: Game) {\n        this.game = game;\n        this.ignoresTerrain = true;\n        this.distanceToWaypoint = new Vector2();\n    }\n    onNewWaypoint(unit: GameObject, waypoint: Vector2): void { }\n    tick(unit: GameObject, waypoint: Vector2, speed: number, isMoving: boolean): {\n        distance: Vector3;\n        done: boolean;\n        isTeleport?: boolean;\n    } {\n        if (isMoving) {\n            return { distance: new Vector3(), done: true };\n        }\n        this.distanceToWaypoint\n            .copy(waypoint)\n            .sub(unit.position.getMapPosition());\n        const distance = this.distanceToWaypoint.length();\n        const generalRules = this.game.rules.general;\n        if (generalRules.chronoTrigger) {\n            const delay = distance < generalRules.chronoRangeMinimum\n                ? generalRules.chronoMinimumDelay\n                : distance / generalRules.chronoDistanceFactor;\n            unit.warpedOutTrait.setTimed(delay, false, this.game);\n        }\n        return {\n            distance: new Vector3(this.distanceToWaypoint.x, 0, this.distanceToWaypoint.y),\n            done: true,\n            isTeleport: true\n        };\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/locomotor/DriveLocomotor.ts",
    "content": "import { FacingUtil } from \"@/game/gameobject/unit/FacingUtil\";\nimport { TurnTask } from \"@/game/gameobject/task/TurnTask\";\nimport { Coords } from \"@/game/Coords\";\nimport * as geometry from \"@/game/math/geometry\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { Vector3 } from \"@/game/math/Vector3\";\nimport { CurvePath } from \"@/game/math/CurvePath\";\nimport { LineCurve } from \"@/game/math/LineCurve\";\nimport { QuadraticBezierCurve } from \"@/game/math/QuadraticBezierCurve\";\nimport * as math from \"@/util/math\";\nenum WaypointType {\n    None = 0,\n    Start = 1,\n    Normal = 2,\n    End = 3,\n    Single = 4\n}\nexport class DriveLocomotor {\n    private game: any;\n    private hasMomentum: boolean = false;\n    private moveOnCurve: boolean = false;\n    private currentSpeed: number = 0;\n    private distanceTravelled: number = 0;\n    private carryOverDistance: number = 0;\n    private currentWaypointType: WaypointType = WaypointType.None;\n    private initialPosition: Vector2;\n    private steerCurve: CurvePath;\n    private lastPosition: Vector2;\n    private totalDistanceToTravel: number;\n    constructor(game: any) {\n        this.game = game;\n    }\n    selectNextWaypoint(unit: any, waypoints: any[]): any {\n        this.currentWaypointType = this.currentWaypointType && this.currentWaypointType !== WaypointType.End\n            ? WaypointType.Normal\n            : WaypointType.Start;\n        this.initialPosition = unit.position.getMapPosition();\n        if (this.currentWaypointType !== WaypointType.Start) {\n            unit.moveTrait.speedPenalty = 0;\n        }\n        else {\n            this.currentSpeed = 0;\n        }\n        if (waypoints.length > 1) {\n            const lastWaypoint = waypoints[waypoints.length - 1];\n            const secondLastWaypoint = waypoints[waypoints.length - 2];\n            const directionToLast = new Vector2(lastWaypoint.tile.rx - unit.tile.rx, lastWaypoint.tile.ry - unit.tile.ry);\n            const angleDifference = Math.abs(geometry.angleDegFromVec2(directionToLast) -\n                geometry.angleDegFromVec2(new Vector2(secondLastWaypoint.tile.rx - lastWaypoint.tile.rx, secondLastWaypoint.tile.ry - lastWaypoint.tile.ry)));\n            if (!Math.abs(FacingUtil.fromMapCoords(directionToLast) - unit.direction) &&\n                angleDifference > 0 &&\n                angleDifference < 90 &&\n                this.hasMomentum) {\n                this.moveOnCurve = true;\n                this.currentWaypointType = waypoints.length === 2\n                    ? (this.currentWaypointType === WaypointType.Start ? WaypointType.Single : WaypointType.End)\n                    : WaypointType.Normal;\n                const startPos = this.initialPosition;\n                const midPos = new Vector2(lastWaypoint.tile.rx + 0.5, lastWaypoint.tile.ry + 0.5).multiplyScalar(Coords.LEPTONS_PER_TILE);\n                const endPos = new Vector2(secondLastWaypoint.tile.rx + 0.5, secondLastWaypoint.tile.ry + 0.5).multiplyScalar(Coords.LEPTONS_PER_TILE);\n                const controlPoint1 = startPos.clone().lerp(midPos, 0.5);\n                const controlPoint2 = endPos.clone().lerp(midPos, 0.5);\n                this.steerCurve = new CurvePath();\n                this.steerCurve.add(new LineCurve(startPos, controlPoint1) as any);\n                this.steerCurve.add(new QuadraticBezierCurve(controlPoint1, midPos, controlPoint2) as any);\n                this.steerCurve.add(new LineCurve(controlPoint2, endPos) as any);\n                this.lastPosition = startPos;\n                return secondLastWaypoint;\n            }\n        }\n        else {\n            this.currentWaypointType = this.currentWaypointType === WaypointType.Start\n                ? WaypointType.Single\n                : WaypointType.End;\n        }\n        this.hasMomentum = true;\n        this.moveOnCurve = false;\n        return waypoints[waypoints.length - 1];\n    }\n    onNewWaypoint(unit: any, targetPosition: Vector2, target: any): any[] | undefined {\n        const direction = new Vector2().copy(targetPosition).sub(this.initialPosition);\n        this.distanceTravelled = 0;\n        this.totalDistanceToTravel = this.moveOnCurve\n            ? this.steerCurve.getLength()\n            : direction.length();\n        const facing = FacingUtil.fromMapCoords(direction);\n        if (facing !== unit.direction) {\n            this.pointTurretToTarget(unit, target);\n            if (!this.moveOnCurve) {\n                unit.moveTrait.velocity.set(0, 0, 0);\n                return [new TurnTask(facing)];\n            }\n        }\n    }\n    tick(unit: any, targetPosition: Vector2, target: any): {\n        distance: Vector3;\n        done: boolean;\n    } {\n        this.pointTurretToTarget(unit, target);\n        let speed = this.currentSpeed;\n        if (unit.rules.accelerates) {\n            const progress = this.distanceTravelled / this.totalDistanceToTravel;\n            this.currentSpeed = this.applyAcceleration(unit, speed, unit.moveTrait.baseSpeed, progress);\n            speed = this.currentSpeed;\n        }\n        else {\n            this.currentSpeed = unit.moveTrait.baseSpeed;\n            speed = this.currentSpeed;\n        }\n        if (speed > 1) {\n            speed = Math.floor(speed);\n        }\n        let terrainSpeed = this.game.map.terrain.getPassableSpeed(unit.tile, unit.rules.speedType, unit.isInfantry(), unit.onBridge, undefined, true);\n        if (terrainSpeed) {\n            unit.moveTrait.lastTileSpeed = terrainSpeed;\n        }\n        else {\n            terrainSpeed = unit.moveTrait.lastTileSpeed || 1;\n        }\n        speed *= terrainSpeed;\n        if (speed > 1) {\n            speed = Math.floor(speed);\n        }\n        if (this.carryOverDistance) {\n            speed = this.carryOverDistance;\n        }\n        const currentPosition = unit.position.getMapPosition();\n        let movementDelta: Vector2;\n        if (this.moveOnCurve) {\n            const curveLength = this.steerCurve.getLength();\n            const newDistance = Math.min(this.distanceTravelled + speed, curveLength);\n            this.carryOverDistance = Math.max(0, this.distanceTravelled + speed - curveLength);\n            this.distanceTravelled = newDistance;\n            const curvePoint = this.steerCurve.getPointAt(this.distanceTravelled / curveLength);\n            const curveTangent = this.steerCurve.getTangentAt(this.distanceTravelled / curveLength);\n            const velocityVector = curveTangent.clone().setLength(speed);\n            unit.moveTrait.velocity.set(velocityVector.x, 0, velocityVector.y);\n            const rotationSpeed = unit.rules.rot;\n            const { facing, delta } = FacingUtil.tick(unit.direction, FacingUtil.fromMapCoords(curveTangent as any), rotationSpeed);\n            unit.direction = facing;\n            unit.spinVelocity = delta;\n            const previousPosition = this.lastPosition;\n            this.lastPosition = curvePoint.clone() as any;\n            movementDelta = curvePoint.sub(previousPosition as any) as any;\n        }\n        else {\n            const directionToTarget = new Vector2().copy(targetPosition).sub(currentPosition);\n            const actualDistance = Math.min(directionToTarget.length(), speed);\n            movementDelta = directionToTarget.clone().setLength(actualDistance);\n            const velocityVector = movementDelta.clone();\n            if (this.carryOverDistance) {\n                velocityVector.add(Coords.vecWorldToGround(unit.moveTrait.velocity));\n            }\n            unit.moveTrait.velocity.set(velocityVector.x, 0, velocityVector.y);\n            this.distanceTravelled += actualDistance;\n            this.carryOverDistance = Math.max(0, speed - directionToTarget.length());\n        }\n        return {\n            distance: new Vector3(movementDelta.x, 0, movementDelta.y),\n            done: !movementDelta.length() || !!this.carryOverDistance\n        };\n    }\n    private pointTurretToTarget(unit: any, target: any): void {\n        if (unit.turretTrait) {\n            let targetPosition = target;\n            if (unit.attackTrait?.currentTarget?.obj) {\n                targetPosition = unit.attackTrait.currentTarget.obj.position.getMapPosition();\n            }\n            const unitPosition = unit.position.getMapPosition();\n            const directionToTarget = new Vector2().copy(targetPosition).sub(unitPosition);\n            if (directionToTarget.length()) {\n                const facing = FacingUtil.fromMapCoords(directionToTarget);\n                unit.turretTrait.desiredFacing = facing;\n            }\n        }\n    }\n    private applyAcceleration(unit: any, currentSpeed: number, baseSpeed: number, progress: number): number {\n        if (this.currentWaypointType === WaypointType.Single) {\n            return baseSpeed / 2;\n        }\n        if (this.currentWaypointType !== WaypointType.End) {\n            return Math.min(currentSpeed + unit.rules.accelerationFactor * baseSpeed, baseSpeed);\n        }\n        let adjustedProgress = progress;\n        if (this.moveOnCurve && this.currentWaypointType === WaypointType.End) {\n            adjustedProgress = progress <= 0.5 ? 0 : 2 * (progress - 0.5);\n        }\n        return math.lerp(1, baseSpeed, 1 - adjustedProgress);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/locomotor/FootLocomotor.ts",
    "content": "import { FacingUtil } from '@/game/gameobject/unit/FacingUtil';\nimport { StanceType } from '@/game/gameobject/infantry/StanceType';\nimport { Vector2 } from '@/game/math/Vector2';\nimport { Vector3 } from '@/game/math/Vector3';\nimport { Game } from '@/game/Game';\nimport { GameObject } from '@/game/gameobject/GameObject';\nexport class FootLocomotor {\n    private game: Game;\n    private currentMoveDirection: Vector2;\n    private distanceToWaypoint: Vector2;\n    private endPauseFrames: number;\n    constructor(game: Game) {\n        this.game = game;\n        this.currentMoveDirection = new Vector2();\n        this.distanceToWaypoint = new Vector2();\n        this.endPauseFrames = 0;\n    }\n    onNewWaypoint(obj: GameObject, target: Vector2): void {\n        this.currentMoveDirection\n            .copy(target)\n            .sub(obj.position.getMapPosition());\n        const facing = FacingUtil.fromMapCoords(this.currentMoveDirection);\n        if (facing !== obj.direction) {\n            obj.direction = facing;\n        }\n        this.endPauseFrames = 1;\n    }\n    onWaypointUpdate(obj: GameObject, target: Vector2): void {\n        this.onNewWaypoint(obj, target);\n    }\n    tick(obj: GameObject, target: Vector2, currentPos: Vector2): {\n        distance: Vector3;\n        done: boolean;\n    } {\n        let speed = obj.moveTrait.baseSpeed;\n        speed = Math.floor(speed);\n        if (obj.stance === StanceType.Prone) {\n            speed *= obj.art.crawls ? 0.5 : 2;\n        }\n        if (obj.isPanicked) {\n            speed *= 2;\n        }\n        let tileSpeed = this.game.map.terrain.getPassableSpeed(obj.tile, obj.rules.speedType, obj.isInfantry(), obj.onBridge, undefined, true);\n        if (tileSpeed) {\n            obj.moveTrait.lastTileSpeed = tileSpeed;\n        }\n        else {\n            tileSpeed = obj.moveTrait.lastTileSpeed || 1;\n        }\n        speed *= tileSpeed;\n        speed = Math.floor(speed);\n        this.distanceToWaypoint\n            .copy(target)\n            .sub(obj.position.getMapPosition());\n        const moveVector = this.distanceToWaypoint.clone().setLength(speed);\n        if (moveVector.length() || target.equals(currentPos)) {\n            obj.moveTrait.velocity.set(moveVector.x, 0, moveVector.y);\n        }\n        const distance = Math.min(this.distanceToWaypoint.length(), speed);\n        const isPaused = !distance && this.endPauseFrames > 0;\n        if (isPaused) {\n            this.endPauseFrames--;\n        }\n        this.distanceToWaypoint.setLength(distance);\n        return {\n            distance: new Vector3(this.distanceToWaypoint.x, 0, this.distanceToWaypoint.y),\n            done: !this.distanceToWaypoint.length() && !isPaused\n        };\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/locomotor/HoverLocomotor.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { FacingUtil } from \"@/game/gameobject/unit/FacingUtil\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nimport { angleDegBetweenVec2 } from \"@/game/math/geometry\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { Vector3 } from \"@/game/math/Vector3\";\nenum WaypointType {\n    None = 0,\n    Start = 1,\n    Normal = 2,\n    End = 3,\n    Single = 4\n}\ninterface HoverRules {\n    acceleration: number;\n    brake: number;\n}\nexport class HoverLocomotor {\n    private hoverRules: HoverRules;\n    private currentSpeed: number;\n    private distanceTravelled: number;\n    private carryOverDistance: number;\n    private currentWaypointType: WaypointType;\n    private nextWaypointDir: Vector2;\n    private initialPosition?: Vector2;\n    private totalDistanceToTravel: number;\n    private maxSpeed: number;\n    private acceleration: number;\n    private deceleration: number;\n    constructor(hoverRules: HoverRules) {\n        this.hoverRules = hoverRules;\n        this.currentSpeed = 0;\n        this.distanceTravelled = 0;\n        this.carryOverDistance = 0;\n        this.currentWaypointType = WaypointType.None;\n        this.nextWaypointDir = new Vector2();\n    }\n    selectNextWaypoint(unit: any, waypoints: any[]): any {\n        this.currentWaypointType =\n            this.currentWaypointType && this.currentWaypointType !== WaypointType.End\n                ? WaypointType.Normal\n                : WaypointType.Start;\n        this.initialPosition = unit.position.getMapPosition();\n        if (this.currentWaypointType === WaypointType.Start) {\n            this.currentSpeed = 0;\n        }\n        if (waypoints.length <= 1) {\n            this.currentWaypointType =\n                this.currentWaypointType === WaypointType.Start\n                    ? WaypointType.Single\n                    : WaypointType.End;\n            const lastWaypoint = waypoints[waypoints.length - 1];\n            if (lastWaypoint) {\n                this.nextWaypointDir.set(lastWaypoint.tile.rx - unit.tile.rx, lastWaypoint.tile.ry - unit.tile.ry);\n            }\n        }\n        else {\n            const lastWaypoint = waypoints[waypoints.length - 1];\n            const secondLastWaypoint = waypoints[waypoints.length - 2];\n            this.nextWaypointDir.set(secondLastWaypoint.tile.rx - lastWaypoint.tile.rx, secondLastWaypoint.tile.ry - lastWaypoint.tile.ry);\n        }\n        return waypoints[waypoints.length - 1];\n    }\n    onNewWaypoint(unit: any, target: Vector2, waypoints: any[]): void {\n        const direction = new Vector2().copy(target).sub(this.initialPosition!);\n        this.distanceTravelled = 0;\n        this.totalDistanceToTravel = direction.length();\n        const maxSpeed = this.maxSpeed = unit.moveTrait.baseSpeed;\n        const accelerationTime = 60 * this.hoverRules.acceleration * GameSpeed.BASE_TICKS_PER_SECOND;\n        this.acceleration = maxSpeed / accelerationTime;\n        const brakeTime = 60 * this.hoverRules.brake * GameSpeed.BASE_TICKS_PER_SECOND;\n        this.deceleration = maxSpeed / brakeTime;\n    }\n    tick(unit: any, target: Vector2): {\n        distance: Vector3;\n        done: boolean;\n    } {\n        const currentPos = unit.position.getMapPosition();\n        const direction = target.clone().sub(currentPos);\n        const distance = direction.length();\n        const maxSpeed = this.maxSpeed;\n        if (this.currentWaypointType === WaypointType.Single) {\n            this.currentSpeed = maxSpeed / 2;\n        }\n        else if (this.currentWaypointType === WaypointType.End) {\n            const brakeDistance = this.computeBrakeDistance(this.currentSpeed, this.deceleration);\n            if (this.totalDistanceToTravel - this.distanceTravelled <= brakeDistance) {\n                this.currentSpeed = Math.max(0, this.currentSpeed - this.deceleration);\n            }\n        }\n        else {\n            this.currentSpeed = Math.min(this.currentSpeed + this.acceleration, maxSpeed);\n        }\n        const currentFacing = FacingUtil.fromMapCoords(direction);\n        const targetFacing = FacingUtil.fromMapCoords(this.nextWaypointDir);\n        let desiredFacing = currentFacing;\n        const rotationSpeed = unit.rules.rot;\n        if (this.currentWaypointType === WaypointType.Normal && currentFacing !== targetFacing) {\n            const angleDiff = angleDegBetweenVec2(this.nextWaypointDir, FacingUtil.toMapCoords(unit.direction));\n            const turnTime = angleDiff / rotationSpeed;\n            const turnDistance = Math.max(this.currentSpeed * turnTime, this.totalDistanceToTravel);\n            if (this.totalDistanceToTravel - this.distanceTravelled <= turnDistance) {\n                desiredFacing = targetFacing;\n                const remainingTime = angleDiff /\n                    ((this.totalDistanceToTravel - this.distanceTravelled) / this.currentSpeed);\n            }\n        }\n        const newFacing = FacingUtil.tick(unit.direction, desiredFacing, rotationSpeed).facing;\n        unit.direction = newFacing;\n        let moveDistance = this.currentSpeed;\n        if (this.carryOverDistance) {\n            moveDistance = this.carryOverDistance;\n        }\n        const actualDistance = Math.min(moveDistance, distance);\n        const moveVector = direction.clone().setLength(actualDistance);\n        const finalMoveVector = moveVector.clone();\n        if (this.carryOverDistance) {\n            finalMoveVector.add(Coords.vecWorldToGround(unit.moveTrait.velocity));\n        }\n        unit.moveTrait.velocity.set(finalMoveVector.x, 0, finalMoveVector.y);\n        this.distanceTravelled += actualDistance;\n        this.carryOverDistance = Math.max(0, moveDistance - distance);\n        return {\n            distance: new Vector3(moveVector.x, 0, moveVector.y),\n            done: !moveVector.length() || !!this.carryOverDistance\n        };\n    }\n    private computeBrakeDistance(speed: number, deceleration: number): number {\n        const time = speed / deceleration;\n        return Math.max(0, speed * time - (deceleration * time * time) / 2);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/locomotor/JumpjetLocomotor.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { FacingUtil } from \"@/game/gameobject/unit/FacingUtil\";\nimport { TargetUtil } from \"@/game/gameobject/unit/TargetUtil\";\nimport * as geometry from \"@/util/geometry\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { ObjectLiftOffEvent } from \"@/game/event/ObjectLiftOffEvent\";\nimport { ObjectLandEvent } from \"@/game/event/ObjectLandEvent\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { Vector3 } from \"@/game/math/Vector3\";\ninterface GameObject {\n    zone: ZoneType;\n    tile: Tile;\n    rules: GameObjectRules;\n    unitOrderTrait: UnitOrderTrait;\n    position: Position;\n    tileElevation: number;\n    moveTrait: MoveTrait;\n    onBridge: boolean;\n    direction: number;\n    isVehicle(): boolean;\n    isBuilding(): boolean;\n    isInfantry(): boolean;\n    isDestroyed: boolean;\n    isOverlay(): boolean;\n    spinVelocity?: number;\n    stance?: StanceType;\n    dockTrait?: DockTrait;\n    art: Art;\n}\ninterface Tile {\n    onBridgeLandType?: boolean;\n    z: number;\n    rx: number;\n    ry: number;\n}\ninterface GameObjectRules {\n    balloonHover: boolean;\n    hoverAttack: boolean;\n    jumpjetHeight: number;\n    jumpjetClimb: number;\n    jumpjetCrash: number;\n    jumpjetSpeed: number;\n    jumpjetTurnRate: number;\n    crate?: boolean;\n}\ninterface UnitOrderTrait {\n    getCurrentTask(): Task | undefined;\n}\ninterface Task {\n    preventLanding: boolean;\n}\ninterface Position {\n    worldPosition: Vector3;\n    getMapPosition(): Vector2;\n    moveByLeptons3(vector: Vector3): void;\n}\ninterface MoveTrait {\n    handleElevationChange(elevation: number, game: Game): void;\n    velocity: Vector3;\n}\ninterface DockTrait {\n    isDocked(object: GameObject): boolean;\n}\ninterface Art {\n    height: number;\n}\ninterface Bridge {\n    tileElevation: number;\n}\ninterface Game {\n    map: GameMap;\n    events: EventDispatcher;\n    crateGeneratorTrait: CrateGeneratorTrait;\n}\ninterface GameMap {\n    tileOccupation: TileOccupation;\n    terrain: Terrain;\n    tiles: TileCollection;\n    getGroundObjectsOnTile(tile: Tile): GameObject[];\n    getTileZone(tile: Tile): ZoneType;\n    isWithinBounds(tile: Tile): boolean;\n    clampWithinBounds(tile: Tile): Tile;\n}\ninterface TileOccupation {\n    getBridgeOnTile(tile: Tile): Bridge | undefined;\n    getGroundObjectsOnTile(tile: Tile): GameObject[];\n}\ninterface Terrain {\n    getPassableSpeed(tile: Tile, speedType: SpeedType, param3: boolean, onBridge: boolean): number;\n    findObstacles(location: {\n        tile: Tile;\n        onBridge?: Bridge;\n    }, object: GameObject): any[];\n}\ninterface TileCollection {\n    getByMapCoords(x: number, y: number): Tile | undefined;\n}\ninterface EventDispatcher {\n    dispatch(event: any): void;\n}\ninterface CrateGeneratorTrait {\n    pickupCrate(unit: GameObject, crate: GameObject, game: Game): void;\n}\ninterface TickResult {\n    distance: Vector3;\n    done: boolean;\n}\nexport class JumpjetLocomotor {\n    private game: Game;\n    private allowOutOfBounds: boolean = true;\n    private currentMoveDir: Vector2;\n    private currentHorizSpeed: number = 0;\n    private cancelDestLeptons?: Vector2;\n    private lastClearZ?: number;\n    constructor(game: Game) {\n        this.game = game;\n        this.currentMoveDir = new Vector2();\n    }\n    static tickStationary(gameObject: GameObject, game: Game): void {\n        if (gameObject.zone === ZoneType.Air) {\n            const bridge = gameObject.tile.onBridgeLandType\n                ? game.map.tileOccupation.getBridgeOnTile(gameObject.tile)\n                : undefined;\n            const canLand = !gameObject.rules.balloonHover &&\n                (!gameObject.unitOrderTrait.getCurrentTask()?.preventLanding ||\n                    !gameObject.rules.hoverAttack) &&\n                (game.map\n                    .getGroundObjectsOnTile(gameObject.tile)\n                    .find((obj) => obj.isBuilding() && obj.dockTrait?.isDocked(gameObject)) ||\n                    (game.map.getTileZone(gameObject.tile) !== ZoneType.Water &&\n                        0 <\n                            game.map.terrain.getPassableSpeed(gameObject.tile, SpeedType.Foot, true, !!gameObject.tile.onBridgeLandType) &&\n                        0 ===\n                            game.map.terrain.findObstacles({ tile: gameObject.tile, onBridge: bridge }, gameObject).length));\n            let targetHeight: number;\n            if (canLand) {\n                const tileHeight = gameObject.tile.z + (bridge?.tileElevation ?? 0);\n                targetHeight = Coords.tileHeightToWorld(tileHeight);\n            }\n            else {\n                const maxObjectHeight = gameObject.tile.z +\n                    game.map\n                        .getGroundObjectsOnTile(gameObject.tile)\n                        .filter((obj) => !(obj.isInfantry() &&\n                        obj.stance === StanceType.Paradrop))\n                        .reduce((max, obj) => Math.max(max, obj.tileElevation + obj.art.height), 0);\n                targetHeight = Coords.tileHeightToWorld(maxObjectHeight) + gameObject.rules.jumpjetHeight;\n            }\n            const currentHeight = gameObject.position.worldPosition.y;\n            if (targetHeight !== currentHeight) {\n                const climbRate = gameObject.rules.jumpjetClimb;\n                const heightDiff = Math.abs(targetHeight - currentHeight);\n                const climbAmount = Math.sign(targetHeight - currentHeight) * Math.min(climbRate, heightDiff);\n                const oldElevation = gameObject.tileElevation;\n                gameObject.position.moveByLeptons3(new Vector3(0, climbAmount, 0));\n                gameObject.moveTrait.handleElevationChange(oldElevation, game);\n            }\n            else if (canLand) {\n                gameObject.zone = ZoneType.Ground;\n                gameObject.onBridge = !!bridge;\n                game.events.dispatch(new ObjectLandEvent(gameObject));\n                const crate = game.map.tileOccupation\n                    .getGroundObjectsOnTile(gameObject.tile)\n                    .find((obj) => obj.isOverlay() && obj.rules.crate);\n                if (crate) {\n                    game.crateGeneratorTrait.pickupCrate(gameObject, crate, game);\n                }\n            }\n        }\n    }\n    static tickCrash(gameObject: GameObject, param2: any, param3: any): Vector3 {\n        const crashRate = 2 * gameObject.rules.jumpjetCrash;\n        gameObject.direction = (gameObject.direction - 6 + 360) % 360;\n        return new Vector3(0, -crashRate, 0);\n    }\n    onNewWaypoint(gameObject: GameObject, param2: any, param3: any): void {\n        this.currentMoveDir = FacingUtil.toMapCoords(gameObject.direction);\n        this.cancelDestLeptons = undefined;\n    }\n    tick(gameObject: GameObject, param2: any, destination: Vector2, isCancel: boolean): TickResult {\n        if (gameObject.zone !== ZoneType.Air) {\n            gameObject.onBridge = false;\n            gameObject.zone = ZoneType.Air;\n            this.game.events.dispatch(new ObjectLiftOffEvent(gameObject));\n        }\n        if (isCancel) {\n            if (!this.cancelDestLeptons) {\n                let tile = gameObject.tile;\n                if (!this.game.map.isWithinBounds(tile)) {\n                    tile = this.game.map.clampWithinBounds(tile);\n                }\n                this.cancelDestLeptons = this.computeCancelDest(tile, destination);\n            }\n            destination = this.cancelDestLeptons;\n        }\n        const currentPos = gameObject.position.getMapPosition();\n        const deltaToTarget = destination.clone().sub(currentPos);\n        const tilesToCheck = this.findTilesToCheckForBlockers(gameObject.tile, currentPos, this.currentMoveDir, deltaToTarget.length());\n        const maxObstacleHeight = tilesToCheck\n            .map((tile) => tile.z +\n            this.game.map\n                .getGroundObjectsOnTile(tile)\n                .filter((obj) => !(obj.isDestroyed ||\n                (obj.isInfantry() &&\n                    obj.stance === StanceType.Paradrop)))\n                .reduce((max, obj) => Math.max(max, obj.tileElevation + obj.art.height), 0))\n            .reduce((max, height) => Math.max(max, height), 0);\n        let extraHeight = 0;\n        if (this.lastClearZ === undefined || 2 < maxObstacleHeight - this.lastClearZ) {\n            extraHeight = 4;\n        }\n        const minHeight = Coords.tileHeightToWorld(maxObstacleHeight);\n        const clearHeight = Coords.tileHeightToWorld(maxObstacleHeight + extraHeight);\n        const currentHeight = gameObject.position.worldPosition.y;\n        const targetFacing = FacingUtil.fromMapCoords(deltaToTarget);\n        const nearTarget = deltaToTarget.length() < gameObject.rules.jumpjetSpeed;\n        let turnDelta = 0;\n        if (minHeight <= currentHeight && !nearTarget) {\n            const { facing: newFacing, delta } = FacingUtil.tick(gameObject.direction, targetFacing, gameObject.rules.jumpjetTurnRate);\n            turnDelta = delta;\n            gameObject.direction = newFacing;\n            this.currentMoveDir.copy(FacingUtil.toMapCoords(gameObject.direction));\n        }\n        if (gameObject.isVehicle()) {\n            gameObject.spinVelocity = turnDelta;\n        }\n        let atCruiseHeight = false;\n        let isDone = false;\n        let verticalSpeed = 0;\n        let horizontalSpeed = 0;\n        const climbRate = gameObject.rules.jumpjetClimb;\n        if (currentHeight < clearHeight) {\n            verticalSpeed = Math.min(climbRate, clearHeight - currentHeight);\n            atCruiseHeight = false;\n            this.currentHorizSpeed = 0;\n        }\n        else {\n            this.lastClearZ = maxObstacleHeight;\n            const cruiseHeight = minHeight + gameObject.rules.jumpjetHeight;\n            atCruiseHeight = true;\n            if (cruiseHeight !== currentHeight) {\n                const heightDiff = Math.abs(cruiseHeight - currentHeight);\n                verticalSpeed = Math.sign(cruiseHeight - currentHeight) * Math.min(climbRate, heightDiff);\n                atCruiseHeight = heightDiff <= climbRate;\n            }\n            const oldHorizSpeed = this.currentHorizSpeed;\n            this.currentHorizSpeed = Math.min(this.currentHorizSpeed + 2, gameObject.rules.jumpjetSpeed);\n            if (targetFacing === gameObject.direction) {\n                horizontalSpeed = Math.min(oldHorizSpeed, deltaToTarget.length());\n                isDone = oldHorizSpeed >= deltaToTarget.length();\n            }\n            else {\n                const turnCircle = oldHorizSpeed || turnDelta\n                    ? TargetUtil.computeTurnCircle(currentPos, this.currentMoveDir, Math.sign(turnDelta) * gameObject.rules.jumpjetTurnRate, oldHorizSpeed)\n                    : undefined;\n                if (turnCircle && geometry.circleContainsPoint(turnCircle, destination)) {\n                    horizontalSpeed = 0;\n                    this.currentHorizSpeed = 0;\n                }\n                else {\n                    horizontalSpeed = oldHorizSpeed;\n                }\n                isDone = false;\n            }\n        }\n        let moveVector: Vector2;\n        if (nearTarget) {\n            isDone = true;\n            moveVector = deltaToTarget;\n        }\n        else {\n            moveVector = this.currentMoveDir.clone().setLength(horizontalSpeed);\n        }\n        const movement = new Vector3(moveVector.x, verticalSpeed, moveVector.y);\n        const velocity = movement.clone();\n        gameObject.moveTrait.velocity.copy(velocity);\n        return { distance: movement, done: isDone && atCruiseHeight };\n    }\n    private findTilesToCheckForBlockers(currentTile: Tile, currentPos: Vector2, moveDir: Vector2, distance: number): Tile[] {\n        const nextPos = moveDir\n            .clone()\n            .setLength(Math.min(distance, Coords.LEPTONS_PER_TILE))\n            .add(currentPos)\n            .multiplyScalar(1 / Coords.LEPTONS_PER_TILE)\n            .floor();\n        const nextTile = this.game.map.tiles.getByMapCoords(nextPos.x, nextPos.y);\n        if (!nextTile || nextTile === currentTile) {\n            return [currentTile];\n        }\n        const dx = Math.sign(nextTile.rx - currentTile.rx);\n        const dy = Math.sign(nextTile.ry - currentTile.ry);\n        const tiles = [currentTile];\n        if (dx) {\n            const tile = this.game.map.tiles.getByMapCoords(currentTile.rx + dx, currentTile.ry);\n            if (tile)\n                tiles.push(tile);\n        }\n        if (dy) {\n            const tile = this.game.map.tiles.getByMapCoords(currentTile.rx, currentTile.ry + dy);\n            if (tile)\n                tiles.push(tile);\n        }\n        if (dx && dy) {\n            const tile = this.game.map.tiles.getByMapCoords(currentTile.rx + dx, currentTile.ry + dy);\n            if (tile)\n                tiles.push(tile);\n        }\n        return tiles;\n    }\n    private computeCancelDest(tile: Tile, target: Vector2): Vector2 {\n        const tilePos = target\n            .clone()\n            .multiplyScalar(1 / Coords.LEPTONS_PER_TILE)\n            .floor()\n            .multiplyScalar(Coords.LEPTONS_PER_TILE);\n        const offset = target.clone().sub(tilePos);\n        return new Vector2(tile.rx, tile.ry)\n            .multiplyScalar(Coords.LEPTONS_PER_TILE)\n            .add(offset);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/locomotor/Locomotor.ts",
    "content": "export class Locomotor {\n    constructor() { }\n}\n"
  },
  {
    "path": "src/game/gameobject/locomotor/LocomotorFactory.ts",
    "content": "import { LocomotorType } from \"@/game/type/LocomotorType\";\nimport { ChronoLocomotor } from \"@/game/gameobject/locomotor/ChronoLocomotor\";\nimport { DriveLocomotor } from \"@/game/gameobject/locomotor/DriveLocomotor\";\nimport { FootLocomotor } from \"@/game/gameobject/locomotor/FootLocomotor\";\nimport { HoverLocomotor } from \"@/game/gameobject/locomotor/HoverLocomotor\";\nimport { JumpjetLocomotor } from \"@/game/gameobject/locomotor/JumpjetLocomotor\";\nimport { MissileLocomotor } from \"@/game/gameobject/locomotor/MissileLocomotor\";\nimport { WingedLocomotor } from \"@/game/gameobject/locomotor/WingedLocomotor\";\nimport { Game } from \"@/game/Game\";\nimport { GameObject } from \"@/game/gameobject/GameObject\";\nexport class LocomotorFactory {\n    private game: Game;\n    constructor(game: Game) {\n        this.game = game;\n    }\n    create(obj: GameObject) {\n        const locomotorType = obj.rules.locomotor;\n        switch (locomotorType) {\n            case LocomotorType.Infantry:\n                return new FootLocomotor(this.game);\n            case LocomotorType.Jumpjet:\n                return new JumpjetLocomotor(this.game);\n            case LocomotorType.Vehicle:\n            case LocomotorType.Ship:\n                return new DriveLocomotor(this.game);\n            case LocomotorType.Chrono:\n                return obj.isVehicle() && obj.harvesterTrait && obj.rules.teleporter\n                    ? new DriveLocomotor(this.game)\n                    : new ChronoLocomotor(this.game);\n            case LocomotorType.Aircraft:\n                return new WingedLocomotor(this.game);\n            case LocomotorType.Missile:\n                return new MissileLocomotor(this.game, this.game.rules.general.getMissileRules(obj.name));\n            case LocomotorType.Hover:\n                return new HoverLocomotor(this.game.rules.general.hover);\n            default:\n                throw new Error(`Unhandled locomotor type ${locomotorType}`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/locomotor/MissileLocomotor.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { ObjectLiftOffEvent } from \"@/game/event/ObjectLiftOffEvent\";\nimport * as geometry from \"@/game/math/geometry\";\nimport { FacingUtil } from \"@/game/gameobject/unit/FacingUtil\";\nimport { Vector3 } from \"@/game/math/Vector3\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { CubicBezierCurve3 } from \"@/game/math/CubicBezierCurve3\";\nimport { GameMath } from \"@/game/math/GameMath\";\nenum FlightPhase {\n    Boost = 0,\n    Midcourse = 1,\n    Terminal = 2\n}\ninterface MissileRules {\n    altitude: number;\n    acceleration: number;\n    lazyCurve: boolean;\n    bodyLength: number;\n}\ninterface GameObject {\n    position: {\n        worldPosition: Vector3;\n    };\n    zone: ZoneType;\n    onBridge: boolean;\n    rules: {\n        speed: number;\n        rot: number;\n    };\n    direction: number;\n    pitch: number;\n    moveTrait: {\n        velocity: Vector3;\n    };\n}\ninterface Game {\n    map: {\n        tileOccupation: {\n            getBridgeOnTile(tile: any): any;\n        };\n        isWithinHardBounds(position: Vector3): boolean;\n    };\n    events: {\n        dispatch(event: any): void;\n    };\n    destroyObject(obj: GameObject): void;\n}\ninterface Waypoint {\n    tile: {\n        rx: number;\n        ry: number;\n        z: number;\n    };\n}\ninterface Tile {\n    rx: number;\n    ry: number;\n    z: number;\n}\nexport class MissileLocomotor {\n    private game: Game;\n    private missileRules: MissileRules;\n    private flightPhase: FlightPhase;\n    private targetPosition?: Vector3;\n    private cruiseAltitude?: number;\n    private currentVelocity?: Vector3;\n    private descentCurve?: CubicBezierCurve3;\n    private descentTravelled?: number;\n    constructor(game: Game, missileRules: MissileRules) {\n        this.game = game;\n        this.missileRules = missileRules;\n        this.flightPhase = FlightPhase.Boost;\n    }\n    selectNextWaypoint(gameObject: GameObject, waypoints: Waypoint[]): Waypoint {\n        const lastWaypoint = waypoints[waypoints.length - 1];\n        const bridge = this.game.map.tileOccupation.getBridgeOnTile(lastWaypoint.tile);\n        const tileZ = lastWaypoint.tile.z + (bridge?.tileElevation ?? 0);\n        this.targetPosition = Coords.tile3dToWorld(lastWaypoint.tile.rx + 0.5, lastWaypoint.tile.ry + 0.5, tileZ);\n        this.cruiseAltitude = Coords.tileHeightToWorld(tileZ) + this.missileRules.altitude;\n        return lastWaypoint;\n    }\n    onNewWaypoint(gameObject: GameObject, waypoint: Waypoint, waypointIndex: number): void {\n    }\n    tick(gameObject: GameObject, deltaTime: number, tickCount: number): {\n        distance: Vector3;\n        done: boolean;\n    } {\n        const currentPosition = gameObject.position.worldPosition.clone();\n        const targetDirection = this.targetPosition!.clone().sub(currentPosition);\n        if (gameObject.zone !== ZoneType.Air) {\n            gameObject.onBridge = false;\n            gameObject.zone = ZoneType.Air;\n            this.game.events.dispatch(new ObjectLiftOffEvent(gameObject));\n        }\n        let speed: number;\n        if (this.currentVelocity) {\n            const maxSpeed = gameObject.rules.speed;\n            speed = Math.min(this.currentVelocity.length() + this.missileRules.acceleration, maxSpeed);\n        }\n        else {\n            speed = this.missileRules.acceleration;\n            if (this.missileRules.lazyCurve) {\n                this.currentVelocity = new Vector3(targetDirection.x, 0, targetDirection.z);\n            }\n            else {\n                this.currentVelocity = Coords.vecGroundToWorld(FacingUtil.toMapCoords(gameObject.direction));\n            }\n            geometry.rotateVec3Towards(this.currentVelocity, new Vector3(this.currentVelocity.x, 1e8, this.currentVelocity.z), gameObject.pitch);\n        }\n        this.currentVelocity.setLength(speed);\n        let done = false;\n        switch (this.flightPhase) {\n            case FlightPhase.Boost:\n                if (gameObject.position.worldPosition.y >= this.cruiseAltitude!) {\n                    this.flightPhase = FlightPhase.Midcourse;\n                }\n                else {\n                    done = false;\n                    break;\n                }\n            // falls through\n            case FlightPhase.Midcourse:\n                const horizontalDistance = new Vector2(targetDirection.x, targetDirection.z).length();\n                if (!this.missileRules.lazyCurve) {\n                    geometry.rotateVec3Towards(this.currentVelocity, new Vector3(this.currentVelocity.x, 0, this.currentVelocity.z), gameObject.rules.rot);\n                    if (this.currentVelocity.y < 1) {\n                        const length = this.currentVelocity.length();\n                        this.currentVelocity.y = 0;\n                        this.currentVelocity.setLength(length);\n                    }\n                    geometry.rotateVec3Towards(this.currentVelocity, new Vector3(targetDirection.x, this.currentVelocity.y, targetDirection.z), gameObject.rules.rot);\n                    gameObject.direction = FacingUtil.fromMapCoords(Coords.vecWorldToGround(this.currentVelocity));\n                    gameObject.pitch = Math.sign(this.currentVelocity.y) *\n                        geometry.angleDegBetweenVec3(this.currentVelocity, new Vector3(this.currentVelocity.x, 0, this.currentVelocity.z));\n                    if (horizontalDistance / (currentPosition.y - this.targetPosition!.y) < 1) {\n                        this.flightPhase = FlightPhase.Terminal;\n                    }\n                    break;\n                }\n                this.flightPhase = FlightPhase.Terminal;\n                const controlPoint1 = currentPosition\n                    .clone()\n                    .add(this.currentVelocity\n                    .clone()\n                    .setLength(horizontalDistance / 3 / GameMath.cos(geometry.degToRad(gameObject.pitch))));\n                const controlPoint2 = this.targetPosition!.clone().lerp(currentPosition, 0.15).setY(controlPoint1.y);\n                this.descentCurve = new CubicBezierCurve3(currentPosition, controlPoint1, controlPoint2, this.targetPosition!);\n            case FlightPhase.Terminal:\n                const bodyLength = this.missileRules.bodyLength;\n                if (this.missileRules.lazyCurve) {\n                    const curveLength = this.descentCurve!.getLength();\n                    this.descentTravelled = this.descentTravelled ?? 0;\n                    this.descentTravelled += Math.min(speed, curveLength - bodyLength - this.descentTravelled);\n                    const t = this.descentTravelled / curveLength;\n                    const pointOnCurve = this.descentCurve!.getPointAt(t);\n                    const tangent = this.descentCurve!.getTangentAt(t);\n                    this.currentVelocity.copy(pointOnCurve.sub(currentPosition));\n                    const horizontalTangent = tangent.clone().setY(0);\n                    gameObject.pitch = Math.sign(tangent.y - horizontalTangent.y) *\n                        geometry.angleDegBetweenVec3(horizontalTangent, tangent);\n                    done = (this.descentTravelled + bodyLength) / curveLength >= 1;\n                }\n                else {\n                    geometry.rotateVec3Towards(this.currentVelocity, targetDirection, gameObject.rules.rot);\n                    gameObject.direction = FacingUtil.fromMapCoords(Coords.vecWorldToGround(this.currentVelocity));\n                    gameObject.pitch = Math.sign(this.currentVelocity.y) *\n                        geometry.angleDegBetweenVec3(this.currentVelocity, new Vector3(this.currentVelocity.x, 0, this.currentVelocity.z));\n                    const distanceToTarget = targetDirection.length() - bodyLength;\n                    if (distanceToTarget < speed || distanceToTarget < 1) {\n                        this.currentVelocity.copy(targetDirection.clone().addScalar(-bodyLength));\n                        done = true;\n                    }\n                }\n                break;\n            default:\n                throw new Error(`Unhandled flight phase \"${this.flightPhase}\"`);\n        }\n        const newPosition = currentPosition.clone().add(this.currentVelocity);\n        if (this.game.map.isWithinHardBounds(newPosition)) {\n            gameObject.moveTrait.velocity.copy(this.currentVelocity);\n            return { distance: this.currentVelocity, done };\n        }\n        else {\n            this.game.destroyObject(gameObject);\n            return { done: true, distance: new Vector3() };\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/locomotor/WingedLocomotor.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { FacingUtil } from \"@/game/gameobject/unit/FacingUtil\";\nimport { TargetUtil } from \"@/game/gameobject/unit/TargetUtil\";\nimport { circleContainsPoint } from \"@/util/geometry\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { ObjectLiftOffEvent } from \"@/game/event/ObjectLiftOffEvent\";\nimport { ObjectLandEvent } from \"@/game/event/ObjectLandEvent\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { MoveToDockTask } from \"@/game/gameobject/task/MoveToDockTask\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { Vector3 } from \"@/game/math/Vector3\";\nimport { lerp } from \"@/util/math\";\nimport { GameMath } from \"@/game/math/GameMath\";\nenum ManeuverType {\n    None = 0,\n    CircleStrafe = 1,\n    HoverStrafe = 2\n}\ninterface Unit {\n    zone: ZoneType;\n    tile: any;\n    rules: any;\n    unitOrderTrait: any;\n    spawnLinkTrait?: any;\n    airportBoundTrait?: any;\n    direction: number;\n    position: any;\n    onBridge: boolean;\n    moveTrait: any;\n    attackTrait?: any;\n    crashableTrait: any;\n    roll: number;\n    pitch: number;\n    tileElevation: number;\n    isUnit(): boolean;\n}\ninterface Game {\n    map: any;\n    events: any;\n    rules: any;\n    crateGeneratorTrait: any;\n    generateRandomInt(min: number, max: number): number;\n}\ninterface CrashState {\n    rollDelta?: number;\n    pitchDelta?: number;\n}\nexport class WingedLocomotor {\n    private game: Game;\n    private allowOutOfBounds: boolean = true;\n    private lastDestLeptons: Vector2;\n    private currentMoveDir: Vector2;\n    private currentHorizSpeed: number = 0;\n    private maneuverType: ManeuverType = ManeuverType.None;\n    private deceleratingToTurn: boolean = false;\n    private cancelDestLeptons?: Vector2;\n    private thrustFacing?: number;\n    static tickStationary(s: Unit, a: Game): void {\n        if (s.zone === ZoneType.Air) {\n            const n = s.tile.onBridgeLandType\n                ? a.map.tileOccupation.getBridgeOnTile(s.tile)\n                : undefined;\n            let e = s.rules.landable &&\n                !s.unitOrderTrait.getCurrentTask()?.preventLanding;\n            const i = s.spawnLinkTrait?.getParent();\n            if (e && i) {\n                e = !(((!i.isUnit() || !i.onBridge) && n) ||\n                    i.tile !== s.tile);\n            }\n            else if (e && !s.airportBoundTrait) {\n                e = a.map.getTileZone(s.tile) !== ZoneType.Water &&\n                    0 < a.map.terrain.getPassableSpeed(s.tile, SpeedType.Foot, true, !!s.tile.onBridgeLandType) &&\n                    0 === a.map.terrain.findObstacles({ tile: s.tile, onBridge: n }, s).length;\n            }\n            let r: number;\n            if (e) {\n                const dockTrait = s.airportBoundTrait?.preferredAirport?.dockTrait;\n                const o = dockTrait?.isDocked(s) || dockTrait?.hasReservedDockForUnit(s);\n                if (!s.airportBoundTrait || o) {\n                    const l = o ? 0 : 270;\n                    if (s.direction !== l) {\n                        s.direction = FacingUtil.tick(s.direction, l, s.rules.rot).facing;\n                        return;\n                    }\n                }\n                if (s.airportBoundTrait) {\n                    let airport = s.airportBoundTrait.preferredAirport;\n                    if (!airport?.dockTrait?.isDocked(s)) {\n                        if (!airport?.dockTrait?.getAvailableDockCount()) {\n                            airport = s.airportBoundTrait.findAvailableAirport(s);\n                            s.airportBoundTrait.preferredAirport = airport;\n                            if (airport) {\n                                const dockNumber = airport.dockTrait.getFirstAvailableDockNumber();\n                                airport.dockTrait.reserveDockAt(s, dockNumber);\n                            }\n                        }\n                        if (airport) {\n                            s.unitOrderTrait.addTask(new MoveToDockTask(a, airport));\n                            s.unitOrderTrait[NotifyTick.onTick](s, a);\n                        }\n                        else {\n                            s.crashableTrait.crash(undefined);\n                        }\n                        return;\n                    }\n                }\n                const t = i\n                    ? i.tile.z + i.tileElevation\n                    : s.tile.z + (n?.tileElevation ?? 0);\n                r = Coords.tileHeightToWorld(t);\n            }\n            else {\n                const t = s.tile.z + (n?.tileElevation ?? 0);\n                const c = s.rules.flightLevel ?? a.rules.general.flightLevel;\n                r = Coords.tileHeightToWorld(t) + c;\n            }\n            const currentY = s.position.worldPosition.y;\n            if (r !== currentY) {\n                const distance = Math.abs(r - currentY);\n                const deltaY = Math.sign(r - currentY) * Math.min(30, distance);\n                const oldElevation = s.tileElevation;\n                s.position.moveByLeptons3(new Vector3(0, deltaY, 0));\n                s.moveTrait.handleElevationChange(oldElevation, a);\n            }\n            else if (e) {\n                s.zone = ZoneType.Ground;\n                if (i) {\n                    i.airSpawnTrait.storeAircraft(s, a);\n                }\n                else {\n                    s.onBridge = !!n;\n                }\n                a.events.dispatch(new ObjectLandEvent(s));\n                const crate = a.map.tileOccupation\n                    .getGroundObjectsOnTile(s.tile)\n                    .find((e: any) => e.isOverlay() && e.rules.crate);\n                if (crate) {\n                    a.crateGeneratorTrait.pickupCrate(s, crate, a);\n                }\n            }\n        }\n    }\n    static tickCrash(e: Unit, t: Game, i: CrashState): Vector3 {\n        if (i.rollDelta === undefined) {\n            i.rollDelta = t.generateRandomInt(-15, 15);\n        }\n        if (i.pitchDelta === undefined) {\n            i.pitchDelta = t.generateRandomInt(0, 15);\n        }\n        e.roll += i.rollDelta;\n        e.pitch += i.pitchDelta;\n        const r = Coords.vecWorldToGround(e.moveTrait.velocity);\n        return new Vector3(r.x, -30, r.y);\n    }\n    constructor(game: Game) {\n        this.game = game;\n        this.allowOutOfBounds = true;\n        this.lastDestLeptons = new Vector2();\n        this.currentMoveDir = new Vector2();\n        this.currentHorizSpeed = 0;\n        this.maneuverType = ManeuverType.None;\n        this.deceleratingToTurn = false;\n    }\n    onNewWaypoint(e: Unit, t: any, i: Vector2): void {\n        this.currentHorizSpeed = Coords.vecWorldToGround(e.moveTrait.velocity).length();\n        this.cancelDestLeptons = undefined;\n    }\n    tick(t: Unit, e: any, i: Vector2, r: boolean): {\n        distance: Vector3;\n        done: boolean;\n    } {\n        if (r) {\n            if (!this.cancelDestLeptons) {\n                let tile = t.tile;\n                if (!this.game.map.isWithinBounds(tile)) {\n                    tile = this.game.map.clampWithinBounds(tile);\n                }\n                this.cancelDestLeptons = this.computeCancelDest(tile, i);\n            }\n            i = this.cancelDestLeptons;\n        }\n        const s = t.position.getMapPosition();\n        const a = i.clone().sub(s);\n        const n = a.length();\n        if (!this.lastDestLeptons.equals(i)) {\n            this.lastDestLeptons.copy(i);\n            if (r) {\n                this.maneuverType = ManeuverType.HoverStrafe;\n            }\n            else if (t.zone === ZoneType.Air && this.currentHorizSpeed < 5) {\n                this.maneuverType = n > Coords.LEPTONS_PER_TILE\n                    ? ManeuverType.CircleStrafe\n                    : ManeuverType.HoverStrafe;\n            }\n            else {\n                this.maneuverType = ManeuverType.None;\n            }\n            this.deceleratingToTurn = false;\n        }\n        if (t.zone !== ZoneType.Air) {\n            t.onBridge = false;\n            t.zone = ZoneType.Air;\n            this.game.events.dispatch(new ObjectLiftOffEvent(t));\n        }\n        const o = t.tile.onBridgeLandType\n            ? this.game.map.tileOccupation.getBridgeOnTile(t.tile)\n            : undefined;\n        const l = t.tile.z + (o?.tileElevation ?? 0);\n        const flightLevel = t.rules.flightLevel ?? this.game.rules.general.flightLevel;\n        const h = Coords.tileHeightToWorld(l) + flightLevel;\n        const currentY = t.position.worldPosition.y;\n        const u = FacingUtil.fromMapCoords(a);\n        if (t.direction === u &&\n            this.maneuverType === ManeuverType.None &&\n            n <= Coords.LEPTONS_PER_TILE) {\n            this.maneuverType = ManeuverType.HoverStrafe;\n        }\n        else if (t.direction === u &&\n            this.maneuverType === ManeuverType.CircleStrafe) {\n            this.maneuverType = ManeuverType.None;\n        }\n        let d: number;\n        switch (this.maneuverType) {\n            case ManeuverType.HoverStrafe:\n                if (t.attackTrait?.currentTarget) {\n                    const targetPos = Coords.vecWorldToGround(t.attackTrait.currentTarget.getWorldCoords());\n                    d = FacingUtil.fromMapCoords(targetPos.sub(s));\n                }\n                else {\n                    d = t.airportBoundTrait?.preferredAirport?.dockTrait?.hasReservedDockForUnit(t)\n                        ? 0\n                        : 270;\n                }\n                break;\n            case ManeuverType.CircleStrafe:\n            case ManeuverType.None:\n                d = u;\n                break;\n            default:\n                throw new Error('Unknown maneuver type \"' + this.maneuverType + '\"');\n        }\n        const { facing: g, delta: p } = FacingUtil.tick(t.direction, d, t.rules.rot);\n        t.direction = g;\n        t.roll = Math.sign(p) * t.rules.pitchAngle;\n        let m: number;\n        switch (this.maneuverType) {\n            case ManeuverType.HoverStrafe:\n                m = u;\n                break;\n            case ManeuverType.CircleStrafe:\n                m = (g - 90 * Math.sign(p) + 360) % 360;\n                break;\n            case ManeuverType.None:\n                m = g;\n                break;\n            default:\n                throw new Error('Unknown maneuver type \"' + this.maneuverType + '\"');\n        }\n        if (this.thrustFacing === undefined) {\n            this.thrustFacing = m;\n        }\n        const rotSpeed = this.currentHorizSpeed > 5\n            ? t.rules.rot\n            : Number.POSITIVE_INFINITY;\n        const { facing: c, delta: deltaThrust } = FacingUtil.tick(this.thrustFacing, m, rotSpeed);\n        this.thrustFacing = c;\n        this.currentMoveDir.copy(FacingUtil.toMapCoords(this.thrustFacing));\n        let f = false;\n        let y = 0;\n        let T = 0;\n        let v = true;\n        if (h !== currentY) {\n            const heightDiff = Math.abs(h - currentY);\n            y = Math.sign(h - currentY) * Math.min(30, heightDiff);\n            v = heightDiff <= 30;\n        }\n        let b = t.rules.speed;\n        if (n <= Coords.LEPTONS_PER_TILE &&\n            this.maneuverType !== ManeuverType.CircleStrafe) {\n            b = lerp(1, b / 2, GameMath.sqrt(n / Coords.LEPTONS_PER_TILE));\n        }\n        if (this.deceleratingToTurn) {\n            this.currentHorizSpeed = Math.max(0, this.currentHorizSpeed - 2);\n        }\n        else {\n            this.currentHorizSpeed = Math.min(this.currentHorizSpeed + 2, b);\n        }\n        const S = this.currentHorizSpeed;\n        this.deceleratingToTurn = false;\n        if (deltaThrust) {\n            const turnCircle = (S || deltaThrust)\n                ? TargetUtil.computeTurnCircle(s, this.currentMoveDir, Math.sign(deltaThrust) * t.rules.rot, S)\n                : undefined;\n            if ((S !== 0 && !circleContainsPoint(turnCircle, i)) ||\n                (this.maneuverType === ManeuverType.HoverStrafe ||\n                    n > Coords.LEPTONS_PER_TILE\n                    ? (this.deceleratingToTurn = true)\n                    : this.maneuverType === ManeuverType.None &&\n                        (this.maneuverType = ManeuverType.HoverStrafe))) {\n            }\n            T = S;\n            f = false;\n        }\n        else {\n            T = Math.min(S, n);\n            f = n <= S;\n        }\n        let w: Vector2;\n        if (n < 1) {\n            f = true;\n            w = a;\n        }\n        else if (f) {\n            w = a;\n        }\n        else {\n            w = this.currentMoveDir.clone().setLength(T);\n        }\n        const C = new Vector3(w.x, y, w.y);\n        const velocity = C.clone();\n        t.moveTrait.velocity.copy(velocity);\n        return { distance: C, done: f && v };\n    }\n    computeCancelDest(e: any, t: Vector2): Vector2 {\n        const tileAligned = t\n            .clone()\n            .multiplyScalar(1 / Coords.LEPTONS_PER_TILE)\n            .floor()\n            .multiplyScalar(Coords.LEPTONS_PER_TILE);\n        const offset = t.clone().sub(tileAligned);\n        return new Vector2(e.rx, e.ry)\n            .multiplyScalar(Coords.LEPTONS_PER_TILE)\n            .add(offset);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/selection/SelectionLevel.ts",
    "content": "export enum SelectionLevel {\n    None = 0,\n    Hover = 1,\n    Selected = 2,\n    SelectedHover = 3\n}\n"
  },
  {
    "path": "src/game/gameobject/selection/SelectionList.ts",
    "content": "export class SelectionList {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/selection/SelectionModel.ts",
    "content": "import { SelectionLevel } from './SelectionLevel';\nimport { GameObject } from '../GameObject';\nexport class SelectionModel {\n    private selectionLevel: SelectionLevel;\n    private maxSelectionLevel: SelectionLevel;\n    private controlGroupNumber?: number;\n    constructor(gameObject: GameObject) {\n        this.selectionLevel = SelectionLevel.None;\n        if (gameObject.isBuilding() && gameObject.rules.wall) {\n            this.maxSelectionLevel = SelectionLevel.None;\n        }\n        else {\n            this.maxSelectionLevel = gameObject.rules.selectable\n                ? SelectionLevel.Selected | SelectionLevel.Hover\n                : SelectionLevel.Hover;\n        }\n    }\n    getSelectionLevel(): SelectionLevel {\n        return this.selectionLevel;\n    }\n    setSelectionLevel(level: SelectionLevel): void {\n        this.selectionLevel = Math.min(this.maxSelectionLevel, level);\n    }\n    setHover(hover: boolean): void {\n        this.setSelectionLevel(hover\n            ? this.selectionLevel | SelectionLevel.Hover\n            : this.selectionLevel & ~SelectionLevel.Hover);\n    }\n    setSelected(selected: boolean): void {\n        this.setSelectionLevel(selected\n            ? this.selectionLevel | SelectionLevel.Selected\n            : this.selectionLevel & ~SelectionLevel.Selected);\n    }\n    isHovered(): boolean {\n        return ((this.selectionLevel >> SelectionLevel.Hover) & 1) as any;\n    }\n    isSelected(): boolean {\n        return this.selectionLevel >= SelectionLevel.Selected;\n    }\n    getControlGroupNumber(): number | undefined {\n        return this.controlGroupNumber;\n    }\n    setControlGroupNumber(number: number): void {\n        this.controlGroupNumber = number;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/selection/UnitSelection.ts",
    "content": "import { SelectionModel } from './SelectionModel';\nimport { fnv32a } from '@/util/math';\nimport { GameObject } from '../GameObject';\nexport class UnitSelection {\n    private selectedUnits: Set<GameObject>;\n    private selectionModelsByUnit: Map<GameObject, SelectionModel>;\n    private groups: Map<number, Set<GameObject>>;\n    private hashNeedsUpdate: boolean;\n    private hash: number;\n    constructor() {\n        this.selectedUnits = new Set();\n        this.selectionModelsByUnit = new Map();\n        this.groups = new Map();\n        this.hashNeedsUpdate = true;\n    }\n    getOrCreateSelectionModel(unit: GameObject): SelectionModel {\n        let model = this.selectionModelsByUnit.get(unit);\n        if (!model) {\n            model = new SelectionModel(unit);\n            this.selectionModelsByUnit.set(unit, model);\n        }\n        return model;\n    }\n    deselectAll(): void {\n        this.selectedUnits.forEach(unit => this.selectionModelsByUnit.get(unit)?.setSelected(false));\n        this.selectedUnits.clear();\n        this.hashNeedsUpdate = true;\n    }\n    addToSelection(unit: GameObject): void {\n        this.selectedUnits.add(unit);\n        this.getOrCreateSelectionModel(unit).setSelected(true);\n        this.hashNeedsUpdate = true;\n    }\n    removeFromSelection(units: GameObject[]): void {\n        units.forEach(unit => {\n            this.selectedUnits.delete(unit);\n            this.getOrCreateSelectionModel(unit).setSelected(false);\n        });\n        this.hashNeedsUpdate = true;\n    }\n    getSelectedUnits(): GameObject[] {\n        return [...this.selectedUnits].filter(unit => !unit.isDestroyed && !unit.isCrashing && !unit.isDisposed && unit.isSpawned);\n    }\n    isSelected(unit: GameObject): boolean {\n        return this.selectedUnits.has(unit);\n    }\n    cleanupUnit(unit: GameObject): void {\n        this.selectionModelsByUnit.delete(unit);\n        this.selectedUnits.delete(unit);\n        this.removeUnitsFromGroup([unit]);\n        this.hashNeedsUpdate = true;\n    }\n    updateHash(): void {\n        this.hash = fnv32a([...this.selectedUnits].map(unit => unit.id));\n    }\n    getHash(): number {\n        if (this.hashNeedsUpdate) {\n            this.updateHash();\n            this.hashNeedsUpdate = false;\n        }\n        return this.hash;\n    }\n    createGroup(groupNumber: number): void {\n        this.addUnitsToGroup(groupNumber, this.getSelectedUnits());\n    }\n    addUnitsToGroup(groupNumber: number, units: GameObject[], clearExisting: boolean = true): void {\n        this.removeUnitsFromGroup(units);\n        let group = this.groups.get(groupNumber);\n        if (!group) {\n            group = new Set();\n            this.groups.set(groupNumber, group);\n        }\n        if (clearExisting) {\n            [...group.values()].forEach(unit => this.selectionModelsByUnit.get(unit)?.setControlGroupNumber(undefined));\n            group.clear();\n        }\n        for (const unit of units) {\n            group.add(unit);\n            this.getOrCreateSelectionModel(unit).setControlGroupNumber(groupNumber);\n        }\n    }\n    addGroupToSelection(groupNumber: number): void {\n        if (this.groups.has(groupNumber)) {\n            for (const unit of [...this.groups.get(groupNumber)!]) {\n                this.addToSelection(unit);\n            }\n        }\n    }\n    selectGroup(groupNumber: number): void {\n        this.deselectAll();\n        this.addGroupToSelection(groupNumber);\n    }\n    getGroupUnits(groupNumber: number): GameObject[] {\n        return [...(this.groups.get(groupNumber) ?? [])];\n    }\n    removeUnitsFromGroup(units: GameObject[]): void {\n        for (const group of this.groups.values()) {\n            for (const unit of units) {\n                group.delete(unit);\n                this.selectionModelsByUnit.get(unit)?.setControlGroupNumber(undefined);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/selection/UnitSelectionLite.ts",
    "content": "import { GameObject } from '../GameObject';\nexport class UnitSelectionLite {\n    private player: any;\n    private selectedUnits: Set<GameObject>;\n    constructor(player: any) {\n        this.player = player;\n        this.selectedUnits = new Set();\n    }\n    update(units: GameObject[]): void {\n        const enemyUnit = [...units].reverse().find(unit => unit.owner !== this.player);\n        if (enemyUnit) {\n            units = [enemyUnit];\n        }\n        this.selectedUnits.clear();\n        for (const unit of units) {\n            if (unit.rules.selectable) {\n                this.selectedUnits.add(unit);\n            }\n        }\n    }\n    getSelectedUnits(): GameObject[] {\n        return [...this.selectedUnits].filter(unit => !unit.isDestroyed && !unit.isCrashing);\n    }\n    isSelected(unit: GameObject): boolean {\n        return this.selectedUnits.has(unit);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/AttackTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { WaitMinutesTask } from \"@/game/gameobject/task/system/WaitMinutesTask\";\nimport { WeaponType } from \"@/game/WeaponType\";\nimport { MoveInWeaponRangeTask } from \"@/game/gameobject/task/move/MoveInWeaponRangeTask\";\nimport { FacingUtil } from \"@/game/gameobject/unit/FacingUtil\";\nimport { TurnTask } from \"@/game/gameobject/task/TurnTask\";\nimport { WaitTicksTask } from \"@/game/gameobject/task/system/WaitTicksTask\";\nimport { AttackState, AttackTrait } from \"@/game/gameobject/trait/AttackTrait\";\nimport { GameObject } from \"@/game/gameobject/GameObject\";\nimport { LosHelper } from \"@/game/gameobject/unit/LosHelper\";\nimport { MoveResult } from \"@/game/gameobject/trait/MoveTrait\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nimport { Coords } from \"@/game/Coords\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { MovePositionHelper } from \"@/game/gameobject/unit/MovePositionHelper\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { TaskStatus } from \"@/game/gameobject/task/system/TaskStatus\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { Vector3 } from \"@/game/math/Vector3\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nconst MAX_MOVE_ATTEMPTS = 3;\nconst FACING_TOLERANCE = 11.25;\nconst FACING_TOLERANCE_PROJECTILE = 4 * FACING_TOLERANCE;\ninterface AttackOptions {\n    force?: boolean;\n    passive?: boolean;\n    holdGround?: boolean;\n    leashTiles?: number;\n    disallowTurning?: boolean;\n}\ninterface TargetLinesConfig {\n    target?: GameObject;\n    pathNodes: Array<{\n        tile: any;\n        onBridge?: any;\n    }>;\n    isAttack?: boolean;\n}\ninterface Position {\n    tile: any;\n    onBridge?: any;\n}\nexport class AttackTask extends Task {\n    public game: any;\n    private target: any;\n    private weapon: any;\n    public options: AttackOptions;\n    private moveExecuted: boolean = false;\n    private moveAttempts: number = 0;\n    private rangeCheckCooldown: number = 0;\n    private lastInRangeTargetPosition: Vector3 = new Vector3();\n    private lastInRangeSelfPosition: Vector3 = new Vector3();\n    private initialIndirectTarget: boolean = false;\n    private forceDropTarget: boolean = false;\n    private rangeHelper: RangeHelper;\n    private losHelper: LosHelper;\n    private targetLinesConfig: TargetLinesConfig;\n    private needsTargetUpdate?: any;\n    private lastValidTargetPosition?: Position;\n    private initialTargetOwner?: any;\n    private initialSelfPosition?: Position;\n    private lastTargetTpCheck?: number;\n    private lastSelfTileBeforeMove?: any;\n    private lastSelfMoveTargetTile?: any;\n    constructor(game: any, target: any, weapon: any, options: AttackOptions = {}) {\n        super();\n        this.game = game;\n        this.target = target;\n        this.weapon = weapon;\n        this.options = options;\n        this.rangeHelper = new RangeHelper(game.map.tileOccupation);\n        this.losHelper = new LosHelper(game.map.tiles, game.map.tileOccupation);\n        this.targetLinesConfig = { pathNodes: [] };\n        this.updateTargetLines(this.target, true);\n    }\n    duplicate(): AttackTask {\n        return new AttackTask(this.game, this.target, this.weapon, this.options);\n    }\n    getWeapon(): any {\n        return this.weapon;\n    }\n    setWeapon(weapon: any): void {\n        this.weapon = weapon;\n    }\n    setForceAttack(force: boolean): void {\n        this.options.force = force;\n    }\n    requestTargetUpdate(target: any): void {\n        if (!this.target.equals(target)) {\n            this.needsTargetUpdate = target;\n        }\n    }\n    public onTargetChange(obj: any): void {\n        const attackTrait = obj.attackTrait;\n        const target = this.target;\n        attackTrait.currentTarget = target;\n        this.lastValidTargetPosition = target.obj\n            ? { tile: target.tile, onBridge: target.getBridge() }\n            : undefined;\n        this.initialTargetOwner = target.obj?.isTechno()\n            ? target.obj.owner\n            : undefined;\n        this.initialIndirectTarget =\n            !target.obj &&\n                this.game.map.tileOccupation\n                    .getObjectsOnTile(target.tile)\n                    .some((e: any) => (e.isOverlay() && !e.isBridgePlaceholder()) ||\n                    e.isTerrain());\n        this.updateTargetLines(target, true);\n    }\n    private updateTargetLines(target: any, isAttack: boolean): void {\n        this.targetLinesConfig.target = target.obj;\n        this.targetLinesConfig.pathNodes = target.obj\n            ? []\n            : [{ tile: target.tile, onBridge: target.getBridge() }];\n        this.targetLinesConfig.isAttack = isAttack;\n    }\n    onStart(obj: any): void {\n        if (!obj.attackTrait) {\n            throw new Error(`Object ${obj.name} has no attack trait`);\n        }\n        if (obj.ammo === 0) {\n            this.cancel();\n            return;\n        }\n        const tileOccupation = this.game.map.tileOccupation;\n        obj.attackTrait.attackState = AttackState.CheckRange;\n        this.onTargetChange(obj);\n        this.initialSelfPosition = {\n            tile: obj.tile,\n            onBridge: obj.isUnit() && obj.onBridge\n                ? tileOccupation.getBridgeOnTile(obj.tile)\n                : undefined,\n        };\n        if (this.weapon.rules.limboLaunch && obj.isUnit() && !this.target.obj) {\n            this.forceDropTarget = true;\n            const { reachable, fallback } = this.findReachableMeleePosition(this.target.tile, !!this.target.getBridge(), { width: 1, height: 1 }, obj);\n            if (!reachable && fallback) {\n                this.lastValidTargetPosition = fallback;\n                this.updateTargetLines(this.game.createTarget(fallback.onBridge, fallback.tile), false);\n            }\n        }\n        if (this.weapon.rules.limboLaunch &&\n            this.target.obj?.isTechno() &&\n            obj.isUnit() &&\n            !this.rangeHelper.isInWeaponRange(obj, this.target.obj, this.weapon, this.game.rules)) {\n            const { reachable, fallback } = this.findReachableMeleePosition(this.target.obj.tile, this.target.obj.isUnit() && this.target.obj.onBridge, this.target.obj.getFoundation(), obj);\n            if (!reachable) {\n                if ((obj.unitOrderTrait.waypointPath?.waypoints?.length ?? 0) > 1) {\n                    this.cancel();\n                }\n                else {\n                    this.forceDropTarget = true;\n                    if (fallback) {\n                        this.lastValidTargetPosition = fallback;\n                        this.updateTargetLines(this.game.createTarget(fallback.onBridge, fallback.tile), false);\n                    }\n                }\n            }\n        }\n        if (this.rangeHelper.isInWeaponRange(obj, this.target.obj ?? this.target.tile, this.weapon, this.game.rules) &&\n            obj.isUnit() &&\n            obj.rules.movementZone === MovementZone.Fly &&\n            obj.zone !== ZoneType.Air &&\n            (obj.rules.hoverAttack || obj.isAircraft())) {\n            this.children.push(new MoveTask(this.game, obj.tile, false).setCancellable(false));\n        }\n    }\n    private findReachableMeleePosition(targetTile: any, onBridge: boolean, foundation: any, obj: any): {\n        reachable: any;\n        fallback?: Position;\n    } {\n        const map = this.game.map;\n        const tileOccupation = map.tileOccupation;\n        const targetBridge = onBridge ? tileOccupation.getBridgeOnTile(targetTile) : undefined;\n        const movePositionHelper = new MovePositionHelper(map);\n        const isFlying = obj.rules.movementZone === MovementZone.Fly;\n        const isPassable = (tile: any, bridge?: any): boolean => isFlying ||\n            (map.terrain.getPassableSpeed(tile, obj.rules.speedType, obj.isInfantry(), !!bridge) > 0 &&\n                movePositionHelper.isEligibleTile(tile, bridge, targetBridge, targetTile) &&\n                !map.terrain.findObstacles({ tile, onBridge: bridge }, obj).length);\n        let fallback: Position | undefined;\n        const tileFinder = new RadialTileFinder(map.tiles, map.mapBounds, targetTile, foundation, 1, Math.ceil(this.weapon.rules.range), (tile: any) => {\n            let found = false;\n            if (isPassable(tile, undefined)) {\n                fallback = fallback ?? { tile, onBridge: undefined };\n                found = true;\n            }\n            if (tile.onBridgeLandType !== undefined) {\n                const bridge = tileOccupation.getBridgeOnTile(tile);\n                if (isPassable(tile, bridge)) {\n                    fallback = fallback ?? { tile, onBridge: bridge };\n                    found = true;\n                }\n            }\n            return (!!found &&\n                this.rangeHelper.isInWeaponRange(obj, targetTile, this.weapon, this.game.rules, tile));\n        });\n        return { reachable: tileFinder.getNextTile(), fallback };\n    }\n    onEnd(obj: any): void {\n        if (obj.isVehicle() && obj.turretTrait) {\n            obj.turretTrait.desiredFacing = obj.direction;\n        }\n        obj.attackTrait.attackState = AttackState.Idle;\n        obj.attackTrait.currentTarget = undefined;\n        const prismType = this.game.rules.general.prism.type;\n        if (obj.isBuilding() &&\n            obj.name === prismType &&\n            this.weapon.type !== WeaponType.Secondary) {\n            this.countSupportBeamsAndFireDownTowers(obj, prismType);\n        }\n        if (this.weapon.rules.limboLaunch) {\n            obj.attackTrait.expirePassiveScanCooldown();\n        }\n        if (obj.isInfantry() || obj.isVehicle()) {\n            obj.isFiring = false;\n        }\n        if (this.weapon.hasBurstsLeft()) {\n            this.weapon.resetBursts();\n        }\n    }\n    forceCancel(obj: any): boolean {\n        if (obj.rules.movementZone !== MovementZone.Fly) {\n            return false;\n        }\n        if (!this.cancellable || this.children.some((task) => !task.cancellable)) {\n            return false;\n        }\n        if (this.status === TaskStatus.Running ||\n            this.status === TaskStatus.Cancelling) {\n            const moveTasks = this.children.filter((task) => task instanceof MoveTask);\n            if (moveTasks.some((task) => !(task as MoveTask).forceCancel(obj))) {\n                return false;\n            }\n            this.onEnd(obj);\n            if (obj.isInfantry() || obj.isVehicle()) {\n                obj.isFiring = false;\n            }\n        }\n        this.status = TaskStatus.Cancelled;\n        return true;\n    }\n    onTick(obj: any): boolean {\n        const attackTrait = obj.attackTrait;\n        if ((obj.isInfantry() || obj.isVehicle()) && attackTrait.attackState !== AttackState.Firing) {\n            obj.isFiring = false;\n        }\n        let targetObj = this.target.obj;\n        const moveTask = this.children.find((task) => task instanceof MoveInWeaponRangeTask) as MoveInWeaponRangeTask | undefined;\n        if (this.isCancelling() && attackTrait.attackState !== AttackState.FireUp) {\n            if (!obj.airSpawnTrait?.isLaunchingMissiles()) {\n                moveTask?.cancel();\n            }\n            return true;\n        }\n        let justFiredUp = false;\n        if (attackTrait.attackState === AttackState.FireUp) {\n            if (attackTrait.isDisabled()) {\n                return true;\n            }\n            attackTrait.attackState = AttackState.Firing;\n            justFiredUp = true;\n        }\n        if (attackTrait.attackState === AttackState.Firing) {\n            if (this.initialIndirectTarget &&\n                !this.game.map\n                    .getObjectsOnTile(this.target.tile)\n                    .find((e: any) => (e.isOverlay() && !e.isBridgePlaceholder()) ||\n                    e.isTerrain())) {\n                this.cancel();\n                return this.onTick(obj);\n            }\n            if (justFiredUp) {\n                const targetOrTile = this.target.obj || this.target.tile;\n                if (!this.game.isValidTarget(this.target.obj) ||\n                    this.shouldDropTarget(this.target.obj) ||\n                    !this.weapon.targeting.canTarget(this.target.obj, this.target.tile, this.game, !!this.options.force, !!this.options.passive) ||\n                    !this.rangeHelper.isInWeaponRange(obj, targetOrTile, this.weapon, this.game.rules) ||\n                    !this.losHelper.hasLineOfSight(obj, targetOrTile, this.weapon)) {\n                    attackTrait.attackState = AttackState.CheckRange;\n                    return this.onTick(obj);\n                }\n            }\n            if (this.weapon.rules.limboLaunch) {\n                if ((targetObj?.isVehicle() || targetObj?.isAircraft()) &&\n                    targetObj.parasiteableTrait?.isInfested()) {\n                    return true;\n                }\n                if (obj.rules.movementZone !== MovementZone.Fly &&\n                    targetObj?.isUnit() &&\n                    targetObj.zone === ZoneType.Air) {\n                    return true;\n                }\n            }\n            if (this.target.tile.onBridgeLandType &&\n                obj.tile.onBridgeLandType &&\n                obj.isUnit() &&\n                (this.game.map.tileOccupation\n                    .getBridgeOnTile(this.target.tile)\n                    ?.isHighBridge() ||\n                    this.game.map.tileOccupation\n                        .getBridgeOnTile(obj.tile)\n                        ?.isHighBridge())) {\n                const targetOnBridge = targetObj\n                    ? targetObj.isUnit() && (targetObj.zone === ZoneType.Air || targetObj.onBridge)\n                    : this.target.isBridge();\n                const selfOnBridge = obj.zone === ZoneType.Air || obj.onBridge;\n                if (targetOnBridge !== selfOnBridge) {\n                    return true;\n                }\n            }\n            let damageMultiplier = 1;\n            const prismType = this.game.rules.general.prism.type;\n            if (obj.isBuilding() &&\n                obj.name === prismType &&\n                this.weapon.type !== WeaponType.Secondary) {\n                const supportCount = this.countSupportBeamsAndFireDownTowers(obj, prismType);\n                damageMultiplier = 1 + supportCount * this.game.rules.general.prism.supportModifier;\n            }\n            if (this.weapon.rules.spawner &&\n                (obj.isVehicle() || obj.isAircraft()) &&\n                obj.parasiteableTrait?.isParalyzed()) {\n                return true;\n            }\n            if (obj.ammo === 0) {\n                if (obj.isAircraft() && (obj.rules.fighter || obj.rules.spawned)) {\n                    moveTask?.cancel();\n                }\n                return true;\n            }\n            let forcedMove = false;\n            if (this.weapon.rules.limboLaunch && moveTask) {\n                if (!moveTask.forceCancel(obj))\n                    return false;\n                obj.moveTrait.lastTargetOffset = undefined;\n                obj.moveTrait.lastVelocity = undefined;\n                forcedMove = true;\n            }\n            this.weapon.fire(this.target, this.game, damageMultiplier);\n            if (forcedMove) {\n                return true;\n            }\n            if (this.weapon.rules.fireOnce) {\n                return true;\n            }\n            if (this.options.passive && obj.rules.distributedFire) {\n                return true;\n            }\n            attackTrait.attackState = AttackState.JustFired;\n            return false;\n        }\n        if (attackTrait.attackState === AttackState.JustFired) {\n            attackTrait.attackState = AttackState.PrepareToFire;\n            return this.onTick(obj);\n        }\n        if (this.needsTargetUpdate) {\n            this.target = this.needsTargetUpdate;\n            targetObj = this.target.obj;\n            this.needsTargetUpdate = undefined;\n            this.onTargetChange(obj);\n            if (!targetObj) {\n                moveTask?.retarget(this.target.tile, !!this.target.getBridge());\n            }\n        }\n        if (targetObj?.isTechno() && targetObj.replacedBy) {\n            const newTarget = this.game.createTarget(targetObj.replacedBy, targetObj.replacedBy.tile);\n            this.target = newTarget;\n            targetObj = targetObj.replacedBy;\n            this.onTargetChange(obj);\n        }\n        let isValidTarget = this.game.isValidTarget(targetObj) && !this.shouldDropTarget(targetObj);\n        if (isValidTarget) {\n            let canTarget = this.weapon.targeting.canTarget(targetObj, this.target.tile, this.game, !!this.options.force, !!this.options.passive);\n            if (!canTarget || !obj.armedTrait.isEquippedWithWeapon(this.weapon)) {\n                const newWeapon = attackTrait.selectWeaponVersus(obj, this.target, this.game, this.options.force, this.options.passive);\n                if (newWeapon) {\n                    this.setWeapon(newWeapon);\n                    if (attackTrait.attackState !== AttackState.CheckRange) {\n                        attackTrait.attackState = AttackState.CheckRange;\n                        return this.onTick(obj);\n                    }\n                    canTarget = true;\n                }\n                else {\n                    canTarget = false;\n                }\n            }\n            isValidTarget = canTarget;\n        }\n        if (isValidTarget) {\n            const lastCheck = this.lastTargetTpCheck;\n            if (targetObj?.isUnit() && lastCheck && targetObj.moveTrait.lastTeleportTick >= lastCheck) {\n                isValidTarget = false;\n                this.rangeCheckCooldown = 0;\n            }\n            else {\n                this.lastTargetTpCheck = this.game.currentTick;\n            }\n        }\n        if (isValidTarget && targetObj) {\n            this.lastValidTargetPosition = {\n                tile: targetObj.tile,\n                onBridge: this.target.getBridge(),\n            };\n        }\n        if (!isValidTarget) {\n            this.targetLinesConfig.isAttack = false;\n        }\n        if (attackTrait.attackState === AttackState.CheckRange) {\n            if (this.rangeCheckCooldown > 0) {\n                this.rangeCheckCooldown--;\n                return false;\n            }\n            const effectiveTarget = this.target.obj\n                ? isValidTarget\n                    ? this.target.obj\n                    : this.lastValidTargetPosition!.tile\n                : this.target.tile;\n            const targetTile = this.target.obj\n                ? isValidTarget\n                    ? this.target.obj.isBuilding()\n                        ? this.target.obj.centerTile\n                        : this.target.obj.tile\n                    : this.lastValidTargetPosition!.tile\n                : this.target.tile;\n            const needsMove = !this.rangeHelper.isInWeaponRange(obj, effectiveTarget, this.weapon, this.game.rules) ||\n                !this.losHelper.hasLineOfSight(obj, effectiveTarget, this.weapon) ||\n                (obj.isUnit() &&\n                    obj.rules.balloonHover &&\n                    !obj.rules.hoverAttack &&\n                    !moveTask &&\n                    obj.tile !== targetTile &&\n                    !this.options.holdGround) ||\n                (obj.isAircraft() &&\n                    this.weapon.projectileRules.iniRot <= 1 &&\n                    !moveTask);\n            if (needsMove) {\n                if (obj.isUnit() && !this.options.holdGround && this.game.map.isWithinBounds(targetTile)) {\n                    if (moveTask) {\n                        if (moveTask.target !== this.target.obj || isValidTarget) {\n                            if (isValidTarget &&\n                                this.target.obj &&\n                                this.rangeHelper.tileDistance(this.target.obj, this.lastSelfMoveTargetTile) > this.weapon.range) {\n                                moveTask.retarget(this.target.obj, !!this.target.getBridge());\n                                this.lastSelfTileBeforeMove = obj.tile;\n                                this.lastSelfMoveTargetTile = this.target.obj?.tile ?? this.target.tile;\n                            }\n                            else {\n                                if (this.options.leashTiles !== undefined &&\n                                    this.rangeHelper.tileDistance(this.initialSelfPosition!.tile, obj.tile) > this.options.leashTiles) {\n                                    moveTask.cancel();\n                                    return true;\n                                }\n                                const targetSpeed = effectiveTarget instanceof GameObject && effectiveTarget.isUnit()\n                                    ? effectiveTarget.moveTrait.baseSpeed\n                                    : 0;\n                                const ticksToWait = Math.ceil((this.rangeHelper.tileDistance(obj, effectiveTarget) -\n                                    (this.weapon.range + 1)) /\n                                    ((obj.moveTrait.baseSpeed + targetSpeed) /\n                                        Coords.LEPTONS_PER_TILE));\n                                if (ticksToWait > 0) {\n                                    this.rangeCheckCooldown = Math.min(GameSpeed.BASE_TICKS_PER_SECOND, ticksToWait);\n                                }\n                            }\n                        }\n                        else {\n                            let fallbackTarget;\n                            if (this.options.leashTiles !== undefined) {\n                                fallbackTarget = this.game.createTarget(this.initialSelfPosition!.onBridge, this.initialSelfPosition!.tile);\n                            }\n                            else {\n                                fallbackTarget = this.game.createTarget(this.lastValidTargetPosition!.onBridge, this.lastValidTargetPosition!.tile);\n                            }\n                            attackTrait.currentTarget = fallbackTarget;\n                            moveTask.retarget(fallbackTarget.tile, fallbackTarget.isBridge());\n                            this.updateTargetLines(fallbackTarget, false);\n                        }\n                        return false;\n                    }\n                    if (!obj.moveTrait || obj.moveTrait.isDisabled()) {\n                        return true;\n                    }\n                    if (this.isCancelling()) {\n                        return true;\n                    }\n                    if (obj.tile === this.lastSelfTileBeforeMove ||\n                        (this.moveExecuted && obj.moveTrait.lastMoveResult === MoveResult.Fail)) {\n                        this.moveAttempts++;\n                    }\n                    else {\n                        this.moveAttempts = 0;\n                    }\n                    if (this.weapon.rules.limboLaunch &&\n                        obj.defaultToGuardArea &&\n                        targetObj &&\n                        this.moveExecuted &&\n                        obj.moveTrait.lastMoveResult === MoveResult.Fail &&\n                        this.rangeHelper.isInRange(obj, targetObj, 0, obj.armedTrait.computeGuardScanRange(this.weapon), true)) {\n                        return true;\n                    }\n                    if (this.moveAttempts > MAX_MOVE_ATTEMPTS) {\n                        return true;\n                    }\n                    if (this.moveAttempts > 0) {\n                        this.children.push(new WaitMinutesTask(1 / 60));\n                    }\n                    const moveTarget = effectiveTarget;\n                    const moveBridge = targetObj && !isValidTarget\n                        ? this.lastValidTargetPosition!.onBridge\n                        : this.target.getBridge();\n                    const newMoveTask = new MoveInWeaponRangeTask(this.game, moveTarget, !!moveBridge, this.weapon);\n                    newMoveTask.blocking = false;\n                    this.children.push(newMoveTask);\n                    this.moveExecuted = true;\n                    this.lastSelfTileBeforeMove = obj.tile;\n                    this.lastSelfMoveTargetTile =\n                        moveTarget instanceof GameObject ? moveTarget.tile : moveTarget;\n                    return this.onTick(obj);\n                }\n                return true;\n            }\n            this.moveExecuted = false;\n            this.moveAttempts = 0;\n            if (moveTask) {\n                const shouldCancelMove = (obj.rules.balloonHover && !obj.rules.hoverAttack) ||\n                    obj.rules.fighter ||\n                    obj.rules.spawned ||\n                    (obj.rules.movementZone === MovementZone.Fly &&\n                        !this.rangeHelper.isInRange2(obj, this.target.obj ?? this.target.tile, this.weapon.minRange, this.weapon.range - 1));\n                if (shouldCancelMove) {\n                    moveTask.cancel();\n                }\n            }\n            if (moveTask && (obj.isInfantry() || this.weapon.rules.spawner)) {\n                return false;\n            }\n            if (moveTask?.children.some((task) => !task.cancellable) &&\n                this.weapon.rules.limboLaunch) {\n                return false;\n            }\n            if (moveTask &&\n                moveTask.shouldAirStrafe(obj) &&\n                this.target.obj?.isUnit() &&\n                this.target.obj.moveTrait.isMoving() &&\n                this.weapon.range > 1 &&\n                !this.rangeHelper.isInRange2(obj, this.target.obj, this.weapon.minRange, this.weapon.range - 1)) {\n                return false;\n            }\n            attackTrait.attackState = AttackState.PrepareToFire;\n        }\n        if (attackTrait.attackState !== AttackState.PrepareToFire) {\n            return false;\n        }\n        if (!isValidTarget || attackTrait.isDisabled()) {\n            moveTask?.cancel();\n            return true;\n        }\n        const targetWorldCoords = this.target.getWorldCoords();\n        const selfWorldPosition = obj.position.worldPosition;\n        if (!(this.lastInRangeTargetPosition.length() &&\n            this.lastInRangeTargetPosition.equals(targetWorldCoords) &&\n            this.lastInRangeSelfPosition.length() &&\n            this.lastInRangeSelfPosition.equals(selfWorldPosition))) {\n            this.lastInRangeTargetPosition.copy(targetWorldCoords);\n            this.lastInRangeSelfPosition.copy(selfWorldPosition);\n            attackTrait.attackState = AttackState.CheckRange;\n            return this.onTick(obj);\n        }\n        if (!(this.weapon.rules.omniFire ||\n            (obj.rules.omniFire && obj.rules.fighter))) {\n            const direction = new Vector3().copy(targetWorldCoords).sub(selfWorldPosition);\n            const desiredFacing = FacingUtil.fromMapCoords(new Vector2(direction.x, direction.z));\n            const facingTolerance = this.weapon.projectileRules.rot ? FACING_TOLERANCE_PROJECTILE : FACING_TOLERANCE;\n            if ((obj.isVehicle() || obj.isBuilding()) && obj.turretTrait) {\n                obj.turretTrait.desiredFacing = desiredFacing;\n                if (Math.abs(desiredFacing - obj.turretTrait.facing) >= facingTolerance) {\n                    return false;\n                }\n            }\n            else if (Math.abs(desiredFacing - obj.direction) >= facingTolerance) {\n                if (obj.isAircraft()) {\n                    obj.direction = FacingUtil.tick(obj.direction, desiredFacing, obj.rules.rot).facing;\n                    return false;\n                }\n                if (moveTask) {\n                    return false;\n                }\n                if (this.options.disallowTurning) {\n                    return true;\n                }\n                if (obj.isVehicle()) {\n                    this.children.push(new TurnTask(desiredFacing));\n                    return false;\n                }\n                obj.direction = desiredFacing;\n            }\n        }\n        if (!this.losHelper.hasLineOfSight(obj, this.target.obj || this.target.tile, this.weapon)) {\n            attackTrait.attackState = AttackState.CheckRange;\n            return this.onTick(obj);\n        }\n        if (attackTrait.isOnCooldown(obj)) {\n            return false;\n        }\n        if (this.weapon.warhead.rules.temporal &&\n            obj.temporalTrait.getTarget() === this.target.obj) {\n            return false;\n        }\n        if (this.weapon.rules.suicide &&\n            this.weapon.type !== WeaponType.DeathWeapon) {\n            this.game.destroyObject(obj, {\n                player: obj.owner,\n                obj: obj,\n                weapon: this.weapon,\n            });\n            return true;\n        }\n        const prismType = this.game.rules.general.prism.type;\n        if (obj.isBuilding() &&\n            obj.name === prismType &&\n            this.weapon.type !== WeaponType.Secondary) {\n            this.fireUpPrismSupportTowers(obj, prismType);\n        }\n        if (obj.isInfantry() || obj.isVehicle()) {\n            obj.isFiring = true;\n        }\n        if (obj.art.fireUp) {\n            this.children.push(new WaitTicksTask(obj.art.fireUp).setCancellable(false));\n            attackTrait.attackState = AttackState.FireUp;\n            return false;\n        }\n        attackTrait.attackState = AttackState.Firing;\n        return this.onTick(obj);\n    }\n    private shouldDropTarget(target: any): boolean {\n        return (this.forceDropTarget ||\n            (target?.isTechno() &&\n                ((this.weapon.rules.limboLaunch &&\n                    (((target.isVehicle() || target.isAircraft()) &&\n                        target.parasiteableTrait?.isInfested()) ||\n                        target.invulnerableTrait.isActive())) ||\n                    (target.warpedOutTrait.isInvulnerable() &&\n                        !this.weapon.warhead.rules.temporal) ||\n                    this.initialTargetOwner !== target.owner)));\n    }\n    private fireUpPrismSupportTowers(obj: any, prismType: string): void {\n        const supportTowers = obj.owner\n            .getOwnedObjectsByType(ObjectType.Building)\n            .filter((building: any) => building.name === prismType &&\n            building.secondaryWeapon &&\n            !building.unitOrderTrait.hasTasks() &&\n            building.attackTrait &&\n            !building.attackTrait.isDisabled() &&\n            !building.attackTrait.isOnCooldown(building))\n            .filter((building: any) => this.rangeHelper.isInWeaponRange(building, obj, building.secondaryWeapon, this.game.rules))\n            .slice(0, this.game.rules.general.prism.supportMax);\n        for (const tower of supportTowers) {\n            tower.unitOrderTrait.addTask(tower.attackTrait.createAttackTask(this.game, obj, obj.centerTile, tower.secondaryWeapon, { passive: true }));\n        }\n    }\n    private countSupportBeamsAndFireDownTowers(obj: any, prismType: string): number {\n        const supportingTowers = obj.owner\n            .getOwnedObjectsByType(ObjectType.Building)\n            .filter((building: any) => building.name === prismType && building.attackTrait?.currentTarget?.obj === obj);\n        for (const tower of supportingTowers) {\n            tower.unitOrderTrait.getCurrentTask()?.cancel();\n        }\n        return Math.min(this.game.rules.general.prism.supportMax, supportingTowers.length);\n    }\n    getTargetLinesConfig(): TargetLinesConfig {\n        return this.targetLinesConfig;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/CaptureBuildingTask.ts",
    "content": "import { Building, BuildStatus } from \"@/game/gameobject/Building\";\nimport { BuildingCaptureEvent } from \"@/game/event/BuildingCaptureEvent\";\nimport { Warhead } from \"@/game/Warhead\";\nimport { CollisionType } from \"@/game/gameobject/unit/CollisionType\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { EnterBuildingTask } from \"@/game/gameobject/task/EnterBuildingTask\";\nexport class CaptureBuildingTask extends EnterBuildingTask {\n    isAllowed(e: any): boolean {\n        return (e.rules.engineer &&\n            this.target.rules.capturable &&\n            !this.target.isDestroyed &&\n            this.target.buildStatus !== BuildStatus.BuildDown &&\n            !this.game.areFriendly(e, this.target));\n    }\n    onEnter(t: any): void {\n        this.game.unspawnObject(t);\n        if (this.game.gameOpts.multiEngineer) {\n            const generalRules = this.game.rules.general;\n            if ((!this.target.rules.needsEngineer || !generalRules.engineerAlwaysCaptureTech) &&\n                this.target.healthTrait.health > 100 * generalRules.engineerCaptureLevel) {\n                let damage = Math.floor(generalRules.engineerDamage * this.target.healthTrait.maxHitPoints);\n                const minHealth = Math.floor((1 - Math.floor(1 / generalRules.engineerDamage) * generalRules.engineerDamage) *\n                    this.target.healthTrait.maxHitPoints);\n                damage = Math.min(damage, this.target.healthTrait.getHitPoints() - minHealth);\n                if (damage > 0) {\n                    const warheadId = this.game.rules.combatDamage.c4Warhead;\n                    const warhead = new Warhead(this.game.rules.getWarhead(warheadId));\n                    warhead.detonate(this.game, damage, this.target.tile, 0, this.target.position.worldPosition, ZoneType.Ground, CollisionType.None, this.game.createTarget(this.target, this.target.tile), { player: t.owner, obj: t, weapon: undefined } as any, false, undefined, 0);\n                    return;\n                }\n            }\n        }\n        t.owner.buildingsCaptured++;\n        this.game.changeObjectOwner(this.target, t.owner);\n        this.game.events.dispatch(new BuildingCaptureEvent(this.target));\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/CheerTask.ts",
    "content": "import { Task } from \"./system/Task\";\nimport { SequenceType } from \"@/game/art/SequenceType\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { WaitMinutesTask } from \"./system/WaitMinutesTask\";\nexport class CheerTask extends Task {\n    private executed: boolean = false;\n    constructor() {\n        super();\n        this.cancellable = false;\n    }\n    onTick(gameObject: any): boolean {\n        if (this.executed) {\n            gameObject.stance = StanceType.None;\n            return true;\n        }\n        if (!gameObject.isInfantry() ||\n            !gameObject.art.sequences.has(SequenceType.Cheer) ||\n            (gameObject.stance !== StanceType.None &&\n                gameObject.stance !== StanceType.Guard)) {\n            return false;\n        }\n        gameObject.stance = StanceType.Cheer;\n        this.children.push(new WaitMinutesTask(1 / 60).setCancellable(false));\n        this.executed = true;\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/EnterBuildingTask.ts",
    "content": "import { Task } from \"./system/Task\";\nimport { MoveOutsideTask } from \"./move/MoveOutsideTask\";\nimport { MoveInsideTask } from \"./move/MoveInsideTask\";\nimport { EnterObjectEvent } from \"@/game/event/EnterObjectEvent\";\nexport class EnterBuildingTask extends Task {\n    protected game: any;\n    public target: any;\n    private aborted: boolean = false;\n    private movePerformed: boolean = false;\n    public preventOpportunityFire: boolean = false;\n    private lastOutsideTile: any;\n    constructor(game: any, target: any) {\n        super();\n        this.game = game;\n        this.target = target;\n    }\n    onTick(gameObject: any): boolean {\n        if ((this.isCancelling() && (!this.movePerformed || this.children.length === 0)) ||\n            this.aborted ||\n            gameObject.moveTrait.isDisabled()) {\n            return true;\n        }\n        if (this.movePerformed && this.children.length) {\n            if (gameObject.tile === this.lastOutsideTile ||\n                this.game.map.tileOccupation.isTileOccupiedBy(gameObject.tile, this.target)) {\n                this.lastOutsideTile = gameObject.tile;\n            }\n            return false;\n        }\n        if (this.game.map.tileOccupation.isTileOccupiedBy(gameObject.tile, this.target)) {\n            if (!this.isAllowed(gameObject) || this.isCancelling()) {\n                this.children.push(new MoveOutsideTask(this.game, this.target, this.lastOutsideTile));\n                this.aborted = true;\n                return false;\n            }\n            this.game.events.dispatch(new EnterObjectEvent(this.target, gameObject));\n            if (this.onEnter(gameObject) === false) {\n                this.children.push(new MoveOutsideTask(this.game, this.target, this.lastOutsideTile));\n                this.aborted = true;\n                return false;\n            }\n        }\n        else if (!this.movePerformed) {\n            this.children.push(new MoveInsideTask(this.game, this.target).setBlocking(false));\n            this.movePerformed = true;\n            this.preventOpportunityFire = true;\n        }\n        return false;\n    }\n    getTargetLinesConfig(gameObject: any) {\n        return { target: this.target, pathNodes: [] };\n    }\n    protected isAllowed(gameObject: any): boolean {\n        return true;\n    }\n    protected onEnter(gameObject: any): any {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/EnterHospitalTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nimport { MoveOutsideTask } from \"@/game/gameobject/task/move/MoveOutsideTask\";\nimport { MoveInsideTask } from \"@/game/gameobject/task/move/MoveInsideTask\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { MovePositionHelper } from \"@/game/gameobject/unit/MovePositionHelper\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { MoveResult } from \"@/game/gameobject/trait/MoveTrait\";\nimport { EnterObjectEvent } from \"@/game/event/EnterObjectEvent\";\nenum EnterHospitalState {\n    MoveToQueueingTile = 0,\n    WaitForTurn = 1,\n    MoveToTarget = 2,\n    EnterTarget = 3,\n    ClearTarget = 4\n}\nexport class EnterHospitalTask extends Task {\n    private game: any;\n    private target: any;\n    private movePerformed: boolean = false;\n    private state: EnterHospitalState;\n    private queueingTile: any;\n    private lastOutsideTile: any;\n    constructor(game: any, target: any) {\n        super();\n        this.game = game;\n        this.target = target;\n    }\n    isAllowed(unit: any): boolean {\n        return (unit.rules.movementZone !== MovementZone.Fly &&\n            unit.healthTrait.health < 100 &&\n            this.target.hospitalTrait &&\n            !this.target.isDestroyed &&\n            !this.target.warpedOutTrait.isActive() &&\n            this.game.areFriendly(unit, this.target) &&\n            (!this.target.ammoTrait || this.target.ammoTrait.ammo > 0));\n    }\n    onStart(unit: any): void {\n        if (!this.target.hospitalTrait) {\n            throw new Error(`Target ${this.target.name} is not a valid hospital`);\n        }\n        if (this.target.hospitalTrait.addToHealQueue(unit) > 0) {\n            this.state = EnterHospitalState.MoveToQueueingTile;\n        }\n        else {\n            this.state = EnterHospitalState.MoveToTarget;\n        }\n    }\n    onEnd(unit: any): void {\n        if (!this.target.isDestroyed && unit.isSpawned) {\n            this.target.hospitalTrait.removeFromHealQueue(unit);\n        }\n    }\n    onTick(unit: any): boolean {\n        if ((this.isCancelling() && this.state !== EnterHospitalState.EnterTarget) ||\n            this.state === EnterHospitalState.ClearTarget ||\n            unit.moveTrait.isDisabled()) {\n            return true;\n        }\n        if (this.state === EnterHospitalState.MoveToQueueingTile) {\n            const movePositionHelper = new MovePositionHelper(this.game.map);\n            const tileFinder = new RadialTileFinder(this.game.map.tiles, this.game.map.mapBounds, this.target.tile, this.target.getFoundation(), 1, 1, (tile: any) => this.game.map.terrain.getPassableSpeed(tile, unit.rules.speedType, unit.isInfantry(), false) > 0 &&\n                movePositionHelper.isEligibleTile(tile, undefined, undefined, this.target.tile));\n            const nextTile = tileFinder.getNextTile();\n            if (!nextTile) {\n                return true;\n            }\n            this.children.push(new MoveTask(this.game, nextTile, false, { closeEnoughTiles: 5 }));\n            this.children.push(new CallbackTask(() => {\n                if (![MoveResult.Success, MoveResult.CloseEnough].includes(unit.moveTrait.lastMoveResult)) {\n                    this.cancel();\n                }\n            }));\n            this.state = EnterHospitalState.WaitForTurn;\n            this.queueingTile = nextTile;\n            return false;\n        }\n        if (this.state === EnterHospitalState.WaitForTurn) {\n            if (!this.target.hospitalTrait.unitIsFirstInHealQueue(unit)) {\n                return false;\n            }\n            this.queueingTile = undefined;\n            this.state = EnterHospitalState.MoveToTarget;\n        }\n        if (this.state === EnterHospitalState.MoveToTarget) {\n            if (this.movePerformed && this.children.length) {\n                if (unit.tile !== this.lastOutsideTile &&\n                    !this.game.map.tileOccupation.isTileOccupiedBy(unit.tile, this.target)) {\n                    this.lastOutsideTile = unit.tile;\n                }\n                return false;\n            }\n            if (!this.isAllowed(unit)) {\n                return true;\n            }\n            if (!this.game.map.tileOccupation.isTileOccupiedBy(unit.tile, this.target)) {\n                if (this.movePerformed) {\n                    return true;\n                }\n                this.children.push(new MoveInsideTask(this.game, this.target).setBlocking(false));\n                this.movePerformed = true;\n                return false;\n            }\n            this.state = EnterHospitalState.EnterTarget;\n        }\n        if (this.state === EnterHospitalState.EnterTarget) {\n            if (!this.isAllowed(unit) || this.isCancelling()) {\n                this.children.push(new MoveOutsideTask(this.game, this.target, this.lastOutsideTile));\n                this.state = EnterHospitalState.ClearTarget;\n                return false;\n            }\n            this.game.limboObject(unit, {\n                selected: false,\n                controlGroup: this.game\n                    .getUnitSelection()\n                    .getOrCreateSelectionModel(unit)\n                    .getControlGroupNumber()\n            });\n            this.target.hospitalTrait.startHealing(unit);\n            this.game.events.dispatch(new EnterObjectEvent(this.target, unit));\n            return true;\n        }\n        return false;\n    }\n    getTargetLinesConfig(unit: any): any {\n        return {\n            target: this.queueingTile ? undefined : this.target,\n            pathNodes: this.queueingTile\n                ? [{ tile: this.queueingTile, onBridge: undefined }]\n                : []\n        };\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/EnterRecyclerTask.ts",
    "content": "import { Building, BuildStatus } from \"@/game/gameobject/Building\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { UnitRecycleEvent } from \"@/game/event/UnitRecycleEvent\";\nimport { EnterBuildingTask } from \"@/game/gameobject/task/EnterBuildingTask\";\nexport class EnterRecyclerTask extends EnterBuildingTask {\n    isAllowed(e: any): boolean {\n        return (e.rules.movementZone !== MovementZone.Fly &&\n            e.rules.locomotor !== LocomotorType.Chrono &&\n            !e.rules.engineer &&\n            this.game.sellTrait.computeRefundValue(e) > 0 &&\n            ((e.isInfantry() && this.target.rules.cloning) ||\n                this.target.rules.grinding) &&\n            !this.target.isDestroyed &&\n            this.target.buildStatus === BuildStatus.Ready &&\n            e.owner === this.target.owner);\n    }\n    onEnter(e: any): void {\n        this.game.sellTrait.sell(e);\n        this.game.events.dispatch(new UnitRecycleEvent(e));\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/EnterTransportTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nimport { MoveOutsideTask } from \"@/game/gameobject/task/move/MoveOutsideTask\";\nimport { MoveInsideTask } from \"@/game/gameobject/task/move/MoveInsideTask\";\nimport { EnterTransportEvent } from \"@/game/event/EnterTransportEvent\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { MoveState, MoveResult } from \"@/game/gameobject/trait/MoveTrait\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { MovePositionHelper } from \"@/game/gameobject/unit/MovePositionHelper\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { EnterObjectEvent } from \"@/game/event/EnterObjectEvent\";\nenum EnterTransportState {\n    MoveToQueueingTile = 0,\n    WaitForTurn = 1,\n    MoveToTransport = 2,\n    EnterTransport = 3,\n    ClearTransport = 4\n}\ninterface QueueingNode {\n    tile: any;\n    onBridge: any;\n}\nexport class EnterTransportTask extends Task {\n    private game: any;\n    public target: any;\n    private movePerformed: boolean = false;\n    private initialTargetTile: any;\n    private state: EnterTransportState;\n    private queueingNode?: QueueingNode;\n    constructor(game: any, target: any) {\n        super();\n        this.game = game;\n        this.target = target;\n        this.preventOpportunityFire = false;\n    }\n    isAllowed(unit: any): boolean {\n        return (!this.target.isDestroyed &&\n            !this.target.isCrashing &&\n            this.game.areFriendly(this.target, unit) &&\n            unit.zone !== ZoneType.Air &&\n            this.target.zone !== ZoneType.Air &&\n            this.target.transportTrait.unitFitsInside(unit) &&\n            this.target.moveTrait.moveState === MoveState.Idle &&\n            !this.target.warpedOutTrait.isActive() &&\n            !unit.mindControllableTrait?.isActive() &&\n            !unit.mindControllerTrait?.isActive());\n    }\n    onStart(unit: any): void {\n        if (!this.target.transportTrait) {\n            throw new Error(`Unit ${this.target.name} is not a valid transport`);\n        }\n        this.initialTargetTile = this.target.tile;\n        if (this.target.transportTrait.addToLoadQueue(unit) > 0) {\n            this.state = EnterTransportState.MoveToQueueingTile;\n        }\n        else {\n            this.state = EnterTransportState.MoveToTransport;\n        }\n    }\n    onEnd(unit: any): void {\n        if (!this.target.isDestroyed) {\n            this.target.transportTrait?.removeFromLoadQueue(unit);\n        }\n    }\n    onTick(unit: any): boolean {\n        if ((this.isCancelling() && this.state !== EnterTransportState.EnterTransport) ||\n            this.state === EnterTransportState.ClearTransport ||\n            unit.moveTrait.isDisabled()) {\n            return true;\n        }\n        if (this.target.tile !== this.initialTargetTile ||\n            this.target.moveTrait.moveState !== MoveState.Idle) {\n            return true;\n        }\n        if (this.state === EnterTransportState.MoveToQueueingTile) {\n            const moveHelper = new MovePositionHelper(this.game.map);\n            const targetBridge = this.target.onBridge\n                ? this.game.map.tileOccupation.getBridgeOnTile(this.target.tile)\n                : undefined;\n            let selectedBridge: any;\n            const queueingTile = new RadialTileFinder(this.game.map.tiles, this.game.map.mapBounds, this.target.tile, this.target.getFoundation(), 1, 1, (tile: any) => {\n                const bridges = [this.game.map.tileOccupation.getBridgeOnTile(tile)];\n                if (bridges[0])\n                    bridges.push(undefined);\n                for (const bridge of bridges) {\n                    if (this.game.map.terrain.getPassableSpeed(tile, unit.rules.speedType, unit.isInfantry(), !!bridge) > 0 &&\n                        moveHelper.isEligibleTile(tile, bridge, targetBridge, this.target.tile)) {\n                        selectedBridge = bridge;\n                        return true;\n                    }\n                }\n                return false;\n            }).getNextTile();\n            if (!queueingTile) {\n                return true;\n            }\n            this.children.push(new MoveTask(this.game, queueingTile, !!selectedBridge, {\n                closeEnoughTiles: 5\n            }));\n            this.children.push(new CallbackTask(() => {\n                if (![MoveResult.Success, MoveResult.CloseEnough].includes(unit.moveTrait.lastMoveResult)) {\n                    this.cancel();\n                }\n            }));\n            this.queueingNode = { tile: queueingTile, onBridge: selectedBridge };\n            this.state = EnterTransportState.WaitForTurn;\n            return false;\n        }\n        if (this.state === EnterTransportState.WaitForTurn) {\n            if (!this.target.transportTrait.unitIsFirstInLoadQueue(unit)) {\n                return false;\n            }\n            this.queueingNode = undefined;\n            this.state = EnterTransportState.MoveToTransport;\n        }\n        if (this.state === EnterTransportState.MoveToTransport) {\n            if (!this.isAllowed(unit)) {\n                return true;\n            }\n            if (!this.game.map.tileOccupation.isTileOccupiedBy(unit.tile, this.target)) {\n                if (this.movePerformed) {\n                    return true;\n                }\n                this.children.push(new MoveInsideTask(this.game, this.target));\n                this.movePerformed = true;\n                this.preventOpportunityFire = true;\n                return false;\n            }\n            this.state = EnterTransportState.EnterTransport;\n        }\n        if (this.state === EnterTransportState.EnterTransport) {\n            if (!this.isAllowed(unit) || this.isCancelling()) {\n                this.children.push(new MoveOutsideTask(this.game, this.target));\n                this.state = EnterTransportState.ClearTransport;\n                return false;\n            }\n            this.game.limboObject(unit, {\n                selected: false,\n                controlGroup: this.game\n                    .getUnitSelection()\n                    .getOrCreateSelectionModel(unit)\n                    .getControlGroupNumber(),\n                inTransport: true\n            });\n            this.game.events.dispatch(new EnterTransportEvent(this.target));\n            this.game.events.dispatch(new EnterObjectEvent(this.target, unit));\n            this.target.transportTrait.units.push(unit);\n            return true;\n        }\n        return false;\n    }\n    getTargetLinesConfig(unit: any) {\n        return {\n            target: this.queueingNode ? undefined : this.target,\n            pathNodes: this.queueingNode ? [this.queueingNode] : []\n        };\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/EvacuateTransportTask.ts",
    "content": "import { LeaveTransportEvent } from '@/game/event/LeaveTransportEvent';\nimport { FacingUtil } from '@/game/gameobject/unit/FacingUtil';\nimport { MovePositionHelper } from '@/game/gameobject/unit/MovePositionHelper';\nimport { MoveTask } from './move/MoveTask';\nimport { ScatterTask } from './ScatterTask';\nimport { Task } from './system/Task';\nimport { TurnTask } from './TurnTask';\nimport { WaitMinutesTask } from './system/WaitMinutesTask';\nimport { ZoneType } from '../unit/ZoneType';\nimport { CallbackTask } from './system/CallbackTask';\nenum EvacuationState {\n    None = 0,\n    OnlyPassengers = 1,\n    All = 2\n}\ninterface EvacTarget {\n    spawnNode: {\n        tile: any;\n        onBridge?: any;\n    };\n    moveNode?: {\n        tile: any;\n        onBridge?: any;\n    };\n    dir: number;\n}\ninterface Unit {\n    position: {\n        tile: any;\n        tileElevation: number;\n    };\n    onBridge: boolean;\n    zone: ZoneType;\n    owner: any;\n    rules: {\n        speedType: any;\n        gunner?: boolean;\n    };\n    unitOrderTrait: {\n        unmarkNextQueuedOrder(): void;\n        addTask(task: Task): void;\n    };\n    isInfantry(): boolean;\n}\ninterface Transport {\n    name: string;\n    tile: any;\n    tileElevation: number;\n    onBridge: boolean;\n    zone: ZoneType;\n    direction: number;\n    rules: {\n        gunner?: boolean;\n    };\n    transportTrait: {\n        units: Unit[];\n    };\n}\ninterface Game {\n    map: {\n        getTileZone(tile: any, onGround: boolean): ZoneType;\n        tiles: {\n            getByMapCoords(x: number, y: number): any;\n        };\n        mapBounds: {\n            isWithinBounds(tile: any): boolean;\n        };\n        terrain: {\n            getPassableSpeed(tile: any, speedType: any, isInfantry: boolean, onBridge: boolean): number;\n            findObstacles(position: {\n                tile: any;\n                onBridge?: any;\n            }, unit: Unit): any[];\n        };\n        tileOccupation: {\n            getBridgeOnTile(tile: any): any;\n        };\n    };\n    events: {\n        dispatch(event: any): void;\n    };\n    destroyObject(obj: any, options: {\n        player: any;\n    }): void;\n    unlimboObject(obj: any, tile: any): void;\n}\nexport class EvacuateTransportTask extends Task {\n    private game: Game;\n    private soft: boolean;\n    private evacState: EvacuationState = EvacuationState.None;\n    private evacTries: number = 0;\n    private turnPerformed: boolean = false;\n    public preventLanding: boolean = false;\n    constructor(game: Game, soft: boolean) {\n        super();\n        this.game = game;\n        this.soft = soft;\n    }\n    forceEvac(): void {\n        this.evacState = EvacuationState.All;\n    }\n    onStart(transport: Transport): void {\n        if (!transport.transportTrait) {\n            throw new Error(`Object \"${transport.name}\" is not a valid transport`);\n        }\n        const transportTrait = transport.transportTrait;\n        if (transportTrait.units.length > 0) {\n            this.evacState = (this.evacState !== EvacuationState.OnlyPassengers &&\n                transportTrait.units.length !== 1) ||\n                !transport.rules.gunner\n                ? EvacuationState.OnlyPassengers\n                : EvacuationState.All;\n        }\n    }\n    onTick(transport: Transport): boolean {\n        if (this.isCancelling() || this.evacState === EvacuationState.None) {\n            return true;\n        }\n        if (transport.zone === ZoneType.Air) {\n            this.children.push(new CallbackTask(() => transport.zone !== ZoneType.Air));\n            return false;\n        }\n        const units = transport.transportTrait.units;\n        if (!units.length ||\n            (transport.rules.gunner && units.length === 1 && this.evacState !== EvacuationState.All)) {\n            return true;\n        }\n        const unitToEvacuate = units[units.length - 1];\n        const evacTarget = this.findValidEvacTarget(transport, unitToEvacuate);\n        if (evacTarget && !this.turnPerformed) {\n            this.turnPerformed = true;\n            const targetDirection = (evacTarget.dir + 180) % 360;\n            if (transport.direction !== targetDirection) {\n                this.children.push(new TurnTask(targetDirection));\n                return false;\n            }\n        }\n        if (this.evacuateUnit(unitToEvacuate, transport, evacTarget)) {\n            units.pop();\n            this.children.push(new WaitMinutesTask(1 / 60));\n            return false;\n        }\n        if (++this.evacTries <= 3) {\n            this.children.push(new WaitMinutesTask(0.05));\n            return false;\n        }\n        return true;\n    }\n    private evacuateUnit(unit: Unit, transport: Transport, evacTarget?: EvacTarget): boolean {\n        if (!evacTarget) {\n            if (!this.soft) {\n                unit.position.tile = transport.tile;\n                unit.position.tileElevation = transport.tileElevation;\n                unit.onBridge = transport.onBridge;\n                unit.zone = transport.zone;\n                this.game.destroyObject(unit, { player: unit.owner });\n                return true;\n            }\n            return false;\n        }\n        const { spawnNode, moveNode } = evacTarget;\n        unit.position.tileElevation = spawnNode.onBridge?.tileElevation ?? 0;\n        unit.onBridge = !!spawnNode.onBridge;\n        unit.zone = this.game.map.getTileZone(spawnNode.tile, !spawnNode.onBridge);\n        this.game.unlimboObject(unit, spawnNode.tile);\n        unit.unitOrderTrait.unmarkNextQueuedOrder();\n        if (moveNode) {\n            unit.unitOrderTrait.addTask(new MoveTask(this.game as any, moveNode.tile, !!moveNode.onBridge));\n        }\n        else {\n            unit.unitOrderTrait.addTask(new ScatterTask(this.game as any, undefined as any, undefined as any));\n        }\n        this.game.events.dispatch(new LeaveTransportEvent(transport));\n        return true;\n    }\n    private findValidEvacTarget(transport: Transport, unit: Unit): EvacTarget | undefined {\n        const map = this.game.map;\n        const moveHelper = new MovePositionHelper(map as any);\n        const bridge = transport.onBridge\n            ? map.tileOccupation.getBridgeOnTile(transport.tile)\n            : undefined;\n        const baseDirection = (transport.direction + 180) % 360;\n        let fallbackTarget: EvacTarget | undefined;\n        for (let angleOffset = 0; angleOffset <= 180; angleOffset += 45) {\n            const directions = angleOffset && angleOffset < 180\n                ? [baseDirection + angleOffset, baseDirection - angleOffset]\n                : [baseDirection];\n            for (const direction of directions) {\n                const mapCoords = FacingUtil.toMapCoords(direction);\n                let currentTile = transport.tile;\n                let currentBridge = bridge;\n                let intermediateNode: {\n                    tile: any;\n                    onBridge?: any;\n                } | undefined;\n                for (let distance = 1; distance <= 2; distance++) {\n                    if (distance === 2) {\n                        if (!intermediateNode)\n                            break;\n                        currentTile = intermediateNode.tile;\n                        currentBridge = intermediateNode.onBridge;\n                    }\n                    const targetX = transport.tile.rx + Math.sign(mapCoords.x) * distance;\n                    const targetY = transport.tile.ry + Math.sign(mapCoords.y) * distance;\n                    const targetTile = map.tiles.getByMapCoords(targetX, targetY);\n                    if (!targetTile || !map.mapBounds.isWithinBounds(targetTile)) {\n                        break;\n                    }\n                    const bridgeOptions = [map.tileOccupation.getBridgeOnTile(targetTile)];\n                    if (bridgeOptions[0]) {\n                        bridgeOptions.push(undefined);\n                    }\n                    for (const bridgeOption of bridgeOptions) {\n                        if (this.isValidEvacPosition(targetTile, bridgeOption, currentBridge, currentTile, unit)) {\n                            if (distance === 1) {\n                                intermediateNode = { tile: targetTile, onBridge: bridgeOption };\n                                fallbackTarget = {\n                                    spawnNode: intermediateNode,\n                                    moveNode: undefined,\n                                    dir: direction\n                                };\n                            }\n                            else {\n                                return {\n                                    spawnNode: intermediateNode!,\n                                    moveNode: { tile: targetTile, onBridge: bridgeOption },\n                                    dir: direction\n                                };\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        return fallbackTarget;\n    }\n    private isValidEvacPosition(tile: any, onBridge: any, fromBridge: any, fromTile: any, unit: Unit): boolean {\n        const map = this.game.map;\n        return map.terrain.getPassableSpeed(tile, unit.rules.speedType, unit.isInfantry(), !!onBridge) > 0 &&\n            new MovePositionHelper(map as any).isEligibleTile(tile, onBridge, fromBridge, fromTile) &&\n            !map.terrain.findObstacles({ tile, onBridge }, unit).length;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/GarrisonBuildingTask.ts",
    "content": "import { BuildingGarrisonEvent } from \"@/game/event/BuildingGarrisonEvent\";\nimport { EnterBuildingTask } from \"@/game/gameobject/task/EnterBuildingTask\";\nexport class GarrisonBuildingTask extends EnterBuildingTask {\n    isAllowed(e: any): boolean {\n        return (!this.target.isDestroyed &&\n            !!this.target.garrisonTrait?.canBeOccupied() &&\n            this.target.garrisonTrait.units.length <\n                this.target.garrisonTrait.maxOccupants &&\n            !(this.target.garrisonTrait.units.length &&\n                this.target.garrisonTrait.units[0].owner !== e.owner) &&\n            !e.mindControllableTrait?.isActive());\n    }\n    onEnter(e: any): void {\n        this.game.limboObject(e, {\n            selected: false,\n            controlGroup: this.game\n                .getUnitSelection()\n                .getOrCreateSelectionModel(e)\n                .getControlGroupNumber(),\n        });\n        let t = this.target.garrisonTrait;\n        if (!t.units.length) {\n            e.owner.buildingsCaptured++;\n            this.game.changeObjectOwner(this.target, e.owner);\n            this.game.events.dispatch(new BuildingGarrisonEvent(this.target));\n        }\n        t.units.push(e);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/InfiltrateBuildingTask.ts",
    "content": "import { Building, BuildStatus } from \"@/game/gameobject/Building\";\nimport { BuildingInfiltrationEvent } from \"@/game/event/BuildingInfiltrationEvent\";\nimport { EnterBuildingTask } from \"@/game/gameobject/task/EnterBuildingTask\";\nexport class InfiltrateBuildingTask extends EnterBuildingTask {\n    isAllowed(e: any): boolean {\n        return (e.rules.infiltrate &&\n            this.target.rules.spyable &&\n            !this.target.isDestroyed &&\n            this.target.buildStatus !== BuildStatus.BuildDown &&\n            !this.game.areFriendly(e, this.target));\n    }\n    onEnter(e: any): void {\n        this.game.unspawnObject(e);\n        e.agentTrait?.infiltrate(e, this.target, this.game);\n        this.game.events.dispatch(new BuildingInfiltrationEvent(this.target, e));\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/MoveToDockTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { WaitMinutesTask } from \"@/game/gameobject/task/system/WaitMinutesTask\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { MoveResult } from \"@/game/gameobject/trait/MoveTrait\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { Coords } from \"@/game/Coords\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nenum DockingStatus {\n    Idle = 0,\n    MoveToQueueingTile = 1,\n    WaitForTurn = 2,\n    MoveToDock = 3,\n    Docking = 4,\n    Docked = 5\n}\nexport class MoveToDockTask extends Task {\n    private game: any;\n    private target: any;\n    public useChildTargetLines: boolean = true;\n    public preventOpportunityFire: boolean = false;\n    private dockingStatus: DockingStatus = DockingStatus.Idle;\n    constructor(game: any, target: any) {\n        super();\n        this.game = game;\n        this.target = target;\n    }\n    onStart(unit: any): void {\n        if (!this.target.dockTrait) {\n            throw new Error(`Target object \"${this.target.name}\" is not a valid dock`);\n        }\n        if (this.target.dockTrait.hasReservedDockForUnit(unit)) {\n            this.dockingStatus = DockingStatus.MoveToDock;\n        }\n        else {\n            const availableDockNumber = this.target.dockTrait.getFirstAvailableDockNumber();\n            if (availableDockNumber !== undefined) {\n                this.target.dockTrait.reserveDockAt(unit, availableDockNumber);\n                this.dockingStatus = DockingStatus.MoveToDock;\n            }\n            else if (this.target.helipadTrait) {\n                this.cancel();\n            }\n            else {\n                this.dockingStatus = DockingStatus.MoveToQueueingTile;\n            }\n        }\n    }\n    onEnd(unit: any): void {\n        if (this.dockingStatus !== DockingStatus.Docked && this.target.isSpawned) {\n            this.target.dockTrait.undockUnit(unit);\n            this.target.dockTrait.unreserveDockForUnit(unit);\n        }\n        this.dockingStatus = DockingStatus.Idle;\n    }\n    onTick(unit: any): boolean {\n        if (this.isCancelling())\n            return true;\n        if (!this.isValidTarget(this.target, unit))\n            return true;\n        if (this.dockingStatus === DockingStatus.MoveToQueueingTile) {\n            const queueingTile = this.findReachableQueueingTile(unit);\n            if (!queueingTile)\n                return true;\n            if (unit.tile !== queueingTile) {\n                this.children.push(new MoveTask(this.game, queueingTile, false, {\n                    closeEnoughTiles: 5,\n                }), new CallbackTask(() => {\n                    if (unit.moveTrait.lastMoveResult === MoveResult.Fail) {\n                        this.cancel();\n                    }\n                    else if (unit.moveTrait.lastMoveResult === MoveResult.CloseEnough) {\n                        if (!this.game.map.tileOccupation.isTileOccupiedBy(unit.tile, this.target)) {\n                            this.dockingStatus = DockingStatus.WaitForTurn;\n                        }\n                    }\n                }));\n                return false;\n            }\n            this.dockingStatus = DockingStatus.WaitForTurn;\n        }\n        if (this.dockingStatus === DockingStatus.WaitForTurn) {\n            const availableDockNumber = this.target.dockTrait.getFirstAvailableDockNumber();\n            if (availableDockNumber === undefined) {\n                this.children.push(new WaitMinutesTask(1 / 60));\n                return false;\n            }\n            this.target.dockTrait.reserveDockAt(unit, availableDockNumber);\n            this.dockingStatus = DockingStatus.MoveToDock;\n        }\n        if (this.dockingStatus === DockingStatus.MoveToDock) {\n            const reservedDock = this.target.dockTrait.getReservedDockForUnit(unit);\n            const dockTile = this.target.dockTrait.getDockTile(reservedDock);\n            const dockOffset = Coords.vecWorldToGround(this.target.dockTrait.getDockOffset(reservedDock))\n                .add(this.target.position.getMapPosition())\n                .sub(new Vector2(dockTile.rx, dockTile.ry).multiplyScalar(Coords.LEPTONS_PER_TILE));\n            if (unit.tile !== dockTile) {\n                this.children.push(new MoveTask(this.game, dockTile, false, {\n                    targetOffset: unit.isAircraft() ? dockOffset : undefined,\n                    closeEnoughTiles: 0,\n                    strictCloseEnough: true,\n                }), new CallbackTask(() => {\n                    if (unit.moveTrait.lastMoveResult === MoveResult.Fail) {\n                        this.cancel();\n                    }\n                }));\n                this.game.afterTick(() => unit.unitOrderTrait[NotifyTick.onTick](unit, this.game));\n                return false;\n            }\n            this.dockingStatus = DockingStatus.Docking;\n        }\n        if (this.dockingStatus !== DockingStatus.Docking)\n            return false;\n        const reservedDock = this.target.dockTrait.getReservedDockForUnit(unit);\n        this.target.dockTrait.unreserveDockForUnit(unit);\n        this.target.dockTrait.dockUnitAt(unit, reservedDock);\n        if (unit.isAircraft() && unit.airportBoundTrait && this.target.helipadTrait) {\n            unit.airportBoundTrait.preferredAirport = this.target;\n        }\n        this.dockingStatus = DockingStatus.Docked;\n        return true;\n    }\n    private isValidTarget(target: any, unit: any): boolean {\n        return target.isSpawned && this.game.areFriendly(target, unit);\n    }\n    private findReachableQueueingTile(unit: any): any {\n        const foundation = this.target.getFoundation();\n        const targetPosition = new Vector2(this.target.tile.rx + foundation.width, this.target.tile.ry + foundation.height);\n        const targetTile = this.game.map.tiles.getByMapCoords(targetPosition.x, targetPosition.y);\n        if (targetTile && this.isValidQueueingTile(targetTile, unit)) {\n            return targetTile;\n        }\n        return new RadialTileFinder(this.game.map.tiles, this.game.map.mapBounds, this.target.tile, this.target.getFoundation(), 1, 1, (tile: any) => this.isValidQueueingTile(tile, unit)).getNextTile();\n    }\n    private isValidQueueingTile(tile: any, unit: any): boolean {\n        const isFlying = unit.rules.movementZone === MovementZone.Fly;\n        const speedType = unit.rules.speedType;\n        const isInfantry = unit.isInfantry();\n        let islandIdMap: any = undefined;\n        if (!isFlying && this.game.map.terrain.getPassableSpeed(unit.tile, speedType, isInfantry, unit.onBridge)) {\n            islandIdMap = this.game.map.terrain.getIslandIdMap(speedType, isInfantry);\n        }\n        const isReachable = isFlying || (islandIdMap?.get(tile, false) === islandIdMap?.get(unit.tile, unit.onBridge) &&\n            Math.abs(tile.z - this.target.tile.z) < 2 &&\n            !tile.onBridgeLandType &&\n            !this.game.map.terrain.findObstacles({ tile: tile, onBridge: undefined }, unit).length);\n        return isReachable && !this.game.map.tileOccupation.isTileOccupiedBy(tile, this.target);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/ParadropTask.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { Vector3 } from \"@/game/math/Vector3\";\nimport { InfDeathType } from \"@/game/gameobject/infantry/InfDeathType\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { Task } from \"@/game/gameobject/task/system/Task\";\nexport class ParadropTask extends Task {\n    private game: any;\n    constructor(game: any) {\n        super();\n        this.game = game;\n    }\n    onTick(e: any): boolean {\n        const fallRate = Math.abs(this.game.rules.general.parachuteMaxFallRate);\n        const bridgeElevation = e.tile.onBridgeLandType\n            ? this.game.map.tileOccupation.getBridgeOnTile(e.tile).tileElevation\n            : 0;\n        const bridgeHeight = Coords.tileHeightToWorld(bridgeElevation);\n        const currentElevation = e.tileElevation;\n        const currentHeight = Coords.tileHeightToWorld(currentElevation);\n        if (bridgeHeight < Math.max(bridgeHeight, currentHeight - fallRate)) {\n            e.position.moveByLeptons3(new Vector3(0, -fallRate, 0));\n            e.moveTrait.handleElevationChange(currentElevation, this.game);\n            return false;\n        }\n        e.position.tileElevation = bridgeElevation;\n        e.stance = StanceType.None;\n        if (!this.game.map.terrain.getPassableSpeed(e.tile, e.rules.speedType, e.isInfantry(), e.onBridge)) {\n            e.infDeathType = InfDeathType.None;\n            this.game.destroyObject(e, undefined, true);\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/PlantC4Task.ts",
    "content": "import { GameSpeed } from \"@/game/GameSpeed\";\nimport { EnterObjectEvent } from \"@/game/event/EnterObjectEvent\";\nimport { EnterBuildingTask } from \"@/game/gameobject/task/EnterBuildingTask\";\nexport class PlantC4Task extends EnterBuildingTask {\n    isAllowed(e: any): boolean {\n        return (!this.target.isDestroyed &&\n            !this.target.invulnerableTrait.isActive());\n    }\n    onEnter(e: any): boolean {\n        const chargeTime = Math.floor(60 *\n            this.game.rules.combatDamage.c4Delay *\n            GameSpeed.BASE_TICKS_PER_SECOND);\n        this.target.c4ChargeTrait.setCharge(chargeTime, {\n            player: e.owner,\n            obj: e,\n        });\n        this.game.events.dispatch(new EnterObjectEvent(this.target, e));\n        return false;\n    }\n    getTargetLinesConfig(e: any): {\n        target: any;\n        pathNodes: any[];\n        isAttack: boolean;\n    } {\n        return { target: this.target, pathNodes: [], isAttack: true };\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/RepairBuildingTask.ts",
    "content": "import { BuildingRepairFullEvent } from \"@/game/event/BuildingRepairFullEvent\";\nimport { BridgeRepairEvent } from \"@/game/event/BridgeRepairEvent\";\nimport { EnterBuildingTask } from \"@/game/gameobject/task/EnterBuildingTask\";\nexport class RepairBuildingTask extends EnterBuildingTask {\n    isAllowed(e: any): boolean {\n        return this.target.cabHutTrait\n            ? this.target.cabHutTrait.canRepairBridge()\n            : e.rules.engineer &&\n                !this.target.isDestroyed &&\n                this.target.rules.repairable &&\n                this.target.healthTrait.health < 100 &&\n                ((!this.target.owner.isCombatant() &&\n                    !!this.target.garrisonTrait) ||\n                    this.game.areFriendly(e, this.target));\n    }\n    onEnter(e: any): void {\n        this.game.unspawnObject(e);\n        if (this.target.cabHutTrait) {\n            this.target.cabHutTrait.repairBridge(this.game, e.owner);\n            this.game.events.dispatch(new BridgeRepairEvent(e.owner, this.target.centerTile));\n        }\n        else {\n            this.target.healthTrait.healToFull(e, this.game);\n            this.game.events.dispatch(new BuildingRepairFullEvent(this.target, e.owner));\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/ScatterTask.ts",
    "content": "import { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { Task } from \"@/game/gameobject/task/system/Task\";\nimport { ScatterPositionHelper } from \"@/game/gameobject/unit/ScatterPositionHelper\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nexport class ScatterTask extends Task {\n    private game: any;\n    private target: any;\n    private options: any;\n    constructor(game: any, target?: any, options?: any) {\n        super();\n        this.game = game;\n        this.target = target;\n        this.options = options;\n    }\n    onStart(unit: any): void {\n        if (!unit.moveTrait.isDisabled() &&\n            unit.rules.movementZone !== MovementZone.Fly) {\n            let tile: any, toBridge: boolean;\n            if (this.target) {\n                ({ tile, toBridge } = this.target);\n            }\n            else {\n                const position = new ScatterPositionHelper(this.game)\n                    .findPositions([unit], this.options)\n                    .get(unit);\n                if (!position)\n                    return;\n                tile = position.tile;\n                toBridge = !!position.onBridge;\n            }\n            this.children.push(new MoveTask(this.game, tile, toBridge, {\n                closeEnoughTiles: 0,\n                ignoredBlockers: this.options?.ignoredBlockers,\n            }));\n        }\n    }\n    onTick(unit: any): boolean {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/TurnTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nimport { FacingUtil } from \"@/game/gameobject/unit/FacingUtil\";\nexport class TurnTask extends Task {\n    private direction: number;\n    public cancellable: boolean = false;\n    constructor(direction: number) {\n        super();\n        this.direction = direction;\n    }\n    onTick(entity: any): boolean {\n        if (entity.direction === this.direction) {\n            entity.spinVelocity = 0;\n            return true;\n        }\n        const rotationSpeed = entity.rules.rot;\n        const { facing, delta } = FacingUtil.tick(entity.direction, this.direction, rotationSpeed);\n        entity.direction = facing;\n        entity.spinVelocity = delta;\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/WaitForBuildUpTask.ts",
    "content": "import { Building, BuildStatus } from \"@/game/gameobject/Building\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { TaskGroup } from \"@/game/gameobject/task/system/TaskGroup\";\nimport { WaitMinutesTask } from \"@/game/gameobject/task/system/WaitMinutesTask\";\nexport class WaitForBuildUpTask extends TaskGroup {\n    public cancellable: boolean = false;\n    constructor(buildTime: number, game: any) {\n        super(new WaitMinutesTask(buildTime), new CallbackTask((building) => {\n            building.setBuildStatus(BuildStatus.Ready, game);\n        }));\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/harvester/GatherOreTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { HarvesterTrait, HarvesterStatus } from \"@/game/gameobject/trait/HarvesterTrait\";\nimport { LandType } from \"@/game/type/LandType\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { TiberiumTrait } from \"@/game/gameobject/trait/TiberiumTrait\";\nimport { TiberiumType } from \"@/engine/type/TiberiumType\";\nimport { WaitMinutesTask } from \"@/game/gameobject/task/system/WaitMinutesTask\";\nimport { ReturnOreTask } from \"@/game/gameobject/task/harvester/ReturnOreTask\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { MoveResult } from \"@/game/gameobject/trait/MoveTrait\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nconst PRIORITY_MATRIX = [\n    [8, 5, 6],\n    [3, 0, 2],\n    [7, 4, 1],\n];\nexport class GatherOreTask extends Task {\n    private game: any;\n    private initialTarget: any;\n    private explicitOrder: boolean;\n    private forceMoveTried: boolean = false;\n    private rangeHelper: RangeHelper;\n    private scanNearRadius: number;\n    private scanFarRadius: number;\n    private target?: any;\n    public useChildTargetLines: boolean = true;\n    public preventOpportunityFire: boolean = false;\n    constructor(game: any, initialTarget?: any, explicitOrder: boolean = false) {\n        super();\n        this.game = game;\n        this.initialTarget = initialTarget;\n        this.explicitOrder = explicitOrder;\n        this.rangeHelper = new RangeHelper(game.map.tileOccupation);\n        this.scanNearRadius = game.rules.ai.tiberiumNearScan;\n        this.scanFarRadius = game.rules.ai.tiberiumFarScan;\n    }\n    onStart(unit: any): void {\n        if (!unit.isVehicle() || !unit.harvesterTrait) {\n            throw new Error(`Unit ${unit.name} is not a harvester.`);\n        }\n        unit.harvesterTrait.status = HarvesterStatus.MovingToOreSite;\n        unit.harvesterTrait.lastGatherExplicit = this.explicitOrder;\n    }\n    onEnd(unit: any): void {\n        if (unit.harvesterTrait.status !== HarvesterStatus.LookingForOreSite) {\n            unit.harvesterTrait.status = HarvesterStatus.Idle;\n        }\n    }\n    onTick(unit: any): boolean {\n        if (this.isCancelling())\n            return true;\n        const harvester = unit.harvesterTrait;\n        if (harvester.status === HarvesterStatus.MovingToOreSite) {\n            const previousTarget = this.target;\n            this.target = this.findClosestReachableOreSite(unit, this.target ||\n                (this.initialTarget?.landType !== LandType.Tiberium\n                    ? (harvester.lastOreSite ?? unit.tile)\n                    : this.initialTarget), true);\n            harvester.lastOreSite = this.target;\n            if (!this.target) {\n                harvester.status = HarvesterStatus.LookingForOreSite;\n                const refinery = this.getRefineryOnTile(unit.tile);\n                if (refinery && unit.unitOrderTrait.getTasks().length === 1) {\n                    const canFly = unit.rules.movementZone === MovementZone.Fly;\n                    const finder = new RadialTileFinder(this.game.map.tiles, this.game.map.mapBounds, refinery.tile, refinery.getFoundation(), 1, 5, (tile: any) => canFly || (this.game.map.terrain.getPassableSpeed(tile, unit.rules.speedType, unit.isInfantry(), false) > 0 &&\n                        Math.abs(tile.z - unit.tile.z) < 2 &&\n                        !this.game.map.terrain.findObstacles({ tile, onBridge: undefined }, unit).length));\n                    const waitTile = finder.getNextTile();\n                    if (waitTile) {\n                        unit.unitOrderTrait.addTasks(new MoveTask(this.game, waitTile, false), new CallbackTask(() => {\n                            if (![MoveResult.Success, MoveResult.CloseEnough, MoveResult.Cancel]\n                                .includes(unit.moveTrait.lastMoveResult)) {\n                                this.children.push(new WaitMinutesTask(1 / 60));\n                            }\n                        }));\n                    }\n                }\n                return true;\n            }\n            const closeEnough = this.game.rules.general.closeEnough;\n            const wasCloseEnough = previousTarget &&\n                this.rangeHelper.tileDistance(unit.tile, this.target) <= closeEnough;\n            if (!(unit.tile === this.target ||\n                (unit.tile.landType === LandType.Tiberium && wasCloseEnough))) {\n                if (unit.tile !== this.target && wasCloseEnough &&\n                    unit.tile.landType !== LandType.Tiberium) {\n                    const nearbyOre = this.findClosestReachableOreSite(unit, unit.tile, false, true);\n                    if (nearbyOre) {\n                        this.target = nearbyOre;\n                        harvester.lastOreSite = this.target;\n                    }\n                    else {\n                        if (!this.forceMoveTried) {\n                            this.forceMoveTried = true;\n                            this.children.push(new MoveTask(this.game, this.target, false, {\n                                closeEnoughTiles: 0,\n                                strictCloseEnough: true\n                            }));\n                            return false;\n                        }\n                        this.forceMoveTried = false;\n                        if (!harvester.isEmpty()) {\n                            this.returnOreIfPossible(unit);\n                            return true;\n                        }\n                        const alternativeTarget = this.findClosestReachableOreSite(unit, unit.tile, true, true);\n                        if (!alternativeTarget) {\n                            harvester.status = HarvesterStatus.LookingForOreSite;\n                            return true;\n                        }\n                        this.target = alternativeTarget;\n                        harvester.lastOreSite = this.target;\n                    }\n                }\n                this.children.push(new MoveTask(this.game, this.target, false, {\n                    closeEnoughTiles: closeEnough\n                }), new CallbackTask(() => {\n                    if (![MoveResult.Success, MoveResult.CloseEnough, MoveResult.Cancel]\n                        .includes(unit.moveTrait.lastMoveResult)) {\n                        this.children.push(new WaitMinutesTask(5 / 60));\n                    }\n                }));\n                return false;\n            }\n            this.target = unit.tile;\n            harvester.lastOreSite = this.target;\n            harvester.status = HarvesterStatus.Harvesting;\n            this.forceMoveTried = false;\n        }\n        if (harvester.status !== HarvesterStatus.Harvesting) {\n            return false;\n        }\n        if (harvester.isFull()) {\n            this.returnOreIfPossible(unit);\n            return true;\n        }\n        const tiberiumOverlay = this.game.map\n            .getObjectsOnTile(unit.tile)\n            .find((obj: any) => obj.isOverlay() && obj.isTiberium());\n        if (!tiberiumOverlay) {\n            const hasNearbyOre = this.findClosestReachableOreSite(unit, harvester.lastOreSite ?? unit.tile, false);\n            if (hasNearbyOre || harvester.isEmpty()) {\n                harvester.status = HarvesterStatus.MovingToOreSite;\n                return this.onTick(unit);\n            }\n            else {\n                this.returnOreIfPossible(unit);\n                return true;\n            }\n        }\n        const tiberiumTrait = tiberiumOverlay.traits.get(TiberiumTrait);\n        const collectedType = tiberiumTrait.collectBail();\n        if (!tiberiumTrait.getBailCount()) {\n            this.game.unspawnObject(tiberiumOverlay);\n        }\n        if (collectedType !== undefined) {\n            if (collectedType === TiberiumType.Ore) {\n                harvester.ore++;\n            }\n            else if (collectedType === TiberiumType.Gems) {\n                harvester.gems++;\n            }\n            else {\n                throw new Error(\"Unsupported tiberium type \" + collectedType);\n            }\n        }\n        const hasRefinery = [...unit.owner.buildings].some((building: any) => building.rules.refinery);\n        if (!hasRefinery && !this.explicitOrder) {\n            return true;\n        }\n        this.children.push(new WaitMinutesTask(1 / 60));\n        return false;\n    }\n    private findClosestReachableOreSite(unit: any, startTile: any, farScan: boolean, checkObstacles: boolean = false): any {\n        const canFly = unit.rules.movementZone === MovementZone.Fly;\n        const speedType = unit.rules.speedType;\n        const isInfantry = unit.isInfantry();\n        const islandMap = !canFly &&\n            this.game.map.terrain.getPassableSpeed(unit.tile, speedType, isInfantry, unit.onBridge)\n            ? this.game.map.terrain.getIslandIdMap(speedType, isInfantry)\n            : undefined;\n        const unitIslandId = islandMap?.get(unit.tile, unit.onBridge);\n        const isValidTile = (tile: any): boolean => {\n            return tile.landType === LandType.Tiberium &&\n                islandMap?.get(tile, false) === unitIslandId &&\n                (!checkObstacles || canFly ||\n                    !this.game.map.terrain.findObstacles({ tile, onBridge: undefined }, unit).length);\n        };\n        if (isValidTile(startTile)) {\n            return startTile;\n        }\n        let startRadius = 1;\n        if (!farScan) {\n            const nearbyFinder = new RadialTileFinder(this.game.map.tiles, this.game.map.mapBounds, startTile, { width: 1, height: 1 }, startRadius, startRadius, isValidTile);\n            const nearbyTiles: any[] = [];\n            let tile;\n            while ((tile = nearbyFinder.getNextTile())) {\n                nearbyTiles.push(tile);\n            }\n            if (nearbyTiles.length) {\n                const tilesWithOre = nearbyTiles.map(tile => {\n                    const ore = this.game.map\n                        .getObjectsOnTile(tile)\n                        .find((obj: any) => obj.isOverlay() && obj.isTiberium());\n                    if (!ore) {\n                        throw new Error(`Ore should exist on tile ${tile.rx},${tile.ry} b/c of landType`);\n                    }\n                    return { tile, ore };\n                });\n                tilesWithOre.sort((a, b) => {\n                    const valueDiff = b.ore.value - a.ore.value;\n                    const priorityA = PRIORITY_MATRIX[1 + a.tile.ry - startTile.ry][1 + a.tile.rx - startTile.rx];\n                    const priorityB = PRIORITY_MATRIX[1 + b.tile.ry - startTile.ry][1 + b.tile.rx - startTile.rx];\n                    return 1000 * valueDiff + (priorityB - priorityA);\n                });\n                return tilesWithOre[0].tile;\n            }\n            startRadius = 2;\n        }\n        const maxRadius = farScan ? this.scanFarRadius : this.scanNearRadius;\n        const finder = new RadialTileFinder(this.game.map.tiles, this.game.map.mapBounds, startTile, { width: 1, height: 1 }, startRadius, maxRadius, isValidTile);\n        return finder.getNextTile();\n    }\n    private getRefineryOnTile(tile: any): any {\n        return this.game.map\n            .getObjectsOnTile(tile)\n            .find((obj: any) => obj.isBuilding() && obj.rules.refinery);\n    }\n    private returnOreIfPossible(unit: any): void {\n        if (unit.unitOrderTrait.getTasks().length === 1) {\n            unit.unitOrderTrait.addTask(new ReturnOreTask(this.game));\n        }\n    }\n    getTargetLinesConfig(unit: any): any {\n        return {\n            pathNodes: this.initialTarget\n                ? [{ tile: this.initialTarget, onBridge: undefined }]\n                : []\n        };\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/harvester/ReturnOreTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { TurnTask } from \"@/game/gameobject/task/TurnTask\";\nimport { WaitMinutesTask } from \"@/game/gameobject/task/system/WaitMinutesTask\";\nimport { TiberiumType } from \"@/engine/type/TiberiumType\";\nimport { HarvesterTrait, HarvesterStatus } from \"@/game/gameobject/trait/HarvesterTrait\";\nimport { TeleportMoveToRefineryTask } from \"@/game/gameobject/task/harvester/TeleportMoveToRefineryTask\";\nimport { GatherOreTask } from \"@/game/gameobject/task/harvester/GatherOreTask\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { MoveTrait, MoveResult } from \"@/game/gameobject/trait/MoveTrait\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nexport class ReturnOreTask extends Task {\n    private game: any;\n    private forceTarget: any;\n    private resetLastOreSite: boolean;\n    private explicitOrder: boolean;\n    private rangeHelper: RangeHelper;\n    private target?: any;\n    private reservedDockNumber?: number;\n    constructor(game: any, forceTarget?: any, resetLastOreSite: boolean = false, explicitOrder: boolean = false) {\n        super();\n        this.game = game;\n        this.forceTarget = forceTarget;\n        this.resetLastOreSite = resetLastOreSite;\n        this.explicitOrder = explicitOrder;\n        this.useChildTargetLines = true;\n        this.preventOpportunityFire = false;\n        this.rangeHelper = new RangeHelper(game.map.tileOccupation);\n    }\n    onStart(unit: any): void {\n        if (!unit.isVehicle() || !unit.harvesterTrait) {\n            throw new Error(`Unit ${unit.name} is not a harvester.`);\n        }\n        unit.harvesterTrait.status = HarvesterStatus.MovingToRefinery;\n        if (this.resetLastOreSite) {\n            unit.harvesterTrait.lastOreSite = undefined;\n        }\n    }\n    onEnd(unit: any): void {\n        if (this.target?.isSpawned) {\n            this.target.dockTrait.undockUnit(unit);\n            this.target.dockTrait.unreserveDockForUnit(unit);\n        }\n        if (unit.harvesterTrait.status !== HarvesterStatus.LookingForRefinery) {\n            unit.harvesterTrait.status = HarvesterStatus.Idle;\n        }\n    }\n    onTick(unit: any): boolean {\n        if (this.isCancelling())\n            return true;\n        const harvesterTrait = unit.harvesterTrait;\n        if (harvesterTrait.status === HarvesterStatus.LookingForRefinery)\n            return true;\n        if (harvesterTrait.status === HarvesterStatus.MovingToRefinery) {\n            if (!this.target ||\n                !this.isValidTargetRefinery(this.target, unit) ||\n                unit.tile !== this.findRefineryDockingTile(this.target)) {\n                const refinery = this.forceTarget ?? this.findClosestReachableRefinery(unit);\n                if (!refinery) {\n                    harvesterTrait.status = HarvesterStatus.LookingForRefinery;\n                    return true;\n                }\n                if (this.target &&\n                    this.target !== refinery &&\n                    this.target.dockTrait.hasReservedDockForUnit(unit)) {\n                    this.target.dockTrait.unreserveDockForUnit(unit);\n                }\n                this.target = refinery;\n            }\n            let dockNumber = this.target.dockTrait.getFirstAvailableDockNumber();\n            let needsReservation = false;\n            if (dockNumber === undefined) {\n                dockNumber = this.target.dockTrait.getFirstEmptyDockNumber();\n                if (dockNumber !== undefined) {\n                    needsReservation = !this.target.dockTrait.hasReservedDockForUnit(unit);\n                }\n            }\n            const dockingTile = this.findRefineryDockingTile(this.target);\n            const distance = this.rangeHelper.tileDistance(unit, dockingTile);\n            if (dockNumber === undefined ||\n                needsReservation ||\n                (distance > this.game.rules.general.harvesterTooFarDistance && !this.explicitOrder)) {\n                const queueingTile = this.findReachableQueueingTile(unit);\n                if (!queueingTile)\n                    return true;\n                if (unit.tile !== queueingTile) {\n                    this.children.push(unit.rules.teleporter\n                        ? new TeleportMoveToRefineryTask(this.game, dockingTile, queueingTile, () => this.chronoMinerCanTeleport(unit, dockingTile, this.target))\n                        : new MoveTask(this.game, queueingTile, false), new CallbackTask(() => {\n                        if (unit.moveTrait.lastMoveResult === MoveResult.Fail) {\n                            harvesterTrait.status = HarvesterStatus.LookingForRefinery;\n                        }\n                        else if (unit.moveTrait.lastMoveResult === MoveResult.CloseEnough) {\n                            this.children.push(new WaitMinutesTask(5 / 60));\n                        }\n                        else if (unit.moveTrait.lastMoveResult === MoveResult.Success) {\n                            this.children.push(new WaitMinutesTask(2 / 60));\n                        }\n                    }));\n                }\n                return false;\n            }\n            if (!this.target.dockTrait.hasReservedDockForUnit(unit)) {\n                this.target.dockTrait.reserveDockAt(unit, dockNumber);\n            }\n            if (this.reservedDockNumber === undefined) {\n                this.reservedDockNumber = this.target.dockTrait.getReservedDockForUnit(unit);\n            }\n            if (unit.tile !== dockingTile) {\n                this.children.push(unit.rules.teleporter\n                    ? new TeleportMoveToRefineryTask(this.game, dockingTile, undefined, () => this.chronoMinerCanTeleport(unit, dockingTile, this.target))\n                    : new MoveTask(this.game, dockingTile, false, {\n                        closeEnoughTiles: 0,\n                        strictCloseEnough: true,\n                    }), new CallbackTask(() => {\n                    if (unit.moveTrait.lastMoveResult === MoveResult.Fail) {\n                        harvesterTrait.status = HarvesterStatus.LookingForRefinery;\n                    }\n                }));\n                return false;\n            }\n            harvesterTrait.status = HarvesterStatus.Docking;\n        }\n        if (!this.isValidTargetRefinery(this.target, unit)) {\n            harvesterTrait.status = HarvesterStatus.MovingToRefinery;\n            this.forceTarget = undefined;\n            return this.onTick(unit);\n        }\n        if (harvesterTrait.status === HarvesterStatus.Docking) {\n            if (unit.direction !== 270) {\n                this.children.push(new TurnTask(270));\n                return false;\n            }\n            this.target.dockTrait.dockUnitAt(unit, this.reservedDockNumber);\n            this.reservedDockNumber = undefined;\n            harvesterTrait.status = HarvesterStatus.PreparingToUnload;\n        }\n        if (harvesterTrait.status === HarvesterStatus.PreparingToUnload) {\n            this.preventOpportunityFire = true;\n            this.children.push(new WaitMinutesTask(2 / 60));\n            harvesterTrait.status = HarvesterStatus.Unloading;\n            return false;\n        }\n        if (harvesterTrait.status !== HarvesterStatus.Unloading)\n            return false;\n        const oreValue = harvesterTrait.ore * this.game.rules.getTiberium(TiberiumType.Ore).value +\n            harvesterTrait.gems * this.game.rules.getTiberium(TiberiumType.Gems).value;\n        this.target.owner.credits += oreValue;\n        const purifierCount = [...this.target.owner.buildings].filter((building: any) => building.rules.orePurifier &&\n            (!building.poweredTrait || !this.target.owner.powerTrait?.isLowPower())).length;\n        const purifierBonus = this.game.rules.general.purifierBonus;\n        this.target.owner.credits += purifierCount * Math.floor(oreValue * purifierBonus);\n        harvesterTrait.ore = 0;\n        harvesterTrait.gems = 0;\n        if (unit.unitOrderTrait.getTasks().length === 1) {\n            unit.unitOrderTrait.addTask(new GatherOreTask(this.game));\n        }\n        return true;\n    }\n    private isValidTargetRefinery(refinery: any, unit: any): boolean {\n        return refinery.isSpawned &&\n            this.game.areFriendly(refinery, unit) &&\n            !refinery.warpedOutTrait.isActive();\n    }\n    private findClosestReachableRefinery(unit: any): any {\n        const rangeHelper = this.rangeHelper;\n        const isAirUnit = unit.zone === ZoneType.Air;\n        const speedType = unit.rules.speedType;\n        const isInfantry = unit.isInfantry();\n        const islandIdMap = !isAirUnit &&\n            this.game.map.terrain.getPassableSpeed(unit.tile, speedType, isInfantry, unit.onBridge)\n            ? this.game.map.terrain.getIslandIdMap(speedType, isInfantry)\n            : undefined;\n        const refineries = [...unit.owner.buildings]\n            .filter((building: any) => building.rules.refinery &&\n            building.dockTrait &&\n            !building.warpedOutTrait.isActive() &&\n            this.isReachableRefinery(building, unit, islandIdMap))\n            .sort((a: any, b: any) => rangeHelper.distance2(unit, a) - rangeHelper.distance2(unit, b));\n        const closestRefinery = refineries[0];\n        const availableRefinery = refineries.find((refinery: any) => refinery.dockTrait.getAvailableDockCount() > 0);\n        if (!availableRefinery ||\n            (closestRefinery &&\n                rangeHelper.tileDistance(unit, availableRefinery.centerTile) -\n                    rangeHelper.tileDistance(unit, closestRefinery.centerTile) >\n                    this.game.rules.general.harvesterTooFarDistance)) {\n            return closestRefinery;\n        }\n        return availableRefinery;\n    }\n    private isReachableRefinery(refinery: any, unit: any, islandIdMap: any): boolean {\n        const dockingTile = this.findRefineryDockingTile(refinery);\n        return unit.rules.teleporter ||\n            islandIdMap?.get(dockingTile, false) === islandIdMap?.get(unit.tile, unit.onBridge);\n    }\n    private findReachableQueueingTile(unit: any): any {\n        if (this.target.art.queueingCell) {\n            const queueingPos = new Vector2(this.target.tile.rx, this.target.tile.ry)\n                .add(this.target.art.queueingCell);\n            const queueingTile = this.game.map.tiles.getByMapCoords(queueingPos.x, queueingPos.y);\n            if (queueingTile && this.isValidQueueingTile(queueingTile, unit)) {\n                return queueingTile;\n            }\n        }\n        return new RadialTileFinder(this.game.map.tiles, this.game.map.mapBounds, this.target.tile, this.target.getFoundation(), 1, 1, (tile: any) => this.isValidQueueingTile(tile, unit)).getNextTile();\n    }\n    private isValidQueueingTile(tile: any, unit: any): boolean {\n        const isAirUnit = unit.zone === ZoneType.Air;\n        const speedType = unit.rules.speedType;\n        const isInfantry = unit.isInfantry();\n        const islandIdMap = !isAirUnit &&\n            this.game.map.terrain.getPassableSpeed(unit.tile, speedType, isInfantry, unit.onBridge)\n            ? this.game.map.terrain.getIslandIdMap(speedType, isInfantry)\n            : undefined;\n        return isAirUnit ||\n            (islandIdMap?.get(tile, false) === islandIdMap?.get(unit.tile, unit.onBridge) &&\n                Math.abs(tile.z - this.target.tile.z) < 2 &&\n                !tile.onBridgeLandType);\n    }\n    private findRefineryDockingTile(refinery: any): any {\n        const dockingPos = {\n            x: refinery.tile.rx + refinery.getFoundation().width - 1,\n            y: refinery.tile.ry + Math.floor(refinery.getFoundation().height / 2),\n        };\n        return this.game.map.tiles.getByMapCoords(dockingPos.x, dockingPos.y);\n    }\n    private chronoMinerCanTeleport(unit: any, targetTile: any, refinery: any): boolean {\n        const rangeHelper = this.rangeHelper;\n        const distance = rangeHelper.tileDistance(unit, targetTile);\n        return !(!this.forceTarget &&\n            distance > this.game.rules.general.chronoHarvTooFarDistance) &&\n            !(distance <= 1) &&\n            !!this.isValidTargetRefinery(refinery, unit) &&\n            !(refinery.dockTrait.getAvailableDockCount() === 0 &&\n                !refinery.dockTrait.hasReservedDockForUnit(unit));\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/harvester/TeleportMoveToRefineryTask.ts",
    "content": "import { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { MoveState } from \"../../trait/MoveTrait\";\nexport class TeleportMoveToRefineryTask extends MoveTask {\n    private teleportTile: any;\n    private teleportCondition: ((unit: any, tile: any) => boolean) | undefined;\n    constructor(game: any, teleportTile: any, targetTile?: any, teleportCondition?: (unit: any, tile: any) => boolean) {\n        super(game, targetTile ?? teleportTile, false, {\n            closeEnoughTiles: targetTile ? undefined : 0,\n            strictCloseEnough: !targetTile,\n        });\n        this.teleportTile = teleportTile;\n        this.teleportCondition = teleportCondition;\n    }\n    onStart(unit: any): void {\n        super.onStart(unit);\n        if (!unit.harvesterTrait || unit.rules.locomotor !== LocomotorType.Chrono) {\n            throw new Error(`Vehicle ${unit.name} is not a chrono miner`);\n        }\n    }\n    onTick(unit: any): boolean {\n        if (unit.moveTrait.isDisabled()) {\n            return false;\n        }\n        if (this.isCancelling() ||\n            unit.moveTrait.moveState !== MoveState.ReachedNextWaypoint ||\n            unit.tile === this.teleportTile ||\n            !this.tryTeleportToRefinery(unit)) {\n            return super.onTick(unit) === true && (this.isCancelling() ||\n                unit.tile === this.teleportTile ||\n                this.tryTeleportToRefinery(unit));\n        }\n        return true;\n    }\n    private tryTeleportToRefinery(unit: any): boolean {\n        if ((this.teleportCondition && this.teleportCondition(unit, this.teleportTile) === false) ||\n            this.game.map.terrain.findObstacles({ tile: this.teleportTile, onBridge: undefined }, unit).length > 0) {\n            return false;\n        }\n        unit.moveTrait.teleportUnitToTile(this.teleportTile, undefined, true, true, this.game);\n        if (unit.zone === ZoneType.Air) {\n            unit.zone = ZoneType.Ground;\n            unit.position.tileElevation = 0;\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/morph/DeployIntoTask.ts",
    "content": "import { MorphIntoTask } from \"@/game/gameobject/task/morph/MorphIntoTask\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nexport class DeployIntoTask extends MorphIntoTask {\n    onStart(unit: any): void {\n        const deploysInto = unit.rules.deploysInto;\n        if (!deploysInto) {\n            throw new Error(`Object type \"${unit.name}\" doesn't deploy into anything`);\n        }\n        this.morphInto = this.game.rules.getObject(deploysInto, ObjectType.Building);\n        super.onStart(unit);\n    }\n    onTick(unit: any): boolean {\n        return !!this.isCancelling() || super.onTick(unit);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/morph/MorphIntoTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { Building, BuildStatus } from \"@/game/gameobject/Building\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { ObjectMorphEvent } from \"@/game/event/ObjectMorphEvent\";\nimport { TurnTask } from \"@/game/gameobject/task/TurnTask\";\nimport { PackBuildingTask } from \"@/game/gameobject/task/morph/PackBuildingTask\";\nexport class MorphIntoTask extends Task {\n    protected game: any;\n    protected morphInto: any;\n    constructor(game: any) {\n        super();\n        this.game = game;\n    }\n    onStart(unit: any): void {\n        if (!this.morphInto)\n            throw new Error(\"morphInto not set\");\n        if (unit.isBuilding() &&\n            unit.buildStatus !== BuildStatus.BuildDown &&\n            this.morphInto.type !== ObjectType.Building) {\n            this.children.push(new PackBuildingTask(this.game));\n        }\n        if (unit.isVehicle() &&\n            this.morphInto.type === ObjectType.Building) {\n            this.children.push(new TurnTask(180));\n        }\n    }\n    onTick(unit: any): boolean {\n        if (!this.morphInto)\n            throw new Error(\"morphInto not set\");\n        const selection = this.game.getUnitSelection();\n        const isSelected = selection.isSelected(unit);\n        const controlGroup = selection.getOrCreateSelectionModel(unit).getControlGroupNumber();\n        const morphTarget = this.morphInto;\n        let newObject: any;\n        if (morphTarget.type === ObjectType.Building) {\n            if (unit.isVehicle() &&\n                unit.parasiteableTrait?.isInfested() &&\n                !unit.parasiteableTrait.beingBoarded) {\n                return true;\n            }\n            const tile = unit.tile;\n            const constructionWorker = this.game.getConstructionWorker(unit.owner);\n            if (!constructionWorker.canPlaceAt(this.morphInto.name, tile, {\n                ignoreAdjacent: true,\n                ignoreObjects: [unit]\n            })) {\n                return true;\n            }\n            this.game.unspawnObject(unit);\n            unit.dispose();\n            [newObject] = constructionWorker.placeAt(this.morphInto.name, tile);\n            newObject.healthTrait.health = unit.healthTrait.health;\n        }\n        else {\n            const moveTasks = unit.unitOrderTrait.getTasks()\n                .filter((task: any) => task instanceof MoveTask);\n            this.game.unspawnObject(unit);\n            unit.dispose();\n            newObject = this.game.createUnitForPlayer(this.morphInto, unit.owner);\n            newObject.direction = 180;\n            newObject.healthTrait.health = unit.healthTrait.health;\n            const foundationCenter = unit.art.foundationCenter;\n            this.game.spawnObject(newObject, this.game.map.tiles.getByMapCoords(unit.tile.rx + foundationCenter.x, unit.tile.ry + foundationCenter.y));\n            moveTasks.forEach((task: any) => newObject.unitOrderTrait.addTask(task));\n        }\n        newObject.purchaseValue = unit.purchaseValue;\n        unit.replacedBy = newObject;\n        if (isSelected) {\n            selection.addToSelection(newObject);\n        }\n        if (controlGroup !== undefined) {\n            selection.addUnitsToGroup(controlGroup, [newObject], false);\n        }\n        this.game.events.dispatch(new ObjectMorphEvent(unit, newObject));\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/morph/PackBuildingTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nimport { Building, BuildStatus } from \"@/game/gameobject/Building\";\nimport { WaitMinutesTask } from \"@/game/gameobject/task/system/WaitMinutesTask\";\nexport class PackBuildingTask extends Task {\n    private game: any;\n    constructor(game: any) {\n        super();\n        this.game = game;\n    }\n    onTick(unit: any): boolean {\n        if (unit.buildStatus !== BuildStatus.BuildDown && !unit.rules.wall) {\n            unit.setBuildStatus(BuildStatus.BuildDown, this.game);\n            this.children.push(new WaitMinutesTask(this.game.rules.general.buildupTime));\n            return false;\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/morph/UndeployIntoTask.ts",
    "content": "import { MorphIntoTask } from \"@/game/gameobject/task/morph/MorphIntoTask\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nexport class UndeployIntoTask extends MorphIntoTask {\n    onStart(unit: any): void {\n        const undeploysInto = unit.rules.undeploysInto;\n        if (!undeploysInto) {\n            throw new Error(`Object type \"${unit.name}\" doesn't undeploy into anything`);\n        }\n        this.morphInto = this.game.rules.getObject(undeploysInto, ObjectType.Vehicle);\n        super.onStart(unit);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/move/AttackMoveTargetTask.ts",
    "content": "import { AttackTask } from \"@/game/gameobject/task/AttackTask\";\nimport { MoveState } from \"@/game/gameobject/trait/MoveTrait\";\nexport class AttackMoveTargetTask extends AttackTask {\n    private attackPerformed: boolean = false;\n    private passedFirstWaypoint: boolean = false;\n    private internalTargetUpdateRequested: boolean = false;\n    private scanCooldownTicks: number = 0;\n    private initialTarget: any;\n    private initialWeapon: any;\n    private requestedTarget: any;\n    private lastScanTile: any;\n    constructor(game: any, target: any, weapon: any) {\n        super(game, target, weapon);\n        if (!target.obj?.isTechno()) {\n            throw new Error(\"Target must be a techno object\");\n        }\n        this.initialTarget = target;\n        this.initialWeapon = weapon;\n        this.requestedTarget = target;\n        this.isAttackMove = true;\n    }\n    duplicate(): AttackMoveTargetTask {\n        return new AttackMoveTargetTask(this.game, this.initialTarget, this.initialWeapon);\n    }\n    requestTargetUpdate(target: any): void {\n        if (this.internalTargetUpdateRequested) {\n            this.requestedTarget = target;\n            this.internalTargetUpdateRequested = false;\n        }\n        else {\n            if (this.requestedTarget === this.initialTarget) {\n                this.requestedTarget = target;\n            }\n            else {\n                this.attackPerformed = true;\n            }\n            this.initialTarget = target;\n        }\n        super.requestTargetUpdate(target);\n    }\n    onTargetChange(unit: any): void {\n        super.onTargetChange(unit);\n        const currentTarget = unit.attackTrait.currentTarget;\n        if (currentTarget &&\n            currentTarget.obj !== this.initialTarget.obj &&\n            currentTarget.obj !== this.requestedTarget.obj) {\n            if (this.requestedTarget === this.initialTarget) {\n                this.requestedTarget = currentTarget;\n            }\n            this.initialTarget = currentTarget;\n        }\n    }\n    onTick(unit: any): boolean {\n        if (unit.moveTrait.moveState === MoveState.Moving) {\n            this.passedFirstWaypoint = true;\n        }\n        this.scanCooldownTicks = Math.max(0, this.scanCooldownTicks - 1);\n        if (unit.attackTrait &&\n            !unit.attackTrait.isDisabled() &&\n            !this.isCancelling() &&\n            (this.requestedTarget === this.initialTarget || this.attackPerformed)) {\n            if (!(unit.moveTrait.isIdle() ||\n                (unit.tile === this.lastScanTile && this.scanCooldownTicks))) {\n                this.lastScanTile = unit.tile;\n                this.scanCooldownTicks = this.game.rules.general.normalTargetingDelay;\n                const weapon = unit.attackTrait.selectDefaultWeapon(unit);\n                if (weapon && (this.passedFirstWaypoint || !weapon.getCooldownTicks())) {\n                    const scanResult = unit.attackTrait.scanForTarget(unit, weapon, this.game);\n                    if (scanResult.target) {\n                        const { target, weapon } = scanResult;\n                        if (!weapon.getCooldownTicks()) {\n                            this.options.holdGround = true;\n                            this.options.passive = true;\n                            this.setWeapon(weapon);\n                            const newTarget = this.game.createTarget(target, target.tile);\n                            this.internalTargetUpdateRequested = true;\n                            this.requestTargetUpdate(newTarget);\n                            this.attackPerformed = false;\n                            return false;\n                        }\n                    }\n                }\n            }\n            if (this.attackPerformed) {\n                if (!unit.isSpawned) {\n                    if (!this.forceCancel(unit)) {\n                        throw new Error(\"Force cancel failed\");\n                    }\n                    return true;\n                }\n                this.attackPerformed = false;\n                this.passedFirstWaypoint = false;\n                this.options.holdGround = false;\n                this.options.passive = false;\n                this.setWeapon(this.initialWeapon);\n                this.internalTargetUpdateRequested = true;\n                this.requestTargetUpdate(this.initialTarget);\n            }\n        }\n        const result = super.onTick(unit);\n        if (result && this.requestedTarget !== this.initialTarget) {\n            this.attackPerformed = true;\n            return this.isCancelling() || unit.attackTrait.isDisabled();\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/move/AttackMoveTask.ts",
    "content": "import { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { MoveState, CollisionState } from \"@/game/gameobject/trait/MoveTrait\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nexport class AttackMoveTask extends MoveTask {\n    private attackPerformed: boolean = false;\n    private passedFirstWaypoint: boolean = false;\n    constructor(game: any, targetTile: any, toBridge: boolean, options?: any) {\n        super(game, targetTile, toBridge, options);\n        this.isAttackMove = true;\n    }\n    duplicate(): AttackMoveTask {\n        return new AttackMoveTask(this.game, this.targetTile, this.toBridge, this.options);\n    }\n    onTick(unit: any): boolean {\n        if (unit.moveTrait.moveState === MoveState.Moving) {\n            this.passedFirstWaypoint = true;\n        }\n        if (unit.moveTrait.moveState === MoveState.ReachedNextWaypoint &&\n            unit.attackTrait &&\n            !unit.attackTrait.isDisabled() &&\n            (unit.rules.movementZone !== MovementZone.Fly || !unit.rules.balloonHover) &&\n            (!unit.ammoTrait || unit.ammoTrait.ammo || !unit.rules.manualReload) &&\n            !this.isCancelling()) {\n            const weapon = unit.attackTrait.selectDefaultWeapon(unit);\n            if (weapon && (this.passedFirstWaypoint || !weapon.getCooldownTicks())) {\n                const scanResult = unit.attackTrait.scanForTarget(unit, weapon, this.game);\n                if (scanResult.target) {\n                    const { target, weapon: targetWeapon } = scanResult;\n                    if (!targetWeapon.getCooldownTicks()) {\n                        const attackTask = unit.attackTrait.createAttackTask(this.game, target, target.tile, targetWeapon, { holdGround: true, passive: true });\n                        this.children.push(attackTask);\n                        this.useChildTargetLines = true;\n                        this.attackPerformed = true;\n                        unit.moveTrait.velocity.set(0, 0, 0);\n                        unit.moveTrait.currentWaypoint = undefined;\n                        unit.moveTrait.collisionState = CollisionState.Waiting;\n                        return false;\n                    }\n                }\n            }\n            if (this.attackPerformed) {\n                if (!unit.isSpawned) {\n                    if (!this.forceCancel(unit)) {\n                        throw new Error(\"Force cancel failed\");\n                    }\n                    return true;\n                }\n                this.attackPerformed = false;\n                this.passedFirstWaypoint = false;\n                this.useChildTargetLines = false;\n                unit.moveTrait.collisionState = CollisionState.Resolved;\n                this.updateTarget(this.targetTile, this.toBridge);\n            }\n        }\n        return super.onTick(unit);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/move/ExitFactoryTask.ts",
    "content": "import { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { MoveState } from \"@/game/gameobject/trait/MoveTrait\";\nimport { FactoryType } from \"@/game/rules/TechnoRules\";\nimport { ScatterTask } from \"@/game/gameobject/task/ScatterTask\";\nimport { AttackTask } from \"@/game/gameobject/task/AttackTask\";\nimport { AttackMoveTargetTask } from \"@/game/gameobject/task/move/AttackMoveTargetTask\";\nimport { AttackMoveTask } from \"@/game/gameobject/task/move/AttackMoveTask\";\nexport class ExitFactoryTask extends MoveTask {\n    private factory: any;\n    private rallyPoint: any;\n    private rampBlockersPushed: boolean = false;\n    private checkRampTiles?: any[];\n    constructor(game: any, factory: any, targetTile: any, rallyPoint: any) {\n        super(game, targetTile, false, {\n            ignoredBlockers: [factory],\n            closeEnoughTiles: 0,\n            strictCloseEnough: true,\n            forceWaitOnPathBlocked: factory.factoryTrait?.type !== FactoryType.InfantryType,\n        });\n        this.factory = factory;\n        this.rallyPoint = rallyPoint;\n        this.preventOpportunityFire = true;\n        this.cancellable = false;\n    }\n    onStart(unit: any): void {\n        super.onStart(unit);\n        if (this.factory.factoryTrait?.type === FactoryType.UnitType) {\n            this.checkRampTiles = this.game.map.tileOccupation\n                .calculateTilesForGameObject(this.factory.tile, this.factory)\n                .filter(tile => this.game.map.terrain.getPassableSpeed(tile, unit.rules.speedType, unit.isInfantry(), false) > 0);\n        }\n    }\n    canStopAtTile(unit: any, tile: any, onBridge: any): boolean {\n        return !this.game.map.tileOccupation.isTileOccupiedBy(tile, this.factory) &&\n            super.canStopAtTile(unit, tile, onBridge);\n    }\n    onTick(unit: any): boolean {\n        if (this.checkRampTiles) {\n            for (const tile of this.checkRampTiles) {\n                for (const obj of this.game.map.tileOccupation.getGroundObjectsOnTile(tile)) {\n                    if (obj.isUnit()) {\n                        if (this.rampBlockersPushed)\n                            return false;\n                        const scatterTask = new ScatterTask(this.game, undefined, {\n                            excludedTiles: this.checkRampTiles,\n                        });\n                        scatterTask.setCancellable(false);\n                        const currentTask = obj.unitOrderTrait.getCurrentTask();\n                        if (currentTask) {\n                            if (currentTask.constructor === MoveTask ||\n                                currentTask.constructor === AttackTask ||\n                                currentTask.constructor === AttackMoveTask ||\n                                currentTask.constructor === AttackMoveTargetTask) {\n                                const duplicateTask = currentTask.duplicate();\n                                currentTask.cancel();\n                                obj.unitOrderTrait.addTaskNext(duplicateTask);\n                                obj.unitOrderTrait.addTaskNext(scatterTask);\n                            }\n                        }\n                        else {\n                            obj.unitOrderTrait.addTask(scatterTask);\n                        }\n                    }\n                }\n            }\n            if (!this.rampBlockersPushed) {\n                this.rampBlockersPushed = true;\n                return false;\n            }\n            this.checkRampTiles = undefined;\n        }\n        if (unit.moveTrait.moveState === MoveState.ReachedNextWaypoint &&\n            this.options?.ignoredBlockers &&\n            !this.game.map.terrain.isBlockerObject(this.factory, unit.tile, false, unit.rules.speedType, unit.isInfantry())) {\n            this.options.ignoredBlockers = undefined;\n            this.preventOpportunityFire = false;\n            if (this.rallyPoint) {\n                this.updateTarget(this.rallyPoint.tile, !!this.rallyPoint.onBridge);\n                this.cancellable = true;\n                this.options.closeEnoughTiles = this.game.rules.general.closeEnough;\n                this.options.strictCloseEnough = false;\n                this.options.forceWaitOnPathBlocked = false;\n            }\n        }\n        return super.onTick(unit);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/move/MoveAsideTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nimport { MovePositionHelper } from \"@/game/gameobject/unit/MovePositionHelper\";\nimport { WaitTicksTask } from \"@/game/gameobject/task/system/WaitTicksTask\";\nimport { MoveTrait, CollisionState, MoveState } from \"@/game/gameobject/trait/MoveTrait\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { rotateVec2 } from \"@/game/math/geometry\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nexport class MoveAsideTask extends Task {\n    private game: any;\n    private fromDirection: any;\n    private resolved: boolean = false;\n    private chainPushIssued: boolean = false;\n    private timeoutTicks?: number;\n    constructor(game: any, fromDirection: any) {\n        super();\n        this.game = game;\n        this.fromDirection = fromDirection;\n    }\n    onEnd(unit: any): void {\n        unit.moveTrait.collisionState = CollisionState.Resolved;\n    }\n    onTick(unit: any): boolean {\n        this.timeoutTicks = this.timeoutTicks === undefined ? 0 : this.timeoutTicks + 1;\n        if (this.timeoutTicks > 40 || this.resolved || this.isCancelling()) {\n            return true;\n        }\n        const map = this.game.map;\n        const moveHelper = new MovePositionHelper(map);\n        const bridge = unit.onBridge ? map.tileOccupation.getBridgeOnTile(unit.tile) : undefined;\n        let targetTile: any;\n        let targetBridge: any;\n        for (let angle = 0; angle < 360; angle += 45) {\n            if ((angle !== 0 || this.chainPushIssued) && angle !== 180) {\n                const direction = rotateVec2(this.fromDirection.clone(), angle).round();\n                const tile = map.tiles.getByMapCoords(unit.tile.rx + Math.sign(direction.x), unit.tile.ry + Math.sign(direction.y));\n                if (tile && map.mapBounds.isWithinBounds(tile)) {\n                    targetBridge = map.tileOccupation.getBridgeOnTile(tile);\n                    if (unit.rules.movementZone === MovementZone.Fly ||\n                        (!map.terrain.findObstacles({ tile, onBridge: targetBridge }, unit).length &&\n                            moveHelper.isEligibleTile(tile, targetBridge, bridge, unit.tile))) {\n                        targetTile = tile;\n                        break;\n                    }\n                }\n            }\n        }\n        if (targetTile) {\n            this.resolved = true;\n            if (unit.isInfantry() && unit.deployerTrait?.isDeployed()) {\n                unit.deployerTrait.setDeployed(false);\n            }\n            if (unit.moveTrait.isDisabled()) {\n                return true;\n            }\n            this.children.push(new MoveTask(this.game, targetTile, !!targetBridge, {\n                closeEnoughTiles: 0,\n                strictCloseEnough: true\n            }));\n            return false;\n        }\n        if (this.chainPushIssued) {\n            this.children.push(new WaitTicksTask(5));\n            return false;\n        }\n        const pushTile = map.tiles.getByMapCoords(unit.tile.rx + Math.sign(this.fromDirection.x), unit.tile.ry + Math.sign(this.fromDirection.y));\n        if (!pushTile || !map.mapBounds.isWithinBounds(pushTile)) {\n            return true;\n        }\n        targetBridge = map.tileOccupation.getBridgeOnTile(pushTile);\n        const pushableUnits = map.tileOccupation.getGroundObjectsOnTile(pushTile).filter(unit => unit.isUnit() &&\n            unit.owner === unit.owner &&\n            unit.tile === pushTile &&\n            unit.onBridge === !!targetBridge &&\n            !(unit.isInfantry() && unit.stance === StanceType.Paradrop) &&\n            !(unit.isAircraft() && unit.missileSpawnTrait));\n        if (pushableUnits.find(unit => unit.moveTrait.collisionState === CollisionState.Waiting ||\n            unit.unitOrderTrait.hasTasks())) {\n            this.children.push(new WaitTicksTask(5));\n            unit.moveTrait.collisionState = CollisionState.Waiting;\n            unit.moveTrait.moveState = MoveState.PlanMove;\n            return false;\n        }\n        pushableUnits.forEach(unit => {\n            unit.unitOrderTrait.addTask(new MoveAsideTask(this.game, this.fromDirection));\n        });\n        this.children.push(new WaitTicksTask(1));\n        unit.moveTrait.collisionState = CollisionState.Waiting;\n        unit.moveTrait.moveState = MoveState.PlanMove;\n        this.chainPushIssued = true;\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/move/MoveInWeaponRangeTask.ts",
    "content": "import { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { GameObject } from \"@/game/gameobject/GameObject\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { Coords } from \"@/game/Coords\";\nimport { LosHelper } from \"@/game/gameobject/unit/LosHelper\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { MoveState } from \"@/game/gameobject/trait/MoveTrait\";\nimport { RandomTileFinder } from \"@/game/map/tileFinder/RandomTileFinder\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\nimport { bresenham } from \"@/util/bresenham\";\nimport { FacingUtil } from \"@/game/gameobject/unit/FacingUtil\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nexport const STRAFE_CLOSE_ENOUGH = 2;\nexport class MoveInWeaponRangeTask extends MoveTask {\n    public target: any;\n    private weapon: any;\n    private recalcMinRange: boolean = true;\n    private cancelRequested: boolean = false;\n    private bomberInitialLock: boolean = false;\n    private rangeHelper: RangeHelper;\n    private losHelper: LosHelper;\n    private bomberManeuverTile?: any;\n    private bomberQueuedTargetTile?: any;\n    constructor(game: any, target: any, unit: any, weapon: any) {\n        super(game, target instanceof GameObject\n            ? target.isBuilding()\n                ? (target as any).centerTile\n                : target.tile\n            : target, unit, {\n            pathFinderIgnoredBlockers: target instanceof GameObject && weapon.range > 0 ? [target] : undefined,\n        });\n        this.target = target;\n        this.weapon = weapon;\n        this.rangeHelper = new RangeHelper(game.map.tileOccupation);\n        this.losHelper = new LosHelper(game.map.tiles, game.map.tileOccupation);\n    }\n    onStart(unit: any) {\n        let target = this.target;\n        let map = this.game.map;\n        if (target instanceof GameObject &&\n            target.isBuilding() &&\n            unit.rules.movementZone !== MovementZone.Fly) {\n            let centerTile = target.tile;\n            const foundation = target instanceof GameObject\n                ? target.getFoundation()\n                : { width: 1, height: 1 };\n            const tileFinder = new RadialTileFinder(map.tiles, map.mapBounds, centerTile, foundation, 1, 5, (tile) => map.terrain.getPassableSpeed(tile, unit.rules.speedType, unit.isInfantry(), false) > 0 && Math.abs(tile.z - centerTile.z) < 2);\n            const tile = tileFinder.getNextTile();\n            if (tile && this.rangeHelper.tileDistance(target, tile) > Math.SQRT2) {\n                this.updateTarget(tile, false);\n            }\n        }\n        this.bomberInitialLock = this.isCloseEnoughToDest(unit, unit.tile);\n        super.onStart(unit);\n    }\n    cancel() {\n        if (this.bomberManeuverTile) {\n            this.cancelRequested = true;\n        }\n        else {\n            super.cancel();\n        }\n    }\n    shouldAirStrafe(unit: any): boolean {\n        return (unit.rules.movementZone === MovementZone.Fly &&\n            unit.rules.locomotor === LocomotorType.Aircraft &&\n            unit.rules.fighter &&\n            this.weapon.projectileRules.iniRot > 1);\n    }\n    isBombingRun(unit: any): boolean {\n        return (unit.rules.movementZone === MovementZone.Fly &&\n            unit.rules.locomotor === LocomotorType.Aircraft &&\n            this.weapon.projectileRules.iniRot <= 1);\n    }\n    isAirStrafeCloseEnough(unit: any): boolean {\n        return (this.rangeHelper.tileDistance(unit, this.targetTile) <\n            Math.min(this.weapon.range, STRAFE_CLOSE_ENOUGH));\n    }\n    bomberCanReturn(unit: any): boolean {\n        return (!this.bomberManeuverTile ||\n            this.rangeHelper.tileDistance(unit, this.bomberManeuverTile) <= 1);\n    }\n    findStrafeDestination(unit: any, targetTile: any): any {\n        const tileFinder = new RandomTileFinder(this.game.map.tiles, this.game.map.mapBounds, targetTile as any, this.weapon.range, this.game, (tile) => this.rangeHelper.isInWeaponRange(unit, targetTile, this.weapon, this.game.rules, tile as any));\n        return tileFinder.getNextTile();\n    }\n    hasReachedDestination(unit: any): boolean {\n        return (super.hasReachedDestination(unit) ||\n            this.canStopAtTile(unit, unit.tile, unit.onBridge));\n    }\n    canStopAtTile(unit: any, tile: any, onBridge: boolean): boolean {\n        if (unit.zone !== ZoneType.Air &&\n            this.target instanceof GameObject &&\n            this.game.map.tileOccupation.isTileOccupiedBy(tile, this.target) &&\n            (!this.target.isUnit() ||\n                (this.target.tile === tile &&\n                    (this.target as any).moveTrait.moveState !== MoveState.Moving &&\n                    this.target.position.subCell === unit.position.subCell))) {\n            return false;\n        }\n        if (unit.zone !== ZoneType.Air) {\n            if (!super.canStopAtTile(unit, tile, onBridge)) {\n                return false;\n            }\n        }\n        else if (this.game.map.tileOccupation\n            .getAirObjectsOnTile(tile)\n            .filter((obj: any) => obj.isUnit() &&\n            obj.moveTrait.moveState !== MoveState.Moving &&\n            obj !== unit).length) {\n            return false;\n        }\n        return (!(this.isBombingRun(unit) && !this.bomberCanReturn(tile)) &&\n            (this.isCancelling() || this.isCloseEnoughToDest(unit, tile)));\n    }\n    isCloseEnoughToDest(unit: any, tile: any): boolean {\n        if (unit.rules.balloonHover && !unit.rules.hoverAttack) {\n            return this.rangeHelper.isInTileRange(tile, this.target, 0, 0);\n        }\n        if (this.weapon.rules.cellRangefinding || !unit.isInfantry()) {\n            return (this.rangeHelper.isInWeaponRange(unit, this.target, this.weapon, this.game.rules, tile) &&\n                this.losHelper.hasLineOfSight(tile, this.target, this.weapon));\n        }\n        const offset = unit.zone === ZoneType.Air\n            ? unit.position.computeSubCellOffset(unit.position.desiredSubCell)\n            : unit.position.getTileOffset();\n        const { minRange, range } = this.rangeHelper.computeWeaponRangeVsTarget(tile, this.target, this.weapon, this.game.rules);\n        const worldPos = Coords.tile3dToWorld(tile.rx + offset.x / Coords.LEPTONS_PER_TILE, tile.ry + offset.y / Coords.LEPTONS_PER_TILE, tile.z + unit.position.tileElevation);\n        return ((unit.isUnit() && unit.rules.movementZone === MovementZone.Fly\n            ? this.rangeHelper.isInRange2(worldPos, this.target, minRange, range)\n            : this.rangeHelper.isInRange3(worldPos, this.target, minRange, range)) &&\n            this.losHelper.hasLineOfSight(tile, this.target, this.weapon));\n    }\n    findRelocationTile(fromTile: any, toTile: any, unit: any): any {\n        if (unit.rules.movementZone !== MovementZone.Fly) {\n            return super.findRelocationTile(fromTile, toTile, unit);\n        }\n        else {\n            const map = this.game.map;\n            const tileFinder = new RandomTileFinder(map.tiles, map.mapBounds, fromTile, 1, this.game, (tile) => this.isCancelling() || this.isCloseEnoughToDest(unit, tile));\n            return tileFinder.getNextTile();\n        }\n    }\n    retarget(newTarget: any, updatePath: boolean) {\n        const targetTile = newTarget instanceof GameObject\n            ? newTarget.isBuilding()\n                ? (newTarget as any).centerTile\n                : newTarget.tile\n            : newTarget;\n        if (this.bomberManeuverTile) {\n            this.bomberQueuedTargetTile = targetTile;\n        }\n        else {\n            this.updateTarget(targetTile, updatePath);\n            this.recalcMinRange = true;\n        }\n        this.target = newTarget;\n        if (this.options?.ignoredBlockers) {\n            this.options.ignoredBlockers =\n                newTarget instanceof GameObject ? [newTarget] : undefined;\n        }\n        if (!this.options) {\n            this.options = {};\n        }\n        this.options.pathFinderIgnoredBlockers =\n            newTarget instanceof GameObject ? [newTarget] : undefined;\n    }\n    onTick(unit: any): boolean {\n        if (this.recalcMinRange) {\n            this.recalcMinRange = false;\n            const relocTile = this.findMinRangeRelocationTile(unit, this.targetTile);\n            if (relocTile !== this.targetTile) {\n                if (!relocTile) {\n                    this.cancel();\n                    return false;\n                }\n                this.updateTarget(relocTile, !!relocTile.onBridgeLandType);\n            }\n        }\n        if (this.shouldAirStrafe(unit) && !this.isCancelling()) {\n            this.updateTarget(this.target instanceof GameObject\n                ? this.target.isBuilding()\n                    ? (this.target as any).centerTile\n                    : this.target.tile\n                : this.target, false);\n            if (!this.isAirStrafeCloseEnough(unit)) {\n                const strafeDest = this.findStrafeDestination(unit, this.targetTile);\n                if (strafeDest) {\n                    this.updateTarget(strafeDest, false);\n                }\n            }\n        }\n        if (this.isBombingRun(unit) &&\n            !this.isCancelling() &&\n            (!unit.ammo || this.weapon.getBurstsFired() || this.bomberInitialLock) &&\n            !this.bomberManeuverTile) {\n            this.bomberInitialLock = false;\n            const unitPos = unit.position.getMapPosition();\n            const targetTile = this.target instanceof GameObject\n                ? this.target.isBuilding()\n                    ? (this.target as any).centerTile\n                    : this.target.tile\n                : this.target;\n            const direction = new Vector2(targetTile.rx + 0.5, targetTile.ry + 0.5)\n                .clone()\n                .multiplyScalar(Coords.LEPTONS_PER_TILE)\n                .sub(unitPos);\n            let distance = direction.length();\n            if (!distance) {\n                direction.copy(FacingUtil.toMapCoords(unit.direction));\n                distance = Number.EPSILON;\n            }\n            const maneuverPos = unitPos\n                .clone()\n                .add(direction.setLength(distance + 7 * Coords.LEPTONS_PER_TILE));\n            const maneuverMapCoords = maneuverPos\n                .multiplyScalar(1 / Coords.LEPTONS_PER_TILE)\n                .floor();\n            const bresCoords = bresenham(maneuverMapCoords.x, maneuverMapCoords.y, unit.tile.rx, unit.tile.ry);\n            if (!bresCoords.length) {\n                throw new Error(\"Bresenham returned no tiles\");\n            }\n            const firstCoord = bresCoords[0];\n            this.bomberManeuverTile =\n                this.game.map.tiles.getByMapCoords(firstCoord.x, firstCoord.y) ??\n                    this.game.map.tiles.getPlaceholderTile(firstCoord.x, firstCoord.y);\n            this.options.allowOutOfBoundsTarget = true;\n            this.updateTarget(this.bomberManeuverTile, false);\n        }\n        if (this.bomberManeuverTile && this.bomberCanReturn(unit.tile)) {\n            this.bomberManeuverTile = undefined;\n            if (this.bomberQueuedTargetTile) {\n                this.updateTarget(this.bomberQueuedTargetTile, false);\n                this.recalcMinRange = true;\n                this.bomberQueuedTargetTile = undefined;\n            }\n        }\n        if (this.cancelRequested) {\n            if (!this.bomberManeuverTile) {\n                this.cancelRequested = false;\n                this.cancel();\n            }\n        }\n        return !!(this.isBombingRun(unit) &&\n            this.isCancelling() &&\n            this.forceCancel(unit)) || super.onTick(unit);\n    }\n    forceCancel(unit: any): boolean {\n        return !this.bomberManeuverTile && super.forceCancel(unit);\n    }\n    findMinRangeRelocationTile(unit: any, targetTile: any): any {\n        const { minRange, range } = this.rangeHelper.computeWeaponRangeVsTarget(unit, this.target, this.weapon, this.game.rules);\n        if (unit.rules.locomotor === LocomotorType.Chrono) {\n            return this.rangeHelper.isInRange(unit, this.target, range - 1, range, this.weapon.rules.cellRangefinding)\n                ? targetTile\n                : (this.findTileInRange(unit, targetTile, range - 1, 2 * range) ?? targetTile);\n        }\n        else {\n            return this.rangeHelper.isInRange(unit, this.target, minRange, Number.POSITIVE_INFINITY, this.weapon.rules.cellRangefinding)\n                ? targetTile\n                : this.findTileInRange(unit, targetTile, 2 * minRange, range - minRange);\n        }\n    }\n    findTileInRange(unit: any, targetTile: any, minDist: number, maxDist: number): any {\n        const map = this.game.map;\n        const direction = new Vector2(unit.tile.rx - targetTile.rx, unit.tile.ry - targetTile.ry)\n            .setLength(minDist)\n            .floor()\n            .add(new Vector2(targetTile.rx, targetTile.ry));\n        let tile;\n        for (const coord of bresenham(direction.x, direction.y, targetTile.rx, targetTile.ry)) {\n            tile = map.tiles.getByMapCoords(coord.x, coord.y);\n            if (tile)\n                break;\n        }\n        if (tile) {\n            const tileFinder = new RadialTileFinder(map.tiles, map.mapBounds, tile as any, { width: 1, height: 1 }, 0, maxDist, (t) => this.rangeHelper.isInWeaponRange(unit, this.target, this.weapon, this.game.rules, t as any) &&\n                this.losHelper.hasLineOfSight(t, this.target, this.weapon) &&\n                map.terrain.getPassableSpeed(t, unit.rules.speedType, unit.isInfantry(), !!t.onBridgeLandType) > 0 &&\n                !map.terrain.findObstacles({ tile: t, onBridge: !!t.onBridgeLandType }, unit).length);\n            return tileFinder.getNextTile();\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/move/MoveInsideTask.ts",
    "content": "import { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nexport class MoveInsideTask extends MoveTask {\n    private target: any;\n    static chooseTargetFoundationTile(target: any, game: any) {\n        if (target.isBuilding()) {\n            let tile = target.centerTile;\n            if (!game.map.mapBounds.isWithinBounds(tile)) {\n                tile = game.map.tileOccupation\n                    .calculateTilesForGameObject(target.tile, target)\n                    .find((tile) => game.map.mapBounds.isWithinBounds(tile)) ?? target.tile;\n            }\n            return tile;\n        }\n        return target.tile;\n    }\n    constructor(game: any, target: any) {\n        super(game, MoveInsideTask.chooseTargetFoundationTile(target, game), false, {\n            ignoredBlockers: [target],\n            closeEnoughTiles: 0,\n        });\n        this.target = target;\n    }\n    hasReachedDestination(unit: any): boolean {\n        return (super.hasReachedDestination(unit) ||\n            this.canStopAtTile(unit, unit.tile, unit.onBridge));\n    }\n    canStopAtTile(unit: any, tile: any, onBridge: any): boolean {\n        const isOccupied = this.game.map.tileOccupation.isTileOccupiedBy(tile, this.target);\n        return (!this.isCancelling() || !isOccupied) && !(!this.isCancelling() && !isOccupied);\n    }\n    isCloseEnoughToDest(unit: any, tile: any, onBridge: any): boolean {\n        return this.game.map.tileOccupation.isTileOccupiedBy(tile, this.target);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/move/MoveOutsideTask.ts",
    "content": "import { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nexport class MoveOutsideTask extends MoveTask {\n    private target: any;\n    constructor(game: any, target: any, targetTile?: any) {\n        super(game, targetTile ?? target.tile, false, { ignoredBlockers: [target] });\n        this.target = target;\n        this.cancellable = false;\n    }\n    canStopAtTile(unit: any, tile: any, onBridge: any): boolean {\n        return (!this.game.map.tileOccupation.isTileOccupiedBy(tile, this.target) &&\n            super.canStopAtTile(unit, tile, onBridge));\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/move/MoveTargetTask.ts",
    "content": "import { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { MoveTrait, MoveState } from \"@/game/gameobject/trait/MoveTrait\";\nexport class MoveTargetTask extends MoveTask {\n    private target: any;\n    private tilesSinceTargetUpdate: number = 0;\n    constructor(game: any, target: any) {\n        super(game, target.tile, target.onBridge, {\n            forceMove: true,\n            pathFinderIgnoredBlockers: [target],\n        });\n        this.target = target;\n    }\n    onTick(unit: any): boolean {\n        if (!this.isCancelling() &&\n            unit.moveTrait.moveState === MoveState.ReachedNextWaypoint &&\n            !(this.target.tile === this.targetTile &&\n                this.target.onBridge === this.toBridge &&\n                this.target.moveTrait.isIdle())) {\n            let shouldUpdate = false;\n            let waypoint;\n            if ((unit.tile === this.targetTile && this.target.tile !== this.targetTile) ||\n                this.tilesSinceTargetUpdate++ > 10) {\n                shouldUpdate = true;\n            }\n            if (shouldUpdate) {\n                this.tilesSinceTargetUpdate = 0;\n                waypoint = this.target.moveTrait.currentWaypoint;\n                if (waypoint) {\n                    this.updateTarget(waypoint.tile, !!waypoint.onBridge);\n                }\n                else {\n                    this.updateTarget(this.target.tile, this.target.onBridge);\n                }\n            }\n        }\n        return super.onTick(unit);\n    }\n    forceCancel(unit: any): boolean {\n        return super.forceCancel(unit);\n    }\n    getTargetLinesConfig(unit: any): {\n        target: any;\n        pathNodes: any[];\n    } {\n        return { target: this.target, pathNodes: [] };\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/move/MoveTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nimport { Infantry } from \"@/game/gameobject/Infantry\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { findIndexReverse, findReverse } from \"@/util/array\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { MoveState, CollisionState, MoveResult, MoveTrait } from \"@/game/gameobject/trait/MoveTrait\";\nimport { WaitTicksTask } from \"@/game/gameobject/task/system/WaitTicksTask\";\nimport { MoveAsideTask } from \"@/game/gameobject/task/move/MoveAsideTask\";\nimport { MovePositionHelper } from \"@/game/gameobject/unit/MovePositionHelper\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport AppLogger from \"@/util/logger\";\nconst Logger = AppLogger;\nimport { Coords } from \"@/game/Coords\";\nimport { TaskStatus } from \"@/game/gameobject/task/system/TaskStatus\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { LocomotorFactory } from \"@/game/gameobject/locomotor/LocomotorFactory\";\nimport { RandomTileFinder } from \"@/game/map/tileFinder/RandomTileFinder\";\nimport { ObjectTeleportEvent } from \"@/game/event/ObjectTeleportEvent\";\nimport { NotifyTeleport } from \"@/game/gameobject/trait/interface/NotifyTeleport\";\nimport { PowerupType } from \"@/game/type/PowerupType\";\nimport { ScatterTask } from \"@/game/gameobject/task/ScatterTask\";\nimport { VeteranAbility } from \"@/game/gameobject/unit/VeteranAbility\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport type { Game } from \"@/game/Game\";\nimport type { Tile } from \"@/game/map/Tile\";\nimport type { GameObject } from \"@/game/gameobject/GameObject\";\nimport type { Bridge } from \"@/game/gameobject/Bridge\";\nimport type { Unit } from \"@/game/gameobject/Unit\";\nimport type { Locomotor } from \"@/game/gameobject/locomotor/Locomotor\";\nimport type { Weapon } from \"@/game/gameobject/Weapon\";\nconst VELOCITY_FACTOR = 1.5;\nconst MAX_PLANNING_TICKS = 200;\nconst WAIT_TICKS = 40;\nconst MAX_UNREACHABLE_TARGETS = 5;\ninterface MoveOptions {\n    targetOffset?: Vector2;\n    allowOutOfBoundsTarget?: boolean;\n    forceMove?: boolean;\n    strictCloseEnough?: boolean;\n    closeEnoughTiles?: number;\n    ignoredBlockers?: GameObject[];\n    pathFinderIgnoredBlockers?: GameObject[];\n    maxExpandedPathNodes?: number;\n    stopOnBlocker?: GameObject;\n    forceWaitOnPathBlocked?: boolean;\n}\ninterface PathNode {\n    tile: Tile;\n    onBridge?: Bridge;\n}\ninterface BlockedPathNode {\n    node: PathNode;\n    obj: GameObject;\n}\ninterface GroundPathPlan {\n    path: PathNode[];\n    ignoredBlockers: GameObject[];\n    blockedPathNodes: BlockedPathNode[];\n}\ninterface TargetLinesConfig {\n    pathNodes: PathNode[];\n    isRecalc?: boolean;\n}\ninterface UnreachableTarget {\n    tile: Tile;\n    toBridge: boolean;\n}\nexport class MoveTask extends Task {\n    protected game: Game;\n    protected targetTile: Tile;\n    protected toBridge: boolean;\n    protected options?: MoveOptions;\n    public preventOpportunityFire = false;\n    private logger: typeof Logger;\n    private destinationLeptons: Vector2;\n    private currentWaypointLeptons: Vector2;\n    private needsPathUpdate = false;\n    private targetChangeRequested = false;\n    private allObstaclesAreBlockers = false;\n    private blockedPathNodes: BlockedPathNode[] = [];\n    private unreachableTargets: UnreachableTarget[] = [];\n    private pushTried = false;\n    private cancelProcessed = false;\n    private cancelRepositionPending = false;\n    private targetLinesConfig: TargetLinesConfig;\n    private path?: PathNode[];\n    private groundPathPlan?: GroundPathPlan;\n    private targetOffset?: Vector2;\n    private inPlanningForTicks?: number;\n    constructor(game: Game, targetTile: Tile, toBridge: boolean, options?: MoveOptions) {\n        super();\n        this.game = game;\n        this.targetTile = targetTile;\n        this.toBridge = toBridge;\n        this.options = options;\n        this.logger = AppLogger.get(\"move\") as any;\n        this.destinationLeptons = new Vector2();\n        this.currentWaypointLeptons = new Vector2();\n        this.targetLinesConfig = { pathNodes: [] };\n    }\n    duplicate(): MoveTask {\n        return new MoveTask(this.game, this.targetTile, this.toBridge, this.options);\n    }\n    setForceMove(force: boolean): void {\n        if (force) {\n            this.options ??= {};\n            this.options.forceMove = true;\n        }\n        else if (this.options?.forceMove) {\n            this.options.forceMove = undefined;\n        }\n    }\n    onStart(unit: Unit): void {\n        if (unit.moveTrait.currentWaypoint) {\n            throw new Error(\"Nested move tasks are not supported\");\n        }\n        if (unit.moveTrait.locomotor === undefined) {\n            unit.moveTrait.locomotor = new LocomotorFactory(this.game).create(unit);\n        }\n        if (unit.moveTrait.lastTargetOffset) {\n            this.targetOffset = unit.moveTrait.lastTargetOffset;\n        }\n        else {\n            this.targetOffset = this.computeTargetOffset(unit);\n        }\n        if (unit.moveTrait.lastVelocity) {\n            unit.moveTrait.velocity = unit.moveTrait.lastVelocity;\n        }\n        if (!this.path) {\n            if (this.groundPathPlan) {\n                if (this.groundPathPlan.path[this.groundPathPlan.path.length - 1].tile === unit.tile) {\n                    this.path = this.applyGroundPathPlan(this.groundPathPlan);\n                }\n                else {\n                    this.computePath(unit, unit.moveTrait.locomotor);\n                }\n                this.groundPathPlan = undefined;\n            }\n            else {\n                this.computePath(unit, unit.moveTrait.locomotor);\n            }\n            this.targetLinesConfig.isRecalc = false;\n        }\n        this.updateDestination(this.path, this.targetOffset);\n        unit.moveTrait.moveState = MoveState.ReachedNextWaypoint;\n        unit.moveTrait.lastMoveResult = undefined;\n        unit.moveTrait.lastTargetOffset = undefined;\n        unit.moveTrait.lastVelocity = undefined;\n    }\n    private computeTargetOffset(unit: Unit): Vector2 {\n        return this.options?.targetOffset ??\n            (unit.isInfantry()\n                ? unit.position.getTileOffset()\n                : unit.position.computeSubCellOffset(0));\n    }\n    private computePath(unit: Unit, locomotor: Locomotor): void {\n        let path: PathNode[];\n        if (!this.options?.allowOutOfBoundsTarget &&\n            !this.game.map.mapBounds.isWithinBounds(this.targetTile)) {\n            path = [];\n        }\n        else if (unit.rules.movementZone === MovementZone.Fly) {\n            path = this.computeAirPath(unit);\n        }\n        else if ((locomotor as any).ignoresTerrain) {\n            path = this.computeDirectJumpPath(unit);\n        }\n        else {\n            const plan = this.computeGroundPath(unit);\n            path = this.applyGroundPathPlan(plan);\n        }\n        if (unit.rules.movementZone === MovementZone.Fly) {\n            this.targetLinesConfig.pathNodes = path.map(({ tile, onBridge }) => ({\n                tile,\n                onBridge\n            }));\n            if (path.length) {\n                this.targetLinesConfig.pathNodes[0].onBridge = this.toBridge\n                    ? this.game.map.tileOccupation.getBridgeOnTile(this.targetTile)\n                    : undefined;\n            }\n        }\n        else {\n            this.targetLinesConfig.pathNodes = path;\n        }\n        this.path = path;\n    }\n    private computeAirPath(unit: Unit): PathNode[] {\n        return [\n            { tile: this.targetTile, onBridge: undefined },\n            { tile: unit.tile, onBridge: undefined }\n        ];\n    }\n    private computeDirectJumpPath(unit: Unit): PathNode[] {\n        const map = this.game.map;\n        const unitBridge = unit.onBridge\n            ? map.tileOccupation.getBridgeOnTile(unit.tile)\n            : undefined;\n        let targetTile = this.targetTile;\n        let targetBridge = this.toBridge\n            ? map.tileOccupation.getBridgeOnTile(this.targetTile)\n            : undefined;\n        const ignoredBlockers = this.options?.ignoredBlockers;\n        const finder = new RadialTileFinder(map.tiles, map.mapBounds, targetTile, { width: 1, height: 1 }, 0, 5, (tile) => map.terrain.getPassableSpeed(tile, unit.rules.speedType, unit.isInfantry(), !!tile.onBridgeLandType, ignoredBlockers) > 0 &&\n            !map.terrain\n                .findObstacles({ tile, onBridge: !!tile.onBridgeLandType }, unit)\n                .find((obstacle) => !ignoredBlockers?.includes(obstacle.obj)));\n        const newTile = finder.getNextTile();\n        if (!newTile) {\n            return [];\n        }\n        if (newTile !== targetTile) {\n            targetTile = newTile;\n            targetBridge = map.tileOccupation.getBridgeOnTile(targetTile);\n        }\n        return [\n            { tile: targetTile, onBridge: targetBridge },\n            { tile: unit.tile, onBridge: unitBridge }\n        ];\n    }\n    private computeGroundPath(unit: Unit): GroundPathPlan {\n        let startTile = unit.tile;\n        let startBridge = unit.onBridge\n            ? this.game.map.tileOccupation.getBridgeOnTile(startTile)\n            : undefined;\n        if (unit.moveTrait.moveState === MoveState.Moving &&\n            unit.moveTrait.currentWaypoint) {\n            startTile = unit.moveTrait.currentWaypoint.tile;\n            startBridge = unit.moveTrait.currentWaypoint.onBridge;\n        }\n        const plan: GroundPathPlan = {\n            path: [],\n            ignoredBlockers: [],\n            blockedPathNodes: []\n        };\n        const startBuilding = this.game.map\n            .getObjectsOnTile(startTile)\n            .find((obj) => obj.isBuilding());\n        if (startBuilding &&\n            !this.game.map.terrain.getPassableSpeed(startTile, unit.rules.speedType, unit.isInfantry(), false)) {\n            const isIgnored = this.options?.ignoredBlockers?.includes(startBuilding);\n            if (!isIgnored) {\n                plan.ignoredBlockers.push(startBuilding);\n            }\n            if (!isIgnored && startBuilding.dockTrait) {\n                const dockTiles = new Set(startBuilding.dockTrait?.getAllDockTiles());\n                const buildingTiles = this.game.map.tileOccupation.calculateTilesForGameObject(startBuilding.tile, startBuilding);\n                buildingTiles\n                    .filter((tile) => !dockTiles.has(tile))\n                    .forEach((tile) => plan.blockedPathNodes.push({\n                    node: { tile, onBridge: undefined },\n                    obj: startBuilding\n                }));\n            }\n        }\n        const disguisedUnit = this.game.map\n            .getGroundObjectsOnTile(this.targetTile)\n            .find((obj) => (obj.isInfantry() || obj.isVehicle()) &&\n            obj.disguiseTrait?.hasTerrainDisguise() &&\n            !(this.game.alliances.haveSharedIntel(unit.owner, obj.owner) ||\n                obj.owner.sharedDetectDisguiseTrait?.has(unit)));\n        if (disguisedUnit) {\n            const bridge = this.toBridge\n                ? this.game.map.tileOccupation.getBridgeOnTile(this.targetTile)\n                : undefined;\n            plan.blockedPathNodes.push({\n                node: { tile: this.targetTile, onBridge: bridge },\n                obj: disguisedUnit\n            });\n        }\n        const allIgnoredBlockers = [\n            ...new Set([\n                ...(this.options?.ignoredBlockers ?? []),\n                ...(this.options?.pathFinderIgnoredBlockers ?? []),\n                ...plan.ignoredBlockers\n            ])\n        ];\n        const allBlockedNodes = [...this.blockedPathNodes, ...plan.blockedPathNodes];\n        const path = this.game.map.terrain.computePath(unit.rules.speedType, unit.isInfantry(), startTile, !!startBridge, this.targetTile, this.toBridge, {\n            maxExpandedNodes: this.allObstaclesAreBlockers\n                ? Math.min(300, this.options?.maxExpandedPathNodes ?? Number.POSITIVE_INFINITY)\n                : this.options?.maxExpandedPathNodes,\n            bestEffort: !this.options?.strictCloseEnough,\n            ignoredBlockers: allIgnoredBlockers,\n            excludeTiles: this.allObstaclesAreBlockers || allBlockedNodes.length\n                ? (node) => this.nodeIsBlockedForPathfinding(node, unit, allIgnoredBlockers, allBlockedNodes)\n                : undefined\n        });\n        plan.path = path;\n        return plan;\n    }\n    private nodeIsBlockedForPathfinding(node: PathNode, unit: Unit, ignoredBlockers: GameObject[], blockedNodes: BlockedPathNode[]): boolean {\n        if (this.allObstaclesAreBlockers) {\n            return !!this.game.map.terrain\n                .findObstacles(node, unit)\n                .find((obstacle) => !ignoredBlockers?.includes(obstacle.obj));\n        }\n        return !!blockedNodes.find(({ node: blockedNode }) => blockedNode.tile === node.tile && blockedNode.onBridge === node.onBridge);\n    }\n    private applyGroundPathPlan(plan: GroundPathPlan): PathNode[] {\n        this.blockedPathNodes = this.blockedPathNodes.filter((blocked) => blocked.obj.isSpawned && blocked.node.tile === blocked.obj.tile);\n        if (plan.ignoredBlockers.length) {\n            this.options ??= {};\n            this.options.ignoredBlockers ??= [];\n            this.options.ignoredBlockers.push(...plan.ignoredBlockers);\n        }\n        this.blockedPathNodes.push(...plan.blockedPathNodes);\n        return plan.path;\n    }\n    private updateDestination(path: PathNode[], offset: Vector2): void {\n        const tile = path.length ? path[0].tile : this.targetTile;\n        this.destinationLeptons\n            .set(tile.rx * Coords.LEPTONS_PER_TILE, tile.ry * Coords.LEPTONS_PER_TILE)\n            .add(offset);\n    }\n    protected canStopAtTile(unit: Unit, tile: Tile, onBridge: boolean): boolean {\n        if (unit.zone === ZoneType.Air) {\n            if ((!unit.isAircraft() || !unit.airportBoundTrait) &&\n                !unit.rules.spawned &&\n                (!this.options?.forceMove ||\n                    !unit.rules.balloonHover ||\n                    unit.rules.hoverAttack) &&\n                (!this.game.map.terrain.getPassableSpeed(tile, SpeedType.Amphibious, false, onBridge) ||\n                    this.game.map\n                        .getObjectsOnTile(tile)\n                        .filter((obj) => (obj.isBuilding() &&\n                        !obj.isDestroyed &&\n                        !obj.dockTrait?.hasReservedDockForUnit(unit) &&\n                        !unit.rules.dock.includes(obj.name)) ||\n                        (obj.isUnit() &&\n                            obj.tile === tile &&\n                            obj.moveTrait.moveState !== MoveState.Moving &&\n                            obj !== unit)).length)) {\n                return false;\n            }\n        }\n        else if (unit.isInfantry()) {\n            const infantryOnTile = this.game.map\n                .getGroundObjectsOnTile(tile)\n                .filter((obj) => obj.isInfantry() &&\n                obj.tile === tile &&\n                obj.onBridge === onBridge &&\n                obj.moveTrait.moveState !== MoveState.Moving &&\n                obj !== unit);\n            if (infantryOnTile.length > 2 ||\n                infantryOnTile.find((inf) => inf.position.subCell === unit.position.subCell)) {\n                return false;\n            }\n        }\n        if (unit.zone !== ZoneType.Air &&\n            unit.rules.tooBigToFitUnderBridge &&\n            !onBridge &&\n            tile.onBridgeLandType &&\n            this.game.map.tileOccupation\n                .getBridgeOnTile(tile)\n                ?.isHighBridge()) {\n            return false;\n        }\n        if (!this.isCancelling() &&\n            this.options?.strictCloseEnough &&\n            this.options?.closeEnoughTiles !== undefined &&\n            !this.isCloseEnoughToDest(unit, tile, this.options.closeEnoughTiles)) {\n            return false;\n        }\n        return true;\n    }\n    protected isCloseEnoughToDest(unit: Unit, tile: Tile, maxDistance?: number): boolean {\n        if (maxDistance === undefined) {\n            return true;\n        }\n        const rangeHelper = new RangeHelper(this.game.map.tileOccupation);\n        return rangeHelper.tileDistance(this.targetTile, tile) <= maxDistance;\n    }\n    protected hasReachedDestination(unit: Unit): boolean {\n        return !this.path!.length;\n    }\n    updateTarget(tile: Tile, toBridge: boolean): void {\n        this.targetTile = tile;\n        this.toBridge = toBridge;\n        this.needsPathUpdate = true;\n        this.targetChangeRequested = true;\n    }\n    onEnd(unit: Unit): void {\n        unit.moveTrait.collisionState = CollisionState.Resolved;\n        unit.moveTrait.currentWaypoint = undefined;\n        if (!this.targetOffset!.equals(this.computeTargetOffset(unit))) {\n            unit.moveTrait.lastTargetOffset = this.targetOffset;\n        }\n    }\n    forceCancel(unit: Unit): boolean {\n        if (!this.cancellable || this.children.some((child) => !child.cancellable)) {\n            return false;\n        }\n        if (!this.options?.allowOutOfBoundsTarget &&\n            !this.game.map.isWithinBounds(unit.tile)) {\n            return false;\n        }\n        if (this.status === TaskStatus.Running || this.status === TaskStatus.Cancelling) {\n            unit.moveTrait.unreservePathNodes();\n            unit.moveTrait.lastMoveResult = MoveResult.Cancel;\n            this.onEnd(unit);\n            unit.moveTrait.lastTargetOffset = this.targetOffset;\n            unit.moveTrait.lastVelocity = unit.moveTrait.velocity.clone();\n        }\n        this.status = TaskStatus.Cancelled;\n        return true;\n    }\n    onTick(unit: Unit): boolean {\n        if (unit.moveTrait.isDisabled() &&\n            unit.moveTrait.moveState === MoveState.ReachedNextWaypoint) {\n            if (this.isCancelling()) {\n                unit.moveTrait.lastMoveResult = MoveResult.Cancel;\n                return true;\n            }\n            return false;\n        }\n        if (this.needsPathUpdate) {\n            if (unit.moveTrait.moveState === MoveState.PlanMove) {\n                this.inPlanningForTicks = undefined;\n                unit.moveTrait.currentWaypoint = undefined;\n                unit.moveTrait.collisionState = CollisionState.Resolved;\n                unit.moveTrait.moveState = MoveState.ReachedNextWaypoint;\n                unit.moveTrait.velocity.set(0, 0, 0);\n            }\n            this.computePath(unit, unit.moveTrait.locomotor);\n            if (!this.path!.length) {\n                this.unreachableTargets.push({\n                    tile: this.targetTile,\n                    toBridge: this.toBridge\n                });\n            }\n            this.updateDestination(this.path!, this.targetOffset!);\n            this.targetLinesConfig.isRecalc = !this.targetChangeRequested;\n            this.targetChangeRequested = false;\n            this.needsPathUpdate = false;\n            this.allObstaclesAreBlockers = false;\n        }\n        const map = this.game.map;\n        if (unit.moveTrait.moveState === MoveState.ReachedNextWaypoint) {\n            unit.moveTrait.unreservePathNodes();\n            const waypointIndex = this.path!.findIndex((node) => node === unit.moveTrait.currentWaypoint);\n            if (waypointIndex !== -1) {\n                this.path!.splice(waypointIndex);\n            }\n            else {\n                this.path!.pop();\n            }\n            unit.moveTrait.currentWaypoint = undefined;\n            if (this.isCancelling() ? !this.cancelProcessed : this.hasReachedDestination(unit)) {\n                const notCloseEnough = !this.isCancelling() &&\n                    !this.isCloseEnoughToDest(unit, unit.tile, this.options?.closeEnoughTiles);\n                if (!notCloseEnough && this.canStopAtTile(unit, unit.tile, unit.onBridge)) {\n                    unit.moveTrait.lastMoveResult = this.isCancelling()\n                        ? MoveResult.Cancel\n                        : MoveResult.Success;\n                    return true;\n                }\n                if (this.unreachableTargets.length > MAX_UNREACHABLE_TARGETS) {\n                    unit.moveTrait.lastMoveResult = MoveResult.Fail;\n                    this.log(unit, \"bail_max_unreachable_dest\");\n                    return true;\n                }\n                let relocTile = unit.tile;\n                let relocBridge = unit.onBridge\n                    ? map.tileOccupation.getBridgeOnTile(relocTile)\n                    : undefined;\n                if (notCloseEnough) {\n                    relocTile = this.targetTile;\n                    relocBridge = this.toBridge\n                        ? map.tileOccupation.getBridgeOnTile(relocTile)\n                        : undefined;\n                }\n                const newTile = this.findRelocationTile(relocTile, relocBridge, unit);\n                if (!newTile) {\n                    unit.moveTrait.lastMoveResult = notCloseEnough\n                        ? MoveResult.Fail\n                        : MoveResult.CloseEnough;\n                    this.log(unit, \"bail_no_free_dest\");\n                    return true;\n                }\n                const newBridge = !relocBridge || relocBridge.isHighBridge()\n                    ? map.tileOccupation.getBridgeOnTile(newTile)\n                    : undefined;\n                this.updateTarget(newTile, !!newBridge);\n                if (this.isCancelling()) {\n                    this.cancelProcessed = true;\n                    this.cancelRepositionPending = true;\n                }\n                return false;\n            }\n            if (this.cancelProcessed && !this.path!.length) {\n                unit.moveTrait.lastMoveResult = MoveResult.Cancel;\n                return true;\n            }\n            this.cancelProcessed = false;\n            unit.moveTrait.moveState = MoveState.PlanMove;\n            const locomotor = unit.moveTrait.locomotor;\n            unit.moveTrait.currentWaypoint = locomotor.selectNextWaypoint\n                ? locomotor.selectNextWaypoint(unit, this.path!)\n                : this.path![this.path!.length - 1];\n            this.currentWaypointLeptons\n                .set(unit.moveTrait.currentWaypoint.tile.rx, unit.moveTrait.currentWaypoint.tile.ry)\n                .multiplyScalar(Coords.LEPTONS_PER_TILE)\n                .add(this.targetOffset!);\n            const newWaypointTasks = locomotor.onNewWaypoint(unit, this.currentWaypointLeptons, this.destinationLeptons);\n            if (newWaypointTasks) {\n                this.children.push(...newWaypointTasks);\n                return false;\n            }\n        }\n        if (unit.moveTrait.moveState === MoveState.PlanMove) {\n            if (this.isCancelling() && !this.cancelRepositionPending) {\n                unit.moveTrait.currentWaypoint = undefined;\n                unit.moveTrait.moveState = MoveState.ReachedNextWaypoint;\n                return this.onTick(unit);\n            }\n            this.inPlanningForTicks = this.inPlanningForTicks === undefined\n                ? 0\n                : this.inPlanningForTicks + 1;\n            if (this.inPlanningForTicks > MAX_PLANNING_TICKS) {\n                this.needsPathUpdate = true;\n                this.allObstaclesAreBlockers = true;\n                unit.moveTrait.velocity.set(0, 0, 0);\n                this.log(unit, \"repath_plan_timeout\");\n                return false;\n            }\n            if (unit.rules.movementZone !== MovementZone.Fly &&\n                !unit.moveTrait.locomotor.ignoresTerrain) {\n                const pathToCheck = this.path!\n                    .slice(this.path!.indexOf(unit.moveTrait.currentWaypoint!))\n                    .reverse();\n                const currentVelocity = unit.moveTrait.velocity.length();\n                for (const node of pathToCheck) {\n                    if (node.onBridge?.isDestroyed) {\n                        node.onBridge = undefined;\n                    }\n                }\n                for (const node of pathToCheck) {\n                    if (!map.terrain.getPassableSpeed(node.tile, unit.rules.speedType, unit.isInfantry(), !!node.onBridge, this.options?.ignoredBlockers)) {\n                        if (this.options?.stopOnBlocker &&\n                            map.terrain\n                                .findObstacles(node, unit)\n                                .some((obstacle) => obstacle.obj === this.options.stopOnBlocker)) {\n                            unit.moveTrait.lastMoveResult = MoveResult.CloseEnough;\n                            return true;\n                        }\n                        this.needsPathUpdate = true;\n                        unit.moveTrait.currentWaypoint = undefined;\n                        unit.moveTrait.moveState = MoveState.ReachedNextWaypoint;\n                        return this.onTick(unit);\n                    }\n                    if (!node.onBridge) {\n                        const crate = map\n                            .getGroundObjectsOnTile(node.tile)\n                            .find((obj) => obj.isOverlay() && obj.rules.crate);\n                        if (crate) {\n                            if (this.game.crateGeneratorTrait.peekInsideCrate(crate) === PowerupType.Unit) {\n                                this.game.crateGeneratorTrait.pickupCrate(unit, crate, this.game);\n                                const spawnedUnit = this.game.map\n                                    .getGroundObjectsOnTile(node.tile)\n                                    .find((obj) => obj.isUnit() && !obj.onBridge);\n                                if (spawnedUnit) {\n                                    this.needsPathUpdate = true;\n                                    this.blockedPathNodes.push({ node, obj: spawnedUnit });\n                                    unit.moveTrait.currentWaypoint = undefined;\n                                    unit.moveTrait.moveState = MoveState.ReachedNextWaypoint;\n                                    return this.onTick(unit);\n                                }\n                            }\n                        }\n                    }\n                    const obstacles = map.terrain\n                        .findObstacles(node, unit)\n                        .filter((obstacle) => !this.options?.ignoredBlockers?.includes(obstacle.obj));\n                    for (const obstacle of obstacles) {\n                        if (obstacle.static) {\n                            this.needsPathUpdate = true;\n                            unit.moveTrait.currentWaypoint = undefined;\n                            unit.moveTrait.moveState = MoveState.ReachedNextWaypoint;\n                            return this.onTick(unit);\n                        }\n                        if (obstacle.obj.rules.crushable) {\n                            if ([SpeedType.Track, SpeedType.Hover].includes(unit.rules.speedType) &&\n                                unit.crusher &&\n                                (!obstacle.obj.isTechno() || !this.game.areFriendly(obstacle.obj, unit))) {\n                                continue;\n                            }\n                            if (!obstacle.obj.isTechno()) {\n                                this.needsPathUpdate = true;\n                                unit.moveTrait.currentWaypoint = undefined;\n                                unit.moveTrait.moveState = MoveState.ReachedNextWaypoint;\n                                return this.onTick(unit);\n                            }\n                        }\n                        if (obstacle.obj.isTerrain()) {\n                            if (!unit.isInfantry()) {\n                                throw new Error(`Obstacle ${obstacle.obj.name} should be a blocker for non infantry`);\n                            }\n                            const freeSubCell = this.findFreeSubCell(unit, node);\n                            if (freeSubCell !== undefined) {\n                                this.relocateToSubCell(unit, freeSubCell);\n                            }\n                            else {\n                                this.needsPathUpdate = true;\n                                this.blockedPathNodes.push({ node, obj: obstacle.obj });\n                                unit.moveTrait.currentWaypoint = undefined;\n                                unit.moveTrait.moveState = MoveState.ReachedNextWaypoint;\n                            }\n                            return this.onTick(unit);\n                        }\n                        if (!obstacle.obj.isTechno()) {\n                            throw new Error(\"Unexpected obstacle of type \" + obstacle.obj.type);\n                        }\n                        const blocker = obstacle.obj as Unit;\n                        const blockerVelocity = blocker.isUnit() ? blocker.moveTrait.velocity.length() : 0;\n                        if (blocker.isAircraft() &&\n                            blocker.zone === ZoneType.Ground &&\n                            this.options?.ignoredBlockers?.some((ignored) => ignored.isBuilding() && ignored.dockTrait?.isDocked(blocker))) {\n                            continue;\n                        }\n                        if (pathToCheck.length === 1 &&\n                            blocker.isUnit() &&\n                            blockerVelocity &&\n                            currentVelocity &&\n                            currentVelocity <= blockerVelocity &&\n                            unit.direction === blocker.direction &&\n                            blocker.tile === node.tile &&\n                            blocker.moveTrait.currentWaypoint?.tile !== node.tile) {\n                            break;\n                        }\n                        if (blocker.isBuilding() ||\n                            blocker.moveTrait.moveState === MoveState.Idle ||\n                            blocker.moveTrait.collisionState !== CollisionState.Resolved) {\n                            if (!currentVelocity &&\n                                unit.moveTrait.collisionState !== CollisionState.Resolved &&\n                                blocker.isUnit() &&\n                                blocker.moveTrait.collisionState !== CollisionState.Resolved) {\n                                if (this.inPlanningForTicks + 1 > MAX_PLANNING_TICKS) {\n                                    this.needsPathUpdate = true;\n                                    this.allObstaclesAreBlockers = true;\n                                    this.log(unit, \"repath_waited_too_long_blocker \" + blocker.id);\n                                    unit.moveTrait.velocity.set(0, 0, 0);\n                                }\n                                return false;\n                            }\n                            if (blocker.isInfantry() &&\n                                unit.isInfantry() &&\n                                blocker.moveTrait.collisionState === CollisionState.Resolved) {\n                                const freeSubCell = this.findFreeSubCell(unit, node);\n                                if (freeSubCell !== undefined) {\n                                    this.relocateToSubCell(unit, freeSubCell);\n                                    return this.onTick(unit);\n                                }\n                            }\n                            const freeWaypointIndex = findIndexReverse(this.path!.slice(0, this.path!.indexOf(node)), (waypoint) => !map.terrain\n                                .findObstacles(waypoint, unit)\n                                .filter((obstacle) => !this.options?.ignoredBlockers?.includes(obstacle.obj)).length);\n                            if (freeWaypointIndex === -1) {\n                                if (this.canStopAtTile(unit, unit.tile, unit.onBridge) &&\n                                    this.isCloseEnoughToDest(unit, unit.tile, this.options?.closeEnoughTiles)) {\n                                    unit.moveTrait.lastMoveResult = MoveResult.CloseEnough;\n                                    this.log(unit, \"bail_waypoints_blocked_close_enough\");\n                                    return true;\n                                }\n                                if (!(this.options?.closeEnoughTiles === 0 ||\n                                    (Math.abs(unit.tile.rx - this.targetTile.rx) <= 1 &&\n                                        Math.abs(unit.tile.ry - this.targetTile.ry) <= 1))) {\n                                    this.needsPathUpdate = true;\n                                    this.blockedPathNodes.push(...this.path!\n                                        .slice(0, this.path!.indexOf(node) + 1)\n                                        .map((waypoint) => ({\n                                        node: waypoint,\n                                        obj: map.terrain.findObstacles(waypoint, unit)[0].obj\n                                    })));\n                                    unit.moveTrait.velocity.set(0, 0, 0);\n                                    this.log(unit, \"repath_waypoints_blocked_too_far\");\n                                    return false;\n                                }\n                            }\n                            let alternatePath: PathNode[] = [];\n                            if (freeWaypointIndex !== -1) {\n                                const targetWaypoint = this.path![freeWaypointIndex];\n                                alternatePath = map.terrain.computePath(unit.rules.speedType, unit.isInfantry(), unit.tile, unit.onBridge, targetWaypoint.tile, !!targetWaypoint.onBridge, {\n                                    maxExpandedNodes: 15,\n                                    bestEffort: false,\n                                    excludeTiles: (testNode) => !!map.terrain\n                                        .findObstacles(testNode, unit)\n                                        .filter((obstacle) => !this.options?.ignoredBlockers?.includes(obstacle.obj)).length,\n                                    ignoredBlockers: this.options?.ignoredBlockers\n                                });\n                            }\n                            if (!alternatePath.length &&\n                                blocker.owner === unit.owner &&\n                                pathToCheck.length === 1) {\n                            }\n                            else if (alternatePath.length) {\n                                this.path!.splice(freeWaypointIndex, this.path!.length, ...alternatePath);\n                                unit.moveTrait.currentWaypoint = undefined;\n                                unit.moveTrait.moveState = MoveState.ReachedNextWaypoint;\n                                return this.onTick(unit);\n                            }\n                            else {\n                                const weapon = this.selectWeaponVsObstacle(unit, blocker);\n                                if (weapon) {\n                                    this.children.push(unit.attackTrait.createAttackTask(this.game, blocker, blocker.tile, weapon, { passive: true, holdGround: true }));\n                                    unit.moveTrait.velocity.set(0, 0, 0);\n                                }\n                                else if (this.options?.forceWaitOnPathBlocked) {\n                                    this.children.push(new WaitTicksTask(WAIT_TICKS));\n                                    this.inPlanningForTicks = 0;\n                                    unit.moveTrait.velocity.set(0, 0, 0);\n                                    unit.moveTrait.collisionState = CollisionState.Waiting;\n                                }\n                                else {\n                                    this.needsPathUpdate = true;\n                                    this.blockedPathNodes.push({ node, obj: blocker });\n                                    if (blocker.isBuilding()) {\n                                        this.allObstaclesAreBlockers = true;\n                                    }\n                                    this.log(unit, \"repath_unavoidable_blocker \" + blocker.id);\n                                    unit.moveTrait.velocity.set(0, 0, 0);\n                                }\n                                return false;\n                            }\n                            const blockerHasTasks = blocker.unitOrderTrait.hasTasks();\n                            if (this.pushTried ||\n                                blocker.isBuilding() ||\n                                blocker.moveTrait.collisionState === CollisionState.Waiting ||\n                                blockerHasTasks ||\n                                (blocker.isAircraft() && blocker.missileSpawnTrait)) {\n                                if (!this.options?.forceWaitOnPathBlocked &&\n                                    (blocker.isBuilding() ||\n                                        (blockerHasTasks && blocker.moveTrait.moveState === MoveState.Idle) ||\n                                        this.inPlanningForTicks + WAIT_TICKS > MAX_PLANNING_TICKS)) {\n                                    this.needsPathUpdate = true;\n                                    this.allObstaclesAreBlockers = true;\n                                    this.log(unit, \"repath_blocker_busy_wait_timeout \" + blocker.id);\n                                    unit.moveTrait.velocity.set(0, 0, 0);\n                                }\n                                else {\n                                    this.children.push(new WaitTicksTask(WAIT_TICKS));\n                                    if (this.options?.forceWaitOnPathBlocked) {\n                                        this.inPlanningForTicks = 0;\n                                    }\n                                    else {\n                                        this.inPlanningForTicks += WAIT_TICKS;\n                                    }\n                                    unit.moveTrait.velocity.set(0, 0, 0);\n                                    unit.moveTrait.collisionState = CollisionState.Waiting;\n                                }\n                                return false;\n                            }\n                            const pushDirection = new Vector2(blocker.tile.rx - unit.tile.rx, blocker.tile.ry - unit.tile.ry);\n                            this.pushTried = true;\n                            blocker.unitOrderTrait.addTask(new MoveAsideTask(this.game, pushDirection));\n                            this.children.push(new WaitTicksTask(1));\n                            unit.moveTrait.velocity.set(0, 0, 0);\n                            unit.moveTrait.collisionState = CollisionState.Waiting;\n                            this.log(unit, \"push \" + blocker.id);\n                            return false;\n                        }\n                        if (blocker.isInfantry() && unit.isInfantry()) {\n                            const freeSubCell = this.findFreeSubCell(unit, node);\n                            if (freeSubCell !== undefined) {\n                                this.relocateToSubCell(unit, freeSubCell);\n                                return this.onTick(unit);\n                            }\n                        }\n                        if (!currentVelocity) {\n                            if (this.inPlanningForTicks > WAIT_TICKS) {\n                                unit.moveTrait.collisionState = CollisionState.Waiting;\n                            }\n                            return false;\n                        }\n                        if (Math.abs(unit.direction - blocker.direction) === 180) {\n                            unit.moveTrait.velocity.set(0, 0, 0);\n                            unit.moveTrait.collisionState = CollisionState.Waiting;\n                            return false;\n                        }\n                        if (Math.abs(unit.direction - blocker.direction) <= 45 &&\n                            blockerVelocity * VELOCITY_FACTOR < currentVelocity) {\n                            const nodeIndex = this.path!.indexOf(node);\n                            if (nodeIndex >= 5) {\n                                const backtrackIndex = findIndexReverse(this.path!.slice(0, nodeIndex - 5), (waypoint) => !map.terrain.findObstacles(waypoint, unit).length);\n                                if (backtrackIndex !== -1) {\n                                    const backtrackTarget = this.path![backtrackIndex];\n                                    const backtrackPath = map.terrain.computePath(unit.rules.speedType, unit.isInfantry(), unit.tile, unit.onBridge, backtrackTarget.tile, !!backtrackTarget.onBridge, {\n                                        maxExpandedNodes: 15,\n                                        bestEffort: false,\n                                        excludeTiles: (testNode) => !!map.terrain.findObstacles(testNode, unit).length ||\n                                            this.path!.findIndex((waypoint) => waypoint.tile === testNode.tile &&\n                                                waypoint.onBridge === testNode.onBridge) > backtrackIndex\n                                    });\n                                    if (backtrackPath.length) {\n                                        this.path!.splice(backtrackIndex, this.path!.length, ...backtrackPath);\n                                        unit.moveTrait.currentWaypoint = undefined;\n                                        unit.moveTrait.moveState = MoveState.ReachedNextWaypoint;\n                                        return this.onTick(unit);\n                                    }\n                                }\n                            }\n                            unit.moveTrait.collisionState = CollisionState.Waiting;\n                            unit.moveTrait.velocity.set(0, 0, 0);\n                            return false;\n                        }\n                        unit.moveTrait.velocity.set(0, 0, 0);\n                        unit.moveTrait.collisionState = CollisionState.Waiting;\n                        return false;\n                    }\n                }\n                if (unit.rules.speedType === SpeedType.Track && currentVelocity) {\n                    const currentIndex = this.path!.indexOf(unit.moveTrait.currentWaypoint!);\n                    if (currentIndex > 0) {\n                        const nextNode = this.path![currentIndex - 1];\n                        for (const crushable of map\n                            .getGroundObjectsOnTile(nextNode.tile)\n                            .filter((obj) => obj.isUnit() &&\n                            obj.onBridge === !!nextNode.onBridge &&\n                            obj.rules.crushable &&\n                            obj.veteranTrait?.hasVeteranAbility(VeteranAbility.SCATTER) &&\n                            !this.game.areFriendly(obj, unit))) {\n                            if (!crushable.unitOrderTrait.hasTasks()) {\n                                crushable.unitOrderTrait.addTask(new ScatterTask(this.game, undefined, undefined));\n                            }\n                        }\n                    }\n                }\n                if (!unit.moveTrait.reservedPathNodes.length) {\n                    unit.moveTrait.reservedPathNodes.push(...pathToCheck);\n                    pathToCheck.forEach((node) => {\n                        map.tileOccupation.occupySingleTile(node.tile, unit);\n                    });\n                }\n            }\n            unit.moveTrait.moveState = MoveState.Moving;\n            this.inPlanningForTicks = undefined;\n            this.unreachableTargets.length = 0;\n            this.pushTried = false;\n            if (unit.moveTrait.collisionState === CollisionState.Waiting) {\n                unit.moveTrait.collisionState = CollisionState.Resolved;\n            }\n        }\n        if (unit.moveTrait.moveState === MoveState.Moving) {\n            const locomotor = unit.moveTrait.locomotor;\n            const { distance, done, isTeleport } = locomotor.tick(unit, this.currentWaypointLeptons, this.destinationLeptons, (this.isCancelling() || !this.path!.length) && !this.cancelRepositionPending);\n            if (isTeleport) {\n                unit.traits.filter(NotifyTeleport).forEach((trait) => {\n                    trait[NotifyTeleport.onBeforeTeleport](unit, this.game, true, true);\n                });\n            }\n            if (distance.length()) {\n                const oldTile = unit.tile;\n                const allowOutOfBounds = locomotor.allowOutOfBounds;\n                if (distance.y) {\n                    const oldElevation = unit.tileElevation;\n                    unit.position.moveByLeptons3(distance, allowOutOfBounds);\n                    unit.moveTrait.handleElevationChange(oldElevation, this.game);\n                }\n                else {\n                    unit.position.moveByLeptons(distance.x, distance.z, allowOutOfBounds);\n                }\n                if (unit.tile !== oldTile) {\n                    const oldBridge = unit.onBridge\n                        ? this.game.map.tileOccupation.getBridgeOnTile(oldTile)\n                        : undefined;\n                    const currentNode = findReverse(this.path!, (node) => node.tile === unit.tile);\n                    const newBridge = currentNode\n                        ? currentNode.onBridge\n                        : oldBridge || unit.moveTrait.currentWaypoint!.onBridge\n                            ? this.game.map.tileOccupation.getBridgeOnTile(unit.tile)\n                            : undefined;\n                    unit.moveTrait.handleTileChange(oldTile, newBridge, false, this.game, isTeleport);\n                    if (isTeleport) {\n                        unit.moveTrait.lastTeleportTick = this.game.currentTick;\n                        this.game.events.dispatch(new ObjectTeleportEvent(unit, true, oldTile));\n                    }\n                    if (unit.isDestroyed) {\n                        return true;\n                    }\n                }\n            }\n            if (done) {\n                unit.moveTrait.moveState = MoveState.ReachedNextWaypoint;\n                return this.onTick(unit);\n            }\n        }\n        return false;\n    }\n    private selectWeaponVsObstacle(unit: Unit, target: GameObject): Weapon | undefined {\n        if (this.game.areFriendly(target, unit) ||\n            !unit.attackTrait ||\n            unit.attackTrait.isDisabled() ||\n            !unit.attackTrait.isIdle()) {\n            return undefined;\n        }\n        const weapon = unit.attackTrait.selectWeaponVersus(unit, target, this.game, false, true);\n        if (!weapon ||\n            weapon.name === unit.armedTrait?.deathWeapon?.name ||\n            (weapon.rules.limboLaunch && weapon.warhead.rules.parasite) ||\n            weapon.warhead.rules.mindControl) {\n            return undefined;\n        }\n        return weapon;\n    }\n    protected findRelocationTile(preferredTile: Tile, preferredBridge: Bridge | undefined, unit: Unit): Tile | undefined {\n        const map = this.game.map;\n        if (unit.rules.movementZone === MovementZone.Fly) {\n            const isValidTile = (tile: Tile): boolean => !map.tileOccupation\n                .getGroundObjectsOnTile(tile)\n                .some((obj) => (obj.isBuilding() && !obj.isDestroyed) ||\n                obj.isTerrain() ||\n                (obj.isOverlay() && obj.rules.isARock));\n            const randomFinder = new RandomTileFinder(map.tiles, map.mapBounds, preferredTile, 1, this.game, isValidTile);\n            let relocTile = randomFinder.getNextTile();\n            if (!relocTile) {\n                const radialFinder = new RadialTileFinder(map.tiles, map.mapBounds, preferredTile, unit.getFoundation(), 2, 15, isValidTile);\n                relocTile = radialFinder.getNextTile();\n            }\n            return relocTile;\n        }\n        else {\n            const islandMap = !this.options?.ignoredBlockers?.length &&\n                map.terrain.getPassableSpeed(unit.tile, unit.rules.speedType, unit.isInfantry(), unit.onBridge)\n                ? this.game.map.terrain.getIslandIdMap(unit.rules.speedType, unit.isInfantry())\n                : undefined;\n            const unitIslandId = islandMap?.get(unit.tile, unit.onBridge);\n            const moveHelper = new MovePositionHelper(map);\n            const finder = new RadialTileFinder(map.tiles, map.mapBounds, preferredTile, { width: 1, height: 1 }, 0, 5, (tile) => {\n                const bridge = !preferredBridge || preferredBridge.isHighBridge()\n                    ? map.tileOccupation.getBridgeOnTile(tile)\n                    : undefined;\n                return (!this.unreachableTargets.find((target) => target.tile === tile && target.toBridge === !!bridge) &&\n                    (unit.zone === ZoneType.Air ||\n                        (islandMap?.get(tile, !!bridge) === unitIslandId &&\n                            !map.terrain.findObstacles({ tile, onBridge: bridge }, unit).length &&\n                            moveHelper.isEligibleTile(tile as any, bridge, preferredBridge as any, preferredTile as any))) &&\n                    this.canStopAtTile(unit, tile, !!bridge));\n            });\n            return finder.getNextTile();\n        }\n    }\n    private findFreeSubCell(unit: Unit, node: PathNode): number | undefined {\n        const groundObjects = this.game.map.getGroundObjectsOnTile(node.tile);\n        const occupiedByInfantry = groundObjects\n            .filter((obj) => obj.isInfantry() &&\n            obj.onBridge === !!node.onBridge &&\n            obj !== unit)\n            .map((inf) => inf.position.desiredSubCell);\n        const occupiedByTerrain = groundObjects\n            .filter((obj) => obj.isTerrain())\n            .map((terrain) => terrain.rules.getOccupiedSubCells(this.game.map.getTheaterType()))\n            .flat();\n        const allOccupied = [...occupiedByInfantry, ...occupiedByTerrain];\n        return Infantry.SUB_CELLS.find((subCell) => !allOccupied.includes(subCell));\n    }\n    private relocateToSubCell(unit: Unit, subCell: number): void {\n        unit.position.desiredSubCell = subCell;\n        const newOffset = unit.position.computeSubCellOffset(subCell);\n        this.targetOffset = newOffset;\n        this.currentWaypointLeptons\n            .set(unit.moveTrait.currentWaypoint!.tile.rx, unit.moveTrait.currentWaypoint!.tile.ry)\n            .multiplyScalar(Coords.LEPTONS_PER_TILE)\n            .add(this.targetOffset);\n        this.updateDestination(this.path!, this.targetOffset);\n        unit.moveTrait.locomotor.onWaypointUpdate?.(unit, this.currentWaypointLeptons, this.destinationLeptons);\n    }\n    getTargetLinesConfig(unit: Unit): TargetLinesConfig {\n        if (!this.path) {\n            const locomotor = new LocomotorFactory(this.game).create(unit);\n            if ((this.options?.allowOutOfBoundsTarget ||\n                this.game.map.mapBounds.isWithinBounds(this.targetTile)) &&\n                unit.rules.movementZone !== MovementZone.Fly &&\n                !(locomotor as any).ignoresTerrain &&\n                unit.unitOrderTrait.getCurrentTask()?.isCancelling()) {\n                if (!this.groundPathPlan) {\n                    const plan = this.computeGroundPath(unit);\n                    this.targetLinesConfig.pathNodes = plan.path;\n                    if (plan.path.length) {\n                        this.groundPathPlan = plan;\n                    }\n                }\n            }\n            else {\n                unit.moveTrait.locomotor ??= locomotor;\n                this.computePath(unit, unit.moveTrait.locomotor);\n            }\n            this.targetLinesConfig.isRecalc = false;\n        }\n        return this.targetLinesConfig;\n    }\n    private log(unit: Unit, message: string): void {\n        this.logger.debug(`<${unit.id}>: ${message}`);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/move/MoveToBlockTask.ts",
    "content": "import { MoveTrait, MoveResult } from \"@/game/gameobject/trait/MoveTrait\";\nimport { Task } from \"@/game/gameobject/task/system/Task\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nexport class MoveToBlockTask extends Task {\n    private game: any;\n    private target: any;\n    private attackPerformed: boolean = false;\n    constructor(game: any, target: any) {\n        super();\n        this.game = game;\n        this.target = target;\n        this.preventOpportunityFire = false;\n        this.useChildTargetLines = true;\n    }\n    onStart(unit: any): void {\n        this.children.push(new MoveTask(this.game, this.target.centerTile, false, {\n            closeEnoughTiles: 1,\n            pathFinderIgnoredBlockers: [this.target],\n            stopOnBlocker: this.target,\n        }));\n    }\n    onTick(unit: any): boolean {\n        if (this.attackPerformed ||\n            this.isCancelling() ||\n            !unit.attackTrait ||\n            unit.attackTrait.isDisabled()) {\n            return true;\n        }\n        if (unit.moveTrait.lastMoveResult !== MoveResult.CloseEnough) {\n            return true;\n        }\n        const weapon = unit.attackTrait.selectWeaponVersus(unit, this.target, this.game, true);\n        if (!weapon) {\n            return true;\n        }\n        this.children.push(unit.attackTrait.createAttackTask(this.game, this.target, this.target.tile, weapon, { force: true }));\n        this.attackPerformed = true;\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/system/CallbackTask.ts",
    "content": "import { Task } from \"@/game/gameobject/task/system/Task\";\nexport class CallbackTask extends Task {\n    private cb: (unit: any) => void;\n    constructor(cb: (unit: any) => void) {\n        super();\n        this.cb = cb;\n    }\n    onTick(unit: any): boolean {\n        this.cb(unit);\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/system/TargetLinesConfig.ts",
    "content": "export interface TargetLinesConfig {\n    isAttack?: boolean;\n    pathNodes?: any[];\n    target?: any;\n}\nexport function cloneConfig(config: TargetLinesConfig | undefined): TargetLinesConfig | undefined {\n    return config ? { ...config } : undefined;\n}\nexport function configsAreEqual(config1: TargetLinesConfig | undefined, config2: TargetLinesConfig | undefined): boolean {\n    return (!config1 && !config2) ||\n        (config1?.isAttack === config2?.isAttack &&\n            config1?.pathNodes === config2?.pathNodes &&\n            config1?.target === config2?.target);\n}\nexport function configHasTarget(config: TargetLinesConfig | undefined): boolean {\n    return !(!config?.pathNodes?.length && !config?.target);\n}\n"
  },
  {
    "path": "src/game/gameobject/task/system/Task.ts",
    "content": "import { TaskStatus } from \"./TaskStatus\";\nexport class Task {\n    public status: TaskStatus;\n    public children: Task[];\n    public cancellable: boolean;\n    public useChildTargetLines: boolean;\n    public blocking: boolean;\n    public waitingForChildrenToFinish: boolean;\n    public preventOpportunityFire: boolean;\n    public preventLanding: boolean;\n    public isAttackMove: boolean;\n    constructor() {\n        this.status = TaskStatus.NotStarted;\n        this.children = [];\n        this.cancellable = true;\n        this.useChildTargetLines = false;\n        this.blocking = true;\n        this.waitingForChildrenToFinish = false;\n        this.preventOpportunityFire = true;\n        this.preventLanding = true;\n        this.isAttackMove = false;\n    }\n    isRunning(): boolean {\n        return this.status === TaskStatus.Running;\n    }\n    isCancelling(): boolean {\n        return this.status === TaskStatus.Cancelling;\n    }\n    setCancellable(value: boolean): this {\n        this.cancellable = value;\n        return this;\n    }\n    setBlocking(value: boolean): this {\n        this.blocking = value;\n        return this;\n    }\n    onStart(object: any): void { }\n    onEnd(object: any): void { }\n    cancel(): void {\n        if (this.cancellable) {\n            if (this.status === TaskStatus.Running) {\n                this.status = TaskStatus.Cancelling;\n                if (this.children.length) {\n                    this.children.forEach(child => child.cancel());\n                }\n            }\n            else if (this.status === TaskStatus.NotStarted &&\n                this.children.length) {\n                this.status = TaskStatus.Cancelled;\n                throw new Error(\"Should't have any children before starting a task\");\n            }\n        }\n    }\n    getTargetLinesConfig(object: any): any { }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/system/TaskGroup.ts",
    "content": "import { Task } from \"./Task\";\nexport class TaskGroup extends Task {\n    constructor(...tasks: Task[]) {\n        super();\n        this.children.push(...tasks);\n    }\n    onTick(object: any): boolean {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/system/TaskRunner.ts",
    "content": "import { TaskStatus } from \"./TaskStatus\";\nimport { Task } from \"./Task\";\nexport class TaskRunner {\n    tick(tasks: Task[], object: any): void {\n        this.tickChildren(tasks, object);\n    }\n    startTask(task: Task, object: any): void {\n        if (task.status !== TaskStatus.NotStarted) {\n            throw new Error(`Attempted to start a task with status ${task.status}`);\n        }\n        task.status = TaskStatus.Running;\n        task.onStart(object);\n    }\n    tickTask(task: Task, object: any): boolean {\n        let allChildrenFinished = this.tickChildren(task.children, object);\n        const blockingChild = task.children.find(child => child.blocking);\n        if (!allChildrenFinished && blockingChild)\n            return false;\n        if (!object.isSpawned)\n            return false;\n        if (task.status === TaskStatus.NotStarted) {\n            throw new Error(\"Attempted tick on a non-started task\");\n        }\n        if (task.isRunning() || task.isCancelling()) {\n            const isCancelling = task.isCancelling();\n            let shouldContinue = !!task.waitingForChildrenToFinish || (task as any).onTick(object);\n            if (task.children.length && !blockingChild && shouldContinue) {\n                allChildrenFinished = task.children.every(child => child.status === TaskStatus.Cancelled || child.status === TaskStatus.Finished);\n                task.waitingForChildrenToFinish = !allChildrenFinished;\n            }\n            shouldContinue = shouldContinue && allChildrenFinished;\n            if (shouldContinue) {\n                task.onEnd(object);\n                task.status = isCancelling ? TaskStatus.Cancelled : TaskStatus.Finished;\n            }\n            return shouldContinue;\n        }\n        return true;\n    }\n    tickChildren(tasks: Task[], object: any): boolean {\n        let allFinished = true;\n        if (tasks.length) {\n            const processedTasks = new Set<Task>();\n            let currentTask: Task | undefined;\n            while (object.isSpawned && (currentTask = tasks.find(task => !processedTasks.has(task)))) {\n                let isFinished: boolean;\n                if (currentTask.status === TaskStatus.NotStarted) {\n                    this.startTask(currentTask, object);\n                }\n                if (currentTask.status === TaskStatus.Running || currentTask.status === TaskStatus.Cancelling) {\n                    isFinished = this.tickTask(currentTask, object) === true;\n                }\n                else {\n                    if (currentTask.status !== TaskStatus.Cancelled) {\n                        throw new Error(`Unhandled task status ${TaskStatus[currentTask.status]}`);\n                    }\n                    isFinished = true;\n                }\n                if (isFinished) {\n                    const index = tasks.indexOf(currentTask);\n                    if (index !== -1) {\n                        tasks.splice(index, 1);\n                    }\n                }\n                else {\n                    allFinished = false;\n                    if (currentTask.blocking)\n                        break;\n                    processedTasks.add(currentTask);\n                }\n            }\n        }\n        return allFinished;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/system/TaskStatus.ts",
    "content": "export enum TaskStatus {\n    NotStarted = 0,\n    Running = 1,\n    Finished = 2,\n    Cancelling = 3,\n    Cancelled = 4\n}\n"
  },
  {
    "path": "src/game/gameobject/task/system/WaitMinutesTask.ts",
    "content": "import { WaitTicksTask } from \"./WaitTicksTask\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nexport class WaitMinutesTask extends WaitTicksTask {\n    constructor(minutes: number) {\n        super(Math.floor(GameSpeed.BASE_TICKS_PER_SECOND * minutes * 60));\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/task/system/WaitTicksTask.ts",
    "content": "import { Task } from \"./Task\";\nexport class WaitTicksTask extends Task {\n    private ticks: number;\n    constructor(ticks: number) {\n        super();\n        this.ticks = ticks;\n    }\n    onTick(): boolean {\n        return this.isCancelling() || !(this.ticks-- > 0);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/AgentTrait.ts",
    "content": "import { FactoryType } from \"@/game/rules/TechnoRules\";\nimport { clamp } from \"@/util/math\";\nexport class AgentTrait {\n    infiltrate(agent: any, target: any, game: any): void {\n        if (target.rules.radar &&\n            ![...target.owner.buildings].some((b: any) => b.rules.spySat)) {\n            game.mapShroudTrait.resetShroud(target.owner, game);\n        }\n        if (target.rules.power > 0) {\n            const blackoutTime = game.rules.general.spyPowerBlackout;\n            target.owner.powerTrait?.setBlackoutFor(blackoutTime, game);\n        }\n        if (target.superWeaponTrait) {\n            target.superWeaponTrait.getSuperWeapon(target)?.resetTimer();\n        }\n        if (target.rules.storage > 0) {\n            const stealPercent = clamp(game.rules.general.spyMoneyStealPercent, 0, 1);\n            const stolenAmount = Math.floor(target.owner.credits * stealPercent);\n            target.owner.credits -= stolenAmount;\n            agent.owner.credits += stolenAmount;\n        }\n        if (game.rules.ai.buildTech.includes(target.name)) {\n            const side = target.rules.aiBasePlanningSide;\n            if (side !== undefined) {\n                agent.owner.production.addStolenTech(side);\n            }\n        }\n        if (target.factoryTrait &&\n            [FactoryType.InfantryType, FactoryType.UnitType].includes(target.factoryTrait.type)) {\n            agent.owner.production?.addVeteranType(target.factoryTrait.type);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/AirSpawnTrait.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { Warhead } from \"@/game/Warhead\";\nimport { CollisionType } from \"@/game/gameobject/unit/CollisionType\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { TaskGroup } from \"@/game/gameobject/task/system/TaskGroup\";\nimport { FacingUtil } from \"@/game/gameobject/unit/FacingUtil\";\nimport { NotifyDestroy } from \"@/game/gameobject/trait/interface/NotifyDestroy\";\nimport { NotifyOwnerChange } from \"@/game/gameobject/trait/interface/NotifyOwnerChange\";\nimport { NotifySpawn } from \"@/game/gameobject/trait/interface/NotifySpawn\";\nimport { NotifyTeleport } from \"@/game/gameobject/trait/interface/NotifyTeleport\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { NotifyUnspawn } from \"@/game/gameobject/trait/interface/NotifyUnspawn\";\nimport { NotifyWarpChange } from \"@/game/gameobject/trait/interface/NotifyWarpChange\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\ninterface MissileLaunch {\n    missile: any;\n    targetTile: any;\n    targetBridge: any;\n    targetWorldPos: any;\n    target: any;\n    warhead: Warhead;\n    damage: number;\n    pauseFrames?: number;\n}\nexport class AirSpawnTrait implements NotifyDestroy, NotifyOwnerChange, NotifySpawn, NotifyTeleport, NotifyTick, NotifyUnspawn, NotifyWarpChange {\n    private spawns: any[] = [];\n    private storage: any[] = [];\n    private missileLaunches: MissileLaunch[] = [];\n    private nextRegenTicks: number[] = [];\n    private nextReloadTicks?: number;\n    get availableSpawns(): number {\n        return this.storage.length;\n    }\n    debugSetStorage(unit: any, count: number): void {\n        this.storage.length = count;\n        this.storage.fill(unit, 0, count);\n    }\n    isLaunchingMissiles(): boolean {\n        return this.missileLaunches.length > 0;\n    }\n    [NotifySpawn.onSpawn](gameObject: any, world: any): void {\n        const aircraftType = world.rules.getObject(gameObject.rules.spawns, ObjectType.Aircraft);\n        for (let i = 0; i < gameObject.rules.spawnsNumber; i++) {\n            this.pushNewSpawn(aircraftType, world, gameObject);\n        }\n    }\n    [NotifyUnspawn.onUnspawn](gameObject: any, world: any): void {\n        this.destroySpawns(gameObject, world, undefined, undefined);\n    }\n    [NotifyDestroy.onDestroy](gameObject: any, world: any, damageSource: any, warhead: any): void {\n        this.destroySpawns(gameObject, world, damageSource, warhead);\n    }\n    private pushNewSpawn(aircraftType: any, world: any, parent: any): void {\n        const spawn = world.createUnitForPlayer(aircraftType, parent.owner);\n        spawn.limboData = { selected: false, controlGroup: undefined };\n        if (aircraftType.missileSpawn) {\n            spawn.pitch = 90 * world.rules.general.getMissileRules(aircraftType.name).pitchInitial;\n        }\n        this.spawns.push(spawn);\n        this.storage.push(spawn);\n    }\n    private destroySpawns(gameObject: any, world: any, damageSource?: any, warhead?: any): void {\n        for (const spawn of this.spawns) {\n            if (!spawn.isDestroyed) {\n                if (spawn.isSpawned && !spawn.rules.missileSpawn && spawn.crashableTrait) {\n                    spawn.crashableTrait.crash(damageSource);\n                }\n                else {\n                    if (!spawn.isSpawned) {\n                        if (spawn.armedTrait) {\n                            spawn.armedTrait.deathWeapon = undefined;\n                        }\n                        spawn.position.tileElevation = gameObject.position.tileElevation;\n                        spawn.zone = gameObject.isUnit() ? gameObject.zone : ZoneType.Ground;\n                        spawn.onBridge = !!gameObject.isUnit() && gameObject.onBridge;\n                        spawn.position.tile = gameObject.tile;\n                    }\n                    world.destroyObject(spawn, damageSource, warhead);\n                }\n            }\n        }\n        this.spawns.length = 0;\n        this.storage.length = 0;\n        this.missileLaunches.length = 0;\n    }\n    [NotifyTick.onTick](gameObject: any, world: any): void {\n        this.spawns = this.spawns.filter(spawn => !spawn.isDestroyed);\n        this.missileLaunches = this.missileLaunches.filter(launch => !launch.missile.isDestroyed);\n        if (this.spawns.length < gameObject.rules.spawnsNumber) {\n            const missingCount = gameObject.rules.spawnsNumber - this.spawns.length;\n            const aircraftType = world.rules.getObject(gameObject.rules.spawns, ObjectType.Aircraft);\n            for (let i = 0; i < missingCount; i++) {\n                if (aircraftType.missileSpawn && i > 0 && this.nextRegenTicks[i] === undefined) {\n                    this.nextRegenTicks[i] = this.nextRegenTicks[0];\n                }\n                else {\n                    this.nextRegenTicks[i] ??= gameObject.rules.spawnRegenRate;\n                    if (this.nextRegenTicks[i] > 0) {\n                        this.nextRegenTicks[i]--;\n                    }\n                }\n                if (this.nextRegenTicks[i] <= 0) {\n                    this.pushNewSpawn(aircraftType, world, gameObject);\n                }\n            }\n            this.nextRegenTicks = this.nextRegenTicks.filter(ticks => ticks > 0);\n        }\n        if (this.storage.length > 0) {\n            this.nextReloadTicks ??= gameObject.rules.spawnReloadRate;\n            if (this.nextReloadTicks > 0) {\n                this.nextReloadTicks--;\n            }\n            if (this.nextReloadTicks <= 0) {\n                for (const spawn of this.storage) {\n                    if (spawn.ammoTrait && spawn.ammoTrait.ammo < spawn.ammoTrait.maxAmmo) {\n                        spawn.ammoTrait.ammo++;\n                    }\n                }\n                this.nextReloadTicks = gameObject.rules.spawnReloadRate;\n            }\n        }\n        else {\n            this.nextReloadTicks = undefined;\n        }\n        for (const launch of this.missileLaunches.slice()) {\n            const missileRules = world.rules.general.getMissileRules(launch.missile.name);\n            launch.pauseFrames ??= missileRules.pauseFrames;\n            if (launch.pauseFrames > 0) {\n                launch.pauseFrames--;\n            }\n            if (launch.pauseFrames <= 0) {\n                const finalPitch = 90 * missileRules.pitchFinal;\n                const pitchIncrement = (90 * (missileRules.pitchFinal - missileRules.pitchInitial)) / missileRules.tiltFrames;\n                const missile = launch.missile;\n                if (missile.pitch < finalPitch) {\n                    missile.pitch = Math.min(finalPitch, missile.pitch + pitchIncrement);\n                }\n                else {\n                    missile.unitOrderTrait.addTask(new TaskGroup(new MoveTask(world, launch.targetTile, !!launch.targetBridge), new CallbackTask(() => {\n                        if (!missile.isDestroyed) {\n                            world.unspawnObject(missile);\n                            missile.dispose();\n                            const offset = Coords.vecGroundToWorld(FacingUtil.toMapCoords(missile.direction).multiplyScalar(1));\n                            const detonationPos = launch.targetWorldPos.clone().add(offset);\n                            const targetZone = world.map.getTileZone(launch.targetTile);\n                            launch.warhead.detonate(world, launch.damage, launch.targetTile, launch.targetBridge?.tileElevation ?? 0, detonationPos, targetZone, launch.targetBridge ? CollisionType.OnBridge : CollisionType.None, launch.target, { player: missile.owner, obj: gameObject, weapon: undefined } as any, false, undefined, undefined);\n                        }\n                    })).setCancellable(false));\n                    const missileIndex = this.spawns.indexOf(missile);\n                    if (missileIndex === -1) {\n                        throw new Error(\"Missile not found in spawns list\");\n                    }\n                    this.spawns.splice(missileIndex, 1);\n                    this.missileLaunches.splice(this.missileLaunches.indexOf(launch), 1);\n                }\n            }\n        }\n    }\n    [NotifyOwnerChange.onChange](gameObject: any, oldOwner: any, world: any): void {\n        for (const spawn of this.spawns) {\n            if (!spawn.isDestroyed) {\n                world.changeObjectOwner(spawn, gameObject.owner);\n            }\n        }\n    }\n    [NotifyWarpChange.onChange](gameObject: any, world: any, isWarping: boolean): void {\n        if (isWarping) {\n            this.removeMissileLaunches(world);\n        }\n    }\n    [NotifyTeleport.onBeforeTeleport](gameObject: any, world: any, targetTile: any, keepSpawns: boolean): void {\n        if (!keepSpawns) {\n            this.removeMissileLaunches(world);\n        }\n    }\n    private removeMissileLaunches(world: any): void {\n        if (this.missileLaunches.length > 0) {\n            for (const launch of this.missileLaunches) {\n                world.unspawnObject(launch.missile);\n                launch.missile.dispose();\n                const missileIndex = this.spawns.indexOf(launch.missile);\n                if (missileIndex === -1) {\n                    throw new Error(\"Missile not found in spawns list\");\n                }\n                this.spawns.splice(missileIndex, 1);\n            }\n            this.missileLaunches.length = 0;\n        }\n    }\n    prepareLaunch(launcher: any, target: any, world: any): any {\n        if (this.storage.length > 0) {\n            const spawn = this.storage[0];\n            if (!spawn.ammo)\n                return;\n            this.storage.shift();\n            if (spawn.missileSpawnTrait) {\n                let warheadType: string;\n                let damage: number;\n                const isElite = launcher.veteranTrait?.isElite();\n                const rules = world.rules;\n                if (launcher.rules.spawns === rules.general.v3Rocket.type) {\n                    warheadType = isElite ? rules.combatDamage.v3EliteWarhead : rules.combatDamage.v3Warhead;\n                    damage = isElite ? rules.general.v3Rocket.eliteDamage : rules.general.v3Rocket.damage;\n                }\n                else if (launcher.rules.spawns === rules.general.dMisl.type) {\n                    warheadType = isElite ? rules.combatDamage.dMislEliteWarhead : rules.combatDamage.dMislWarhead;\n                    damage = isElite ? rules.general.dMisl.eliteDamage : rules.general.dMisl.damage;\n                }\n                else {\n                    throw new Error(`Unhandled missile type \"${launcher.rules.spawns}\"`);\n                }\n                const warhead = new Warhead(world.rules.getWarhead(warheadType));\n                spawn.missileSpawnTrait.setDamage(damage).setWarhead(warhead).setLauncher(launcher);\n                this.missileLaunches.push({\n                    missile: spawn,\n                    targetTile: (target.obj?.isUnit() ? target.obj : target).tile,\n                    targetBridge: target.getBridge(),\n                    targetWorldPos: target.getWorldCoords().clone(),\n                    target: target,\n                    warhead: warhead,\n                    damage: damage,\n                    pauseFrames: undefined\n                });\n            }\n            else {\n                if (!spawn.spawnLinkTrait) {\n                    throw new Error(`Aircraft \"${spawn.name}\" must have Spawned=yes to be launchable`);\n                }\n                spawn.spawnLinkTrait.setParent(launcher);\n            }\n            return spawn;\n        }\n    }\n    storeAircraft(aircraft: any, world: any): void {\n        if (!this.spawns.includes(aircraft)) {\n            throw new Error(`Object \"${aircraft.name}#${aircraft.id}\" not found in list of linked spawns`);\n        }\n        if (aircraft.limboData) {\n            throw new Error(`Object \"${aircraft.name}#${aircraft.id}\" is already in limbo`);\n        }\n        world.limboObject(aircraft, { selected: false, controlGroup: undefined });\n        this.storage.push(aircraft);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/AirportBoundTrait.ts",
    "content": "export class AirportBoundTrait {\n    private airportNames: string[];\n    constructor(airportNames: string[]) {\n        this.airportNames = airportNames;\n    }\n    findAvailableAirport(unit: {\n        owner: {\n            buildings: any[];\n        };\n    }) {\n        return [...unit.owner.buildings].find((building) => building.dockTrait &&\n            this.airportNames.includes(building.name) &&\n            building.dockTrait.getAvailableDockCount() > 0);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/AmmoTrait.ts",
    "content": "import { clamp } from \"@/util/math\";\nexport class AmmoTrait {\n    private _ammo: number;\n    private maxAmmo: number;\n    constructor(maxAmmo: number, ammo: number = maxAmmo) {\n        this.maxAmmo = maxAmmo;\n        this.ammo = ammo;\n    }\n    get ammo(): number {\n        return this._ammo;\n    }\n    set ammo(value: number) {\n        this._ammo = clamp(value, 0, this.maxAmmo);\n    }\n    isFull(): boolean {\n        return this.ammo === this.maxAmmo;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/ArmedTrait.ts",
    "content": "import { Weapon } from \"@/game/Weapon\";\nimport { WeaponType } from \"@/game/WeaponType\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { NotifyDestroy } from \"@/game/gameobject/trait/interface/NotifyDestroy\";\nimport { VeteranLevel } from \"@/game/gameobject/unit/VeteranLevel\";\nimport { isNotNullOrUndefined } from \"@/util/typeGuard\";\ninterface GameObject {\n    veteranLevel: VeteranLevel;\n    rules: GameObjectRules;\n    art: GameObjectArt;\n    name: string;\n    explodes: boolean;\n    crashableTrait?: any;\n    isCrashing?: boolean;\n    tile: any;\n    transportTrait?: {\n        units: GameObject[];\n    };\n    isVehicle(): boolean;\n}\ninterface GameObjectRules {\n    weaponCount?: number;\n    elitePrimary?: string;\n    primary?: string;\n    eliteSecondary?: string;\n    secondary?: string;\n    deathWeapon?: string;\n    combatDamage: {\n        deathWeapon?: string;\n    };\n    guardRange: number;\n    deployFire?: boolean;\n    deployFireWeapon?: WeaponType;\n    getEliteWeaponAtIndex(index: number): string | undefined;\n    getWeaponAtIndex(index: number): string | undefined;\n}\ninterface GameObjectArt {\n    elitePrimaryFireFlh?: any;\n    primaryFireFlh?: any;\n    eliteSecondaryFireFlh?: any;\n    secondaryFireFlh?: any;\n    getSpecialWeaponFlh(index: number): any;\n}\ninterface DestroyContext {\n    weapon?: {\n        warhead: {\n            rules: {\n                temporal: boolean;\n            };\n        };\n        rules: {\n            suicide: boolean;\n        };\n    };\n    obj?: GameObject;\n}\ninterface Target {\n    createTarget(obj: GameObject, tile: any): any;\n}\nexport class ArmedTrait implements NotifyTick, NotifyDestroy {\n    private gameObject: GameObject;\n    private rules: GameObjectRules;\n    private specialWeaponIndex: number = 0;\n    private guardWeaponRangeOverride?: number;\n    public primaryWeapon?: Weapon;\n    public secondaryWeapon?: Weapon;\n    public deathWeapon?: Weapon;\n    constructor(gameObject: GameObject, rules: GameObjectRules) {\n        this.gameObject = gameObject;\n        this.rules = rules;\n        this.specialWeaponIndex = 0;\n        const isElite = gameObject.veteranLevel === VeteranLevel.Elite;\n        if (gameObject.rules.weaponCount) {\n            this.selectSpecialWeapon(0, isElite);\n            this.guardWeaponRangeOverride = this.primaryWeapon?.range;\n        }\n        else {\n            this.selectStandardWeapons(isElite);\n        }\n    }\n    private selectStandardWeapons(isElite: boolean = false): void {\n        const gameObject = this.gameObject;\n        const primaryWeaponName = (isElite && gameObject.rules.elitePrimary) || gameObject.rules.primary;\n        if (primaryWeaponName) {\n            const fireFlh = isElite ? gameObject.art.elitePrimaryFireFlh : gameObject.art.primaryFireFlh;\n            this.primaryWeapon = Weapon.factory(primaryWeaponName, WeaponType.Primary, gameObject as any, this.rules as any, fireFlh);\n        }\n        else {\n            this.primaryWeapon = undefined;\n        }\n        const secondaryWeaponName = (isElite && gameObject.rules.eliteSecondary) || gameObject.rules.secondary;\n        if (secondaryWeaponName) {\n            const fireFlh = isElite ? gameObject.art.eliteSecondaryFireFlh : gameObject.art.secondaryFireFlh;\n            this.secondaryWeapon = Weapon.factory(secondaryWeaponName, WeaponType.Secondary, gameObject as any, this.rules as any, fireFlh);\n        }\n        else {\n            this.secondaryWeapon = undefined;\n        }\n        if (gameObject.explodes || gameObject.crashableTrait) {\n            const deathWeaponName = gameObject.rules.deathWeapon ||\n                (gameObject.crashableTrait && this.secondaryWeapon?.rules.name) ||\n                this.primaryWeapon?.rules.name ||\n                this.rules.combatDamage.deathWeapon;\n            this.deathWeapon = Weapon.factory(deathWeaponName, WeaponType.DeathWeapon, gameObject as any, this.rules as any);\n        }\n    }\n    private selectSpecialWeapon(index: number, isElite: boolean = false): void {\n        const gameObject = this.gameObject;\n        const weaponCount = gameObject.rules.weaponCount;\n        if (!weaponCount || weaponCount < 1) {\n            throw new Error(`Object \"${gameObject.name}\" doesn't support special weapons`);\n        }\n        if (weaponCount - 1 < index) {\n            throw new RangeError(`Weapon index ${index} out of bounds (max ${weaponCount}) for object ${gameObject.name}`);\n        }\n        const weaponName = (isElite && gameObject.rules.getEliteWeaponAtIndex(index)) ||\n            gameObject.rules.getWeaponAtIndex(index);\n        if (!weaponName) {\n            throw new Error(`Missing weapon at index ${index} for object \"${gameObject.name}\"`);\n        }\n        const fireFlh = gameObject.art.getSpecialWeaponFlh(index);\n        this.primaryWeapon = Weapon.factory(weaponName, WeaponType.Primary, gameObject as any, this.rules as any, fireFlh);\n        this.secondaryWeapon = undefined;\n        this.specialWeaponIndex = index;\n        this.deathWeapon = (this.primaryWeapon.rules as any).suicide\n            ? Weapon.factory(gameObject.rules.deathWeapon || this.primaryWeapon.name, WeaponType.DeathWeapon, gameObject as any, this.rules as any)\n            : undefined;\n    }\n    public toggleEliteWeapons(isElite: boolean): void {\n        if (this.gameObject.rules.weaponCount) {\n            this.selectSpecialWeapon(this.specialWeaponIndex, isElite);\n        }\n        else {\n            this.selectStandardWeapons(isElite);\n        }\n    }\n    public getSpecialWeaponIndex(): number {\n        return this.specialWeaponIndex;\n    }\n    public computeGuardScanRange(weapon?: Weapon): number {\n        const maxWeaponRange = this.guardWeaponRangeOverride ??\n            [this.primaryWeapon, this.secondaryWeapon]\n                .filter(w => w === weapon || (w?.rules as any).neverUse)\n                .reduce((max, w) => Math.max(max, w!.range), 0);\n        const guardRange = Math.max(maxWeaponRange, this.gameObject.rules.guardRange);\n        return Math.min(15, 2 * guardRange - 1);\n    }\n    public getDeployFireWeapon(): Weapon | undefined {\n        if (this.gameObject.rules.deployFire) {\n            return this.gameObject.rules.deployFireWeapon === WeaponType.Primary\n                ? this.primaryWeapon\n                : this.secondaryWeapon;\n        }\n        return undefined;\n    }\n    public isEquippedWithWeapon(weapon: Weapon): boolean {\n        return [this.primaryWeapon, this.secondaryWeapon].includes(weapon);\n    }\n    public getWeapons(): Weapon[] {\n        return [this.primaryWeapon, this.secondaryWeapon].filter(isNotNullOrUndefined);\n    }\n    [NotifyTick.onTick](): void {\n        this.primaryWeapon?.tick();\n        this.secondaryWeapon?.tick();\n    }\n    [NotifyDestroy.onDestroy](gameObject: GameObject, target: Target, context?: DestroyContext): void {\n        if (!this.deathWeapon)\n            return;\n        if (context?.weapon?.warhead.rules.temporal)\n            return;\n        if (gameObject.crashableTrait && !gameObject.isCrashing)\n            return;\n        if (context?.obj?.isVehicle() &&\n            context.weapon?.rules.suicide &&\n            context.obj.transportTrait?.units.find(unit => unit === gameObject))\n            return;\n        this.deathWeapon.fire(target.createTarget(gameObject, gameObject.tile), target as any);\n    }\n    public dispose(): void {\n        this.gameObject = undefined!;\n        this.primaryWeapon = undefined;\n        this.secondaryWeapon = undefined;\n        this.deathWeapon = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/AttackTrait.ts",
    "content": "import { isNotNullOrUndefined } from \"@/util/typeGuard\";\nimport { ArmorType } from \"@/game/type/ArmorType\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { AttackTask } from \"@/game/gameobject/task/AttackTask\";\nimport { SideType } from \"@/game/SideType\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { NotifyDamage } from \"@/game/gameobject/trait/interface/NotifyDamage\";\nimport { TaskRunner } from \"@/game/gameobject/task/system/TaskRunner\";\nimport { Target } from \"@/game/Target\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { MoveResult } from \"@/game/gameobject/trait/MoveTrait\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { Coords } from \"@/game/Coords\";\nimport { NotifyTeleport } from \"@/game/gameobject/trait/interface/NotifyTeleport\";\nimport { VhpScan } from \"@/game/type/VhpScan\";\nimport { LosHelper } from \"@/game/gameobject/unit/LosHelper\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { Box2 } from \"@/game/math/Box2\";\nexport enum AttackState {\n    Idle = 0,\n    CheckRange = 1,\n    PrepareToFire = 2,\n    FireUp = 3,\n    Firing = 4,\n    JustFired = 5\n}\nexport class AttackTrait implements NotifyTick, NotifyDamage, NotifyTeleport {\n    private disabled: boolean = false;\n    private attackState: AttackState = AttackState.Idle;\n    private passiveScanCooldownTicks: number = 0;\n    private taskRunner: TaskRunner = new TaskRunner();\n    private distributedFireHistory: Map<any, number> = new Map();\n    private rangeHelper: RangeHelper;\n    private losHelper: LosHelper;\n    private opportunityFireTask?: any;\n    private retaliateTarget?: any;\n    private currentTarget?: any;\n    constructor(e: any, t: any) {\n        this.rangeHelper = new RangeHelper(t);\n        this.losHelper = new LosHelper(e, t);\n    }\n    isIdle(): boolean {\n        return this.attackState === AttackState.Idle;\n    }\n    isDisabled(): boolean {\n        return this.disabled;\n    }\n    setDisabled(e: boolean): void {\n        this.disabled = e;\n    }\n    isOnCooldown(e: any): boolean {\n        let t = [e.primaryWeapon, e.secondaryWeapon];\n        const i = e.armedTrait?.getDeployFireWeapon();\n        if (i?.rules.areaFire && !i.rules.fireOnce) {\n            t = t.filter((e) => e !== i);\n        }\n        return t.some((e) => (e?.getCooldownTicks() ?? 0) > 0);\n    }\n    expirePassiveScanCooldown(): void {\n        this.passiveScanCooldownTicks = 0;\n    }\n    increasePassiveScanCooldown(e: number): void {\n        this.passiveScanCooldownTicks += e;\n    }\n    cancelOpportunityFire(): void {\n        this.opportunityFireTask?.cancel();\n    }\n    selectDefaultWeapon(e: any): any {\n        let i;\n        if ((e.isInfantry() || e.isVehicle()) && e.rules.deployFire) {\n            const t = e.armedTrait?.getDeployFireWeapon();\n            i = e.deployerTrait?.isDeployed()\n                ? t && !t.rules.areaFire\n                    ? t\n                    : undefined\n                : [e.primaryWeapon, e.secondaryWeapon].find((e) => e !== t);\n        }\n        else {\n            i =\n                e.isBuilding() && e.garrisonTrait\n                    ? e.garrisonTrait.isOccupied()\n                        ? e.owner.country.side === SideType.GDI\n                            ? e.primaryWeapon\n                            : (e.secondaryWeapon ?? e.primaryWeapon)\n                        : undefined\n                    : e.isBuilding() && e.overpoweredTrait\n                        ? e.overpoweredTrait.getWeapon()\n                        : e.primaryWeapon;\n        }\n        return i;\n    }\n    selectWeaponVersus(e: any, t: any, i: any, r: boolean = false, s: boolean = false): any {\n        const a = t.tile;\n        const n = t instanceof Target ? t.obj : t;\n        const o = this.getAvailableWeapons(e, s, n?.isOverlay() || (r && !n));\n        return this.selectWeaponFromList(e, n, a, o, i, r, s, false);\n    }\n    selectWeaponFromList(e: any, t: any, i: any, r: any[], s: any, a: boolean, n: boolean, o: boolean): any {\n        if ((!t?.isInfantry() && !t?.isVehicle()) ||\n            !t.disguiseTrait ||\n            this.canAttackThroughDisguise(e, t, t.disguiseTrait, s, a, n, o)) {\n            if (t?.isBuilding() &&\n                t.overpoweredTrait &&\n                t.owner === e.owner &&\n                r.find((e: any) => e.warhead.rules.electricAssault)) {\n                r = r.filter((e: any) => e.warhead.rules.electricAssault);\n            }\n            if (!(n &&\n                t?.isAircraft() &&\n                t.missileSpawnTrait &&\n                t.zone !== ZoneType.Air)) {\n                const l = t?.isTechno() ? t.rules.armor : undefined;\n                for (const c of r) {\n                    if (c.targeting.canTarget(t, i, s, a, n) &&\n                        (l === undefined || this.checkArmor(c.warhead.rules, l, n))) {\n                        return c;\n                    }\n                }\n            }\n        }\n    }\n    getAvailableWeapons(e: any, t: boolean, i: boolean): any[] {\n        let r;\n        let s;\n        if ((e.isInfantry() || e.isVehicle()) &&\n            e.rules.deployFire &&\n            e.armedTrait) {\n            s = e.armedTrait.getDeployFireWeapon();\n            r = [\n                e.deployerTrait?.isDeployed()\n                    ? s.rules.areaFire\n                        ? undefined\n                        : s\n                    : s === e.secondaryWeapon\n                        ? e.primaryWeapon\n                        : e.secondaryWeapon,\n            ];\n        }\n        else if (e.isBuilding() && e.garrisonTrait) {\n            r = e.garrisonTrait.isOccupied()\n                ? [\n                    e.owner.country.side === SideType.GDI\n                        ? e.primaryWeapon\n                        : (e.secondaryWeapon ?? e.primaryWeapon),\n                ]\n                : [];\n        }\n        else if (e.isBuilding() && e.overpoweredTrait) {\n            r = [e.overpoweredTrait.getWeapon()];\n        }\n        else if (i || t) {\n            r = [\n                e.primaryWeapon,\n                !i && t && e.secondaryWeapon\n                    ? e.secondaryWeapon\n                    : undefined,\n            ];\n        }\n        else {\n            r = [e.primaryWeapon, e.secondaryWeapon];\n        }\n        return r.filter((e) => e && !e.rules.neverUse);\n    }\n    canAttackThroughDisguise(e: any, t: any, i: any, r: any, s: boolean, a: boolean, n: boolean): boolean {\n        if (!s &&\n            i.hasTerrainDisguise() &&\n            !r.areFriendly(e, t) &&\n            !e.owner.sharedDetectDisguiseTrait?.has(t)) {\n            return false;\n        }\n        if (a) {\n            if (n &&\n                t.moveTrait.isIdle() &&\n                !e.rules.detectDisguise &&\n                !e.owner.sharedDetectDisguiseTrait?.has(t) &&\n                !r.areFriendly(t, e)) {\n                return false;\n            }\n            const o = i.getDisguise();\n            if (o?.owner &&\n                !e.rules.detectDisguise &&\n                !e.owner.sharedDetectDisguiseTrait?.has(t) &&\n                (o.owner === e.owner ||\n                    r.alliances.areAllied(e.owner, o.owner))) {\n                return false;\n            }\n        }\n        return true;\n    }\n    checkArmor(e: any, t: ArmorType, i: boolean): boolean {\n        const r = e.ivanBomb || e.bombDisarm || e.nukeMaker\n            ? 1\n            : e.verses.get(t);\n        if (r === undefined) {\n            console.warn(`Unhandled ArmorType ${ArmorType[t]} in warhead ${e.name} verses`);\n            return false;\n        }\n        return !(100 * r <= (i ? 1 : 0));\n    }\n    createAttackTask(e: any, t: any, i: any, r: any, s: any): AttackTask {\n        return new AttackTask(e, e.createTarget(t, i), r, s);\n    }\n    [NotifyTick.onTick](a: any, n: any): void {\n        if (!this.isDisabled()) {\n            if (this.opportunityFireTask &&\n                (!a.unitOrderTrait.hasTasks() ||\n                    (a.isUnit() &&\n                        !a.unitOrderTrait.getTasks()[0]\n                            .preventOpportunityFire) ||\n                    (a.unitOrderTrait.getTasks()[0] instanceof AttackTask\n                        ? (this.opportunityFireTask = undefined)\n                        : this.opportunityFireTask.cancel()),\n                    this.opportunityFireTask)) {\n                const h = [this.opportunityFireTask];\n                this.taskRunner.tick(h, a);\n                if (!h.length) {\n                    this.opportunityFireTask = undefined;\n                }\n            }\n            if (!this.opportunityFireTask && this.retaliateTarget) {\n                const o = this.retaliateTarget;\n                this.retaliateTarget = undefined;\n                let e;\n                if (!a.unitOrderTrait.hasTasks() && n.isValidTarget(o)) {\n                    e = this.selectWeaponVersus(a, o, n, false);\n                    if (e) {\n                        a.unitOrderTrait.addTask(this.createAttackTask(n, o, o.tile, e, {\n                            holdGround: a.rules.movementZone === MovementZone.Fly,\n                        }));\n                    }\n                }\n            }\n            if (!this.opportunityFireTask && this.shouldPassiveAcquire(a)) {\n                if (this.passiveScanCooldownTicks > 0) {\n                    this.passiveScanCooldownTicks--;\n                }\n                else {\n                    this.passiveScanCooldownTicks = a.guardMode\n                        ? n.rules.general.guardAreaTargetingDelay\n                        : n.rules.general.normalTargetingDelay;\n                    let e = this.selectDefaultWeapon(a);\n                    const h = a.unitOrderTrait.hasTasks();\n                    let t = undefined;\n                    let i;\n                    let r;\n                    if (!h && a.guardMode && e && a.owner.isCombatant()) {\n                        t = a.armedTrait?.computeGuardScanRange(e);\n                        i = a.guardArea?.tile;\n                        r = 50;\n                    }\n                    let s = false;\n                    if (e) {\n                        const o = this.scanForTarget(a, e, n, t, i);\n                        if (o.target) {\n                            const { target: l, weapon: c } = o;\n                            const task = this.createAttackTask(n, l, l.tile, c, {\n                                holdGround: h || !a.guardMode,\n                                disallowTurning: h,\n                                leashTiles: r,\n                                passive: true,\n                            });\n                            if (h) {\n                                this.opportunityFireTask = task;\n                            }\n                            else {\n                                a.unitOrderTrait.addTask(task);\n                            }\n                            s = true;\n                            if (!h && a.guardMode && !a.guardArea) {\n                                a.guardArea = {\n                                    tile: a.tile,\n                                    onBridge: !!a.isUnit() && a.onBridge,\n                                };\n                            }\n                            if (s && !h) {\n                                a.unitOrderTrait[NotifyTick.onTick](a, n);\n                            }\n                        }\n                    }\n                    if (!s && !h && a.secondaryWeapon?.warhead.rules.electricAssault) {\n                        e = a.secondaryWeapon;\n                        const c = this.scanForTarget(a, e, n, undefined, undefined, true);\n                        if (c.target) {\n                            const { target: l, weapon: c2 } = c;\n                            const task = this.createAttackTask(n, l, l.tile, c2, {\n                                passive: true,\n                            });\n                            a.unitOrderTrait.addTask(task);\n                            s = true;\n                        }\n                    }\n                    if (!s && !h && a.guardArea && a.isUnit() && a.moveTrait && !a.moveTrait.isDisabled() && a.guardArea.tile !== a.tile) {\n                        a.unitOrderTrait.addTasks(new MoveTask(n, a.guardArea.tile, a.guardArea.onBridge), new CallbackTask(() => {\n                            if (![\n                                MoveResult.Success,\n                                MoveResult.CloseEnough,\n                            ].includes(a.moveTrait.lastMoveResult)) {\n                                a.resetGuardModeToIdle();\n                            }\n                            a.guardArea = undefined;\n                        }));\n                    }\n                }\n            }\n        }\n    }\n    [NotifyDamage.onDamage](e: any, t: any, i: number, r: any): void {\n        if (!this.isDisabled() && !this.retaliateTarget && !this.opportunityFireTask && r && r.obj && r.weapon) {\n            if (this.shouldRetaliate(e, t, i, r.obj, r.weapon.warhead)) {\n                this.retaliateTarget = r.obj;\n            }\n        }\n    }\n    [NotifyTeleport.onBeforeTeleport](e: any, t: any, i: any, r: boolean): void {\n        if (!r) {\n            this.attackState = AttackState.Idle;\n            this.currentTarget = undefined;\n            this.retaliateTarget = undefined;\n            this.opportunityFireTask = undefined;\n        }\n    }\n    shouldPassiveAcquire(e: any): boolean {\n        if ((!e.owner.isCombatant() && e.rules.needsEngineer) ||\n            !e.rules.canPassiveAquire ||\n            !e.primaryWeapon ||\n            (e.ammoTrait && !e.ammoTrait.ammo && e.rules.manualReload)) {\n            return false;\n        }\n        if (e.mindControllerTrait?.isAtCapacity()) {\n            return false;\n        }\n        const t = e.rules.opportunityFire ||\n            (e.rules.balloonHover &&\n                e.unitOrderTrait.getCurrentTask()?.isAttackMove);\n        if (e.isUnit() && t) {\n            if (e.unitOrderTrait.hasTasks() &&\n                e.unitOrderTrait.getTasks()[0].preventOpportunityFire) {\n                return false;\n            }\n        }\n        else if (e.unitOrderTrait.hasTasks()) {\n            return false;\n        }\n        return true;\n    }\n    shouldRetaliate(e: any, t: any, i: number, r: any, s: any): boolean {\n        if (i < 1 ||\n            t.areFriendly(e, r) ||\n            !e.rules.canRetaliate ||\n            !e.primaryWeapon ||\n            (e.ammoTrait && !e.ammoTrait.ammo && e.rules.manualReload) ||\n            s.rules.temporal ||\n            r.rules.missileSpawn ||\n            e.unitOrderTrait.hasTasks() ||\n            !t.isValidTarget(r) ||\n            ((r.isInfantry() || r.isVehicle()) &&\n                r.disguiseTrait &&\n                !e.rules.detectDisguise) ||\n            e.mindControllerTrait?.isAtCapacity()) {\n            return false;\n        }\n        const a = this.selectWeaponVersus(e, r, t, false);\n        if (!a) {\n            return false;\n        }\n        const distance = e.isBuilding() || r.isBuilding()\n            ? this.rangeHelper.tileDistance(e, r)\n            : this.rangeHelper.distance2(e, r) / Coords.LEPTONS_PER_TILE;\n        return !(distance > Math.max(a.range, e.sight));\n    }\n    scanForTarget(e: any, t: any, i: any, r?: number, s?: any, a: boolean = false): {\n        target?: any;\n        weapon?: any;\n    } {\n        let n: {\n            target?: any;\n            weapon?: any;\n        } = {};\n        let o = Number.NEGATIVE_INFINITY;\n        const l = this.getAvailableWeapons(e, true, false);\n        const c = r ??\n            (e.rules.guardRange || t.range) +\n                1 +\n                3 +\n                i.rules.elevationModel.bonusCap +\n                (t.projectileRules.isAntiAir ? e.rules.airRangeBonus : 0);\n        for (const d of this.scanTechnosAround(e, c, i)) {\n            const u = this.selectWeaponFromList(e, d, d.tile, l, i, false, true, true);\n            if (u &&\n                this.canPassiveAcquire(d, i) &&\n                i.isValidTarget(d) &&\n                (r\n                    ? this.rangeHelper.isInRange(e, d, u.minRange, r, u.rules.cellRangefinding) &&\n                        (!s || this.rangeHelper.isInRange2(s, d, 0, r))\n                    : this.rangeHelper.isInWeaponRange(e, d, u, i.rules)) &&\n                (a || this.losHelper.hasLineOfSight(e, d, u))) {\n                let h = this.rangeHelper.distance3(e, d) /\n                    Coords.LEPTONS_PER_TILE;\n                h = this.computeThreat(d, e, u, h, i.rules.general.threat);\n                if (h > o) {\n                    n = { target: d, weapon: u } as any;\n                    o = h;\n                }\n            }\n        }\n        if (n.target && e.rules.distributedFire) {\n            this.updateDistributedFireHistory(n as any);\n        }\n        return n;\n    }\n    scanTechnosAround(e: any, t: number, i: any): any[] {\n        const r = e.getFoundation();\n        const s = new Vector2(e.tile.rx, e.tile.ry);\n        const a = new Vector2(e.tile.rx + r.width - 1, e.tile.ry + r.height - 1);\n        s.addScalar(-t);\n        a.addScalar(t);\n        const box = new Box2(s, a);\n        return i.map.technosByTile.queryRange(box);\n    }\n    canPassiveAcquire(e: any, t: any): boolean {\n        return (!e.owner.isNeutral &&\n            !e.rules.civilian &&\n            !e.rules.insignificant &&\n            (e.rules.threatPosed > 1 ||\n                (e.rules.specialThreatValue > 0 && !e.isBuilding()) ||\n                e.rules.harvester ||\n                e.name === t.rules.general.paradrop.paradropPlane));\n    }\n    computeThreat(e: any, t: any, i: any, r: number, s: any): number {\n        let n = [e.primaryWeapon, e.secondaryWeapon]\n            .filter(isNotNullOrUndefined)\n            .map((e) => e.warhead.rules.verses.get(t.rules.armor) ?? 0)\n            .reduce((e, t) => Math.max(e, t), 0) *\n            s.targetEffectivenessCoefficientDefault;\n        if (e.attackTrait?.currentTarget?.obj === t) {\n            n *= -1;\n        }\n        n +=\n            e.rules.specialThreatValue *\n                s.targetSpecialThreatCoefficientDefault;\n        n +=\n            (i.warhead.rules.verses.get(e.rules.armor) ?? 0) *\n                s.myEffectivenessCoefficientDefault;\n        n +=\n            (e.healthTrait.health / 100) *\n                s.targetStrengthCoefficientDefault;\n        n += r * s.targetDistanceCoefficientDefault;\n        n += 1e5;\n        if (t.rules.vhpScan !== VhpScan.None) {\n            const a = e.healthTrait.getProjectedHitPoints();\n            if (t.rules.vhpScan === VhpScan.Strong) {\n                if (a <= 0) {\n                    n = Number.NEGATIVE_INFINITY;\n                }\n            }\n            else if (t.rules.vhpScan === VhpScan.Normal) {\n                if (a <= 0) {\n                    n /= 2;\n                }\n                else if (a <= e.healthTrait.maxHitPoints / 2) {\n                    n *= 2;\n                }\n            }\n        }\n        if (t.rules.distributedFire) {\n            n -= 1e6 * (this.distributedFireHistory.get(e) ?? 0);\n        }\n        return n;\n    }\n    updateDistributedFireHistory(e: {\n        target: any;\n        weapon: any;\n    }): void {\n        if (this.distributedFireHistory.get(e.target) !== 50) {\n            for (const [t, i] of this.distributedFireHistory) {\n                const newValue = i - 1;\n                if (newValue <= 0) {\n                    this.distributedFireHistory.delete(t);\n                }\n                else {\n                    this.distributedFireHistory.set(t, newValue);\n                }\n            }\n            this.distributedFireHistory.set(e.target, 50);\n        }\n    }\n    dispose(): void {\n        this.distributedFireHistory.clear();\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/AutoRepairTrait.ts",
    "content": "import { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { NotifyOwnerChange } from \"@/game/gameobject/trait/interface/NotifyOwnerChange\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\ninterface GameObject {\n    healthTrait: {\n        health: number;\n        maxHitPoints: number;\n        getHitPoints(): number;\n        healBy(amount: number, gameObject: GameObject, game: any): void;\n    };\n    isInfantry(): boolean;\n    isBuilding(): boolean;\n    owner: {\n        credits: number;\n    };\n    purchaseValue: number;\n}\nexport class AutoRepairTrait implements NotifyTick, NotifyOwnerChange {\n    private freeRepair: boolean;\n    private disabled: boolean;\n    private cooldownTicks: number;\n    private healLeftover: number;\n    constructor(freeRepair: boolean = false) {\n        this.freeRepair = freeRepair;\n        this.disabled = true;\n        this.cooldownTicks = 0;\n        this.healLeftover = 0;\n    }\n    isDisabled(): boolean {\n        return this.disabled;\n    }\n    setDisabled(disabled: boolean): void {\n        this.disabled = disabled;\n    }\n    [NotifyTick.onTick](gameObject: GameObject, game: any): void {\n        if (this.isDisabled())\n            return;\n        if (gameObject.healthTrait.health === 100) {\n            this.setDisabled(true);\n            return;\n        }\n        if (this.cooldownTicks <= 0) {\n            const repairRules = game.rules.general.repair;\n            const repairRate = gameObject.isInfantry()\n                ? repairRules.iRepairRate\n                : gameObject.isBuilding()\n                    ? repairRules.repairRate\n                    : repairRules.uRepairRate;\n            this.cooldownTicks += GameSpeed.BASE_TICKS_PER_SECOND * repairRate * 60;\n            const repairStep = gameObject.isInfantry() ? repairRules.iRepairStep : repairRules.repairStep;\n            const repairPercent = this.freeRepair ? 0 : repairRules.repairPercent;\n            let healAmount: number;\n            if (repairPercent) {\n                const costPerHP = (repairPercent * gameObject.purchaseValue) / gameObject.healthTrait.maxHitPoints;\n                const maxAffordable = Math.min(gameObject.owner.credits, Math.max(1, Math.floor(costPerHP * repairStep)));\n                if (maxAffordable) {\n                    healAmount = costPerHP ? maxAffordable / costPerHP : repairStep;\n                    gameObject.owner.credits -= maxAffordable;\n                }\n                else {\n                    healAmount = 0;\n                    this.setDisabled(true);\n                }\n            }\n            else {\n                healAmount = repairStep;\n            }\n            if (healAmount) {\n                healAmount += this.healLeftover;\n                healAmount = Math.min(gameObject.healthTrait.maxHitPoints - gameObject.healthTrait.getHitPoints(), healAmount);\n                if (healAmount) {\n                    const wholeHeal = Math.floor(healAmount);\n                    this.healLeftover = healAmount - wholeHeal;\n                    if (wholeHeal) {\n                        gameObject.healthTrait.healBy(wholeHeal, gameObject, game);\n                    }\n                }\n            }\n        }\n        else {\n            this.cooldownTicks--;\n        }\n    }\n    [NotifyOwnerChange.onChange](): void {\n        this.setDisabled(true);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/BridgeTrait.ts",
    "content": "import { NotifyDamage } from \"@/game/gameobject/trait/interface/NotifyDamage\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { NotifyDestroy } from \"@/game/gameobject/trait/interface/NotifyDestroy\";\nimport { InfDeathType } from \"@/game/gameobject/infantry/InfDeathType\";\nimport { getLandType } from \"@/game/type/LandType\";\nimport { getZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nexport class BridgeTrait {\n    private bridges: any;\n    private needsImageUpdate: boolean;\n    private dominoHandled: boolean;\n    constructor(bridges: any) {\n        this.bridges = bridges;\n        this.needsImageUpdate = false;\n        this.dominoHandled = false;\n    }\n    [NotifyDamage.onDamage]() {\n        this.needsImageUpdate = true;\n    }\n    [NotifyTick.onTick](e: any) {\n        if (this.needsImageUpdate) {\n            this.needsImageUpdate = false;\n            this.bridges.handlePieceHealthChange(this.bridges.getPieceAtTile(e.tile));\n        }\n    }\n    [NotifyDestroy.onDestroy](s: any, a: any, n: any) {\n        const piece = this.bridges.getPieceAtTile(s.tile);\n        if (!this.dominoHandled) {\n            this.bridges\n                .findDominoPieces(piece)\n                .filter((e: any) => !e.obj.isDestroyed)\n                .forEach((e: any) => {\n                e.obj.traits.get(BridgeTrait).dominoHandled = true;\n                a.destroyObject(e.obj, n);\n            });\n        }\n        const tiles = a.map.tileOccupation.calculateTilesForGameObject(s.tile, s);\n        tiles.forEach((tile: any) => {\n            const landType = getLandType(tile.terrainType);\n            const landRules = a.rules.getLandRules(landType);\n            a.map.getGroundObjectsOnTile(tile).forEach((obj: any) => {\n                if (obj.isUnit() &&\n                    (obj.onBridge || obj.moveTrait.reservedPathNodes.some((node: any) => node.onBridge === s)) &&\n                    !obj.isDestroyed) {\n                    if ((s.isLowBridge() && landRules.getSpeedModifier(obj.rules.speedType) > 0) ||\n                        (obj.isInfantry() && obj.stance === StanceType.Paradrop)) {\n                        if (obj.onBridge) {\n                            obj.onBridge = false;\n                            obj.zone = getZoneType(landType);\n                        }\n                        for (const node of obj.moveTrait.reservedPathNodes) {\n                            if (node.onBridge === s) {\n                                node.onBridge = undefined;\n                            }\n                        }\n                        if (obj.moveTrait.currentWaypoint?.onBridge === s) {\n                            obj.moveTrait.currentWaypoint.onBridge = undefined;\n                        }\n                    }\n                    else {\n                        if (obj.isInfantry()) {\n                            obj.infDeathType = InfDeathType.None;\n                        }\n                        a.destroyObject(obj, n, true);\n                    }\n                }\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/C4ChargeTrait.ts",
    "content": "import { DeathType } from \"@/game/gameobject/common/DeathType\";\nimport { Timer } from \"@/game/gameobject/unit/Timer\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nexport class C4ChargeTrait {\n    private timer: Timer;\n    private attackerInfo: any;\n    constructor() {\n        this.timer = new Timer();\n    }\n    hasCharge(): boolean {\n        return this.timer.isActive();\n    }\n    setCharge(duration: number, attacker: any): void {\n        if (!this.hasCharge()) {\n            this.timer.setActiveFor(duration);\n            this.attackerInfo = attacker;\n        }\n    }\n    [NotifyTick.onTick](target: any, context: any): void {\n        if (this.timer.isActive() &&\n            this.timer.tick(context.currentTick) === true) {\n            if (!target.invulnerableTrait.isActive()) {\n                if (target.isBuilding() && target.cabHutTrait) {\n                    target.cabHutTrait.demolishBridge(context, this.attackerInfo);\n                }\n                else {\n                    target.deathType = DeathType.Demolish;\n                    context.destroyObject(target, this.attackerInfo, true);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/CabHutTrait.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { BridgeOverlayTypes } from \"@/game/map/BridgeOverlayTypes\";\nimport { ScatterTask } from \"@/game/gameobject/task/ScatterTask\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { DeathType } from \"@/game/gameobject/common/DeathType\";\nexport class CabHutTrait {\n    private gameObject: any;\n    private bridges: any;\n    private checkedClosestBridge: boolean;\n    private closestBridge: any;\n    constructor(gameObject: any, bridges: any) {\n        this.gameObject = gameObject;\n        this.bridges = bridges;\n        this.checkedClosestBridge = false;\n    }\n    canRepairBridge(): boolean {\n        const bridgeBounds = this.findClosestBridgeBounds();\n        if (bridgeBounds) {\n            return this.bridges.canBeRepaired(bridgeBounds);\n        }\n        console.warn(`No bridge associated with hut at ${this.gameObject.tile.rx}, ${this.gameObject.tile.ry}.`);\n        return false;\n    }\n    repairBridge(context: any, player: any): void {\n        const bridgeBounds = this.findClosestBridgeBounds();\n        if (!bridgeBounds) {\n            throw new Error(\"No bridge bounds found\");\n        }\n        const destroyedTiles = this.bridges.findDestroyedPieceTiles(bridgeBounds);\n        const isHorizontal = bridgeBounds.start.rx !== bridgeBounds.end.rx;\n        const overlayId = bridgeBounds.isHigh\n            ? BridgeOverlayTypes.calculateHighBridgeOverlayId(bridgeBounds.type, isHorizontal)\n            : BridgeOverlayTypes.calculateLowBridgeOverlayId(bridgeBounds.type, isHorizontal);\n        const overlayName = context.rules.getOverlayName(overlayId);\n        for (const tile of destroyedTiles) {\n            const overlay = context.createObject(ObjectType.Overlay, overlayName);\n            overlay.overlayId = overlayId;\n            overlay.value = 0;\n            overlay.position.tileElevation = bridgeBounds.isHigh ? 4 : 0;\n            context.spawnObject(overlay, tile);\n            this.updateUnitsUnderBridgePiece(tile, bridgeBounds, context, player);\n        }\n        for (const piece of this.bridges.findBridgePieces(bridgeBounds)) {\n            piece.obj.bridgeTrait.bridgeSpec = bridgeBounds;\n        }\n    }\n    updateUnitsUnderBridgePiece(tile: any, bridgeSpec: any, context: any, player: any): void {\n        for (const pieceTile of this.bridges.getPieceTiles(this.bridges.getPieceAtTile(tile))) {\n            if (bridgeSpec.isHigh) {\n                const unitsToScatter = context.map\n                    .getGroundObjectsOnTile(pieceTile)\n                    .filter((obj: any) => obj.tile === pieceTile &&\n                    obj.isUnit() &&\n                    !obj.unitOrderTrait.hasTasks() &&\n                    obj.rules.tooBigToFitUnderBridge);\n                unitsToScatter.forEach((unit: any) => unit.unitOrderTrait.addTask(new ScatterTask(context)));\n            }\n            else {\n                for (const obj of context.map.getGroundObjectsOnTile(pieceTile)) {\n                    if (obj.isUnit()) {\n                        if (context.map.terrain.getPassableSpeed(pieceTile, obj.rules.speedType, obj.isInfantry(), true)) {\n                            obj.zone = ZoneType.Ground;\n                            obj.onBridge = true;\n                        }\n                        else if (!obj.isDestroyed) {\n                            context.destroyObject(obj, { player });\n                        }\n                    }\n                }\n            }\n        }\n    }\n    demolishBridge(context: any, attacker: any): void {\n        const pieces = this.getBridgePieces();\n        if (pieces) {\n            for (const piece of pieces) {\n                if ((piece.obj.isLowBridge() &&\n                    context.map.getTileZone(piece.obj.tile, true) !== ZoneType.Water) ||\n                    !piece.obj.isDestroyed) {\n                    piece.obj.deathType = DeathType.Demolish;\n                    context.destroyObject(piece.obj, attacker, true);\n                }\n            }\n        }\n    }\n    getBridgePieces(): any[] | undefined {\n        const bridgeBounds = this.findClosestBridgeBounds();\n        if (bridgeBounds) {\n            return this.bridges.findBridgePieces(bridgeBounds);\n        }\n    }\n    findClosestBridgeBounds(): any {\n        if (!this.checkedClosestBridge) {\n            this.checkedClosestBridge = true;\n            this.closestBridge = this.bridges.findClosestBridgeSpec(this.gameObject.tile);\n        }\n        return this.closestBridge;\n    }\n    dispose(): void {\n        this.gameObject = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/CloakableTrait.ts",
    "content": "import { ObjectCloakChangeEvent } from \"@/game/event/ObjectCloakChangeEvent\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nimport { NotifyDamage } from \"@/game/gameobject/trait/interface/NotifyDamage\";\nimport { NotifySpawn } from \"@/game/gameobject/trait/interface/NotifySpawn\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nexport class CloakableTrait {\n    private gameObject: any;\n    private cloakDelayMinutes: number;\n    private isActive: boolean;\n    private cooldownTicks: number;\n    constructor(gameObject: any, cloakDelayMinutes: number) {\n        this.gameObject = gameObject;\n        this.cloakDelayMinutes = cloakDelayMinutes;\n        this.isActive = false;\n        this.resetCloakCooldown();\n    }\n    isCloaked(): boolean {\n        return this.isActive;\n    }\n    uncloak(context: any): void {\n        const wasActive = this.isActive;\n        this.resetCloakCooldown();\n        if (wasActive) {\n            this.isActive = false;\n            context.events.dispatch(new ObjectCloakChangeEvent(this.gameObject));\n        }\n    }\n    resetCloakCooldown(): void {\n        this.cooldownTicks = Math.floor(60 * this.cloakDelayMinutes * GameSpeed.BASE_TICKS_PER_SECOND);\n    }\n    [NotifySpawn.onSpawn](target: any, context: any): void {\n        this.resetCloakCooldown();\n    }\n    [NotifyTick.onTick](target: any, context: any): void {\n        if (this.cooldownTicks > 0) {\n            this.cooldownTicks--;\n        }\n        if (this.cooldownTicks <= 0 &&\n            !this.isActive &&\n            !(target.isVehicle() &&\n                target.submergibleTrait &&\n                !target.submergibleTrait.isSubmerged()) &&\n            !target.temporalTrait.getTarget()) {\n            this.isActive = true;\n            context.events.dispatch(new ObjectCloakChangeEvent(this.gameObject));\n        }\n    }\n    [NotifyDamage.onDamage](target: any, context: any): void {\n        this.uncloak(context);\n    }\n    dispose(): void {\n        this.gameObject = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/CrashableTrait.ts",
    "content": "import { ObjectCrashingEvent } from \"@/game/event/ObjectCrashingEvent\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { JumpjetLocomotor } from \"@/game/gameobject/locomotor/JumpjetLocomotor\";\nimport { WingedLocomotor } from \"@/game/gameobject/locomotor/WingedLocomotor\";\nimport { NotifyCrash } from \"@/game/gameobject/trait/interface/NotifyCrash\";\nexport class CrashableTrait {\n    private gameObject: any;\n    private crashingEvtSent: boolean;\n    private crashState: any;\n    private attackerInfo: any;\n    constructor(gameObject: any) {\n        this.gameObject = gameObject;\n        this.crashingEvtSent = false;\n        this.crashState = {};\n    }\n    [NotifyTick.onTick](target: any, context: any): void {\n        if (target.isCrashing) {\n            if (!this.crashingEvtSent ||\n                (this.crashingEvtSent = true,\n                    target.traits\n                        .filter(NotifyCrash)\n                        .forEach((trait) => trait[NotifyCrash.onCrash](target, context)),\n                    context.events.dispatch(new ObjectCrashingEvent(target)))) {\n                if (target.rules.locomotor !== LocomotorType.Jumpjet &&\n                    target.rules.locomotor !== LocomotorType.Aircraft) {\n                    throw new Error(\"Crashing logic not implemented for locomotor \" +\n                        LocomotorType[target.rules.locomotor]);\n                }\n                let movement;\n                if (target.rules.locomotor === LocomotorType.Jumpjet) {\n                    movement = JumpjetLocomotor.tickCrash(target, context, this.crashState);\n                }\n                else {\n                    if (target.rules.locomotor !== LocomotorType.Aircraft) {\n                        throw new Error(`Unhandled locomotor type \"${target.rules.locomotor}\"`);\n                    }\n                    if (!target.isAircraft()) {\n                        throw new Error(`Obj \"${target.name}#${target.id} is not an aircraft`);\n                    }\n                    movement = WingedLocomotor.tickCrash(target, context, this.crashState);\n                }\n                let shouldDestroy = false;\n                const newPosition = movement.clone().add(target.position.worldPosition);\n                if (context.map.isWithinHardBounds(newPosition)) {\n                    const oldTile = target.tile;\n                    const oldElevation = target.tileElevation;\n                    target.position.moveByLeptons3(movement);\n                    if (target.tile !== oldTile) {\n                        target.moveTrait.handleTileChange(oldTile, undefined, false, context);\n                    }\n                    const bridge = target.tile.onBridgeLandType\n                        ? context.map.tileOccupation.getBridgeOnTile(target.tile)\n                        : undefined;\n                    const bridgeElevation = bridge?.tileElevation ?? 0;\n                    target.position.tileElevation = Math.max(target.position.tileElevation, bridgeElevation);\n                    if (target.position.tileElevation === bridgeElevation) {\n                        target.zone = context.map.getTileZone(target.tile);\n                        target.onBridge = !!bridge;\n                        shouldDestroy = true;\n                    }\n                    if (target.tileElevation !== oldElevation) {\n                        target.moveTrait.handleElevationChange(oldElevation, context);\n                    }\n                }\n                else {\n                    shouldDestroy = true;\n                }\n                if (shouldDestroy) {\n                    context.destroyObject(target, this.attackerInfo);\n                }\n            }\n        }\n    }\n    crash(attacker: any): void {\n        this.attackerInfo = attacker;\n        this.gameObject.isCrashing = true;\n        this.gameObject.cachedTraits.tick.length = 0;\n        this.gameObject.cachedTraits.tick = [this];\n    }\n    dispose(): void {\n        this.gameObject = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/CrewedTrait.ts",
    "content": "import { NotifySell } from \"@/game/gameobject/trait/interface/NotifySell\";\nimport { NotifyDestroy } from \"@/game/gameobject/trait/interface/NotifyDestroy\";\nimport { SideType } from \"@/game/SideType\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { ScatterTask } from \"@/game/gameobject/task/ScatterTask\";\nimport { Infantry } from \"@/game/gameobject/Infantry\";\nimport { VeteranLevel } from \"@/game/gameobject/unit/VeteranLevel\";\nexport class CrewedTrait {\n    [NotifySell.onSell](target: any, context: any): void {\n        this.spawnSurvivors(target, context);\n    }\n    [NotifyDestroy.onDestroy](target: any, context: any, damageInfo: any, isSell: boolean): void {\n        if (!isSell &&\n            !(damageInfo?.obj === target && damageInfo.weapon?.rules.suicide) &&\n            !(target.isVehicle() && target.moveTrait.isMoving()) &&\n            !target.crashableTrait) {\n            this.spawnSurvivors(target, context);\n        }\n    }\n    private spawnSurvivors(target: any, context: any): void {\n        const crewRules = context.rules.general.crew;\n        const side = target.owner.country.side;\n        let survivorDivisor: number;\n        let crewType: string;\n        if (side === SideType.GDI) {\n            survivorDivisor = crewRules.alliedSurvivorDivisor;\n            crewType = crewRules.alliedCrew;\n        }\n        else if (side === SideType.Nod) {\n            survivorDivisor = crewRules.sovietSurvivorDivisor;\n            crewType = crewRules.sovietCrew;\n        }\n        else {\n            return;\n        }\n        let survivorCount = context.sellTrait.computeRefundValue(target) / survivorDivisor;\n        survivorCount = survivorCount > 0 && survivorCount < 1 ? 1 : Math.floor(survivorCount);\n        survivorCount = target.isVehicle() ? Math.min(1, survivorCount) : Math.min(5, survivorCount);\n        const crewTypes: string[] = [];\n        for (let i = 0; i < survivorCount; i++) {\n            crewTypes.push(crewType);\n        }\n        if (crewTypes.length > 0) {\n            if (target.rules.constructionYard) {\n                crewTypes[crewTypes.length - 1] = context.rules.general.engineer;\n            }\n            const validTiles = context.map.tiles\n                .getInRectangle(target.tile, target.getFoundation())\n                .filter((tile: any) => context.map.isWithinBounds(tile));\n            let availableTiles = [...validTiles];\n            for (const crewType of crewTypes) {\n                const infantryRules = context.rules.getObject(crewType, ObjectType.Infantry);\n                if (context.map.terrain.getPassableSpeed(target.tile, infantryRules.speedType, true, !target.isBuilding() && target.onBridge, undefined, true)) {\n                    const unit = context.createUnitForPlayer(infantryRules, target.owner);\n                    let spawnTile = availableTiles.length\n                        ? availableTiles.splice(context.generateRandomInt(0, availableTiles.length - 1), 1)[0]\n                        : undefined;\n                    spawnTile = spawnTile || validTiles[context.generateRandomInt(0, validTiles.length - 1)];\n                    if (unit.isInfantry()) {\n                        unit.position.subCell = Infantry.SUB_CELLS[0];\n                    }\n                    if (unit.veteranTrait && target.owner.canProduceVeteran(unit.rules)) {\n                        unit.veteranTrait.setVeteranLevel(VeteranLevel.Veteran);\n                    }\n                    context.spawnObject(unit, spawnTile);\n                    if (target.isBuilding()) {\n                        unit.unitOrderTrait.addTask(new ScatterTask(context, undefined, {\n                            ignoredBlockers: target.isDestroyed ? undefined : [target]\n                        }));\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/DelayedKillTrait.ts",
    "content": "import { Timer } from \"@/game/gameobject/unit/Timer\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nexport class DelayedKillTrait {\n    private timer: Timer;\n    private attackerInfo: any;\n    constructor() {\n        this.timer = new Timer();\n    }\n    isActive(): boolean {\n        return this.timer.isActive();\n    }\n    activate(ticks: number, attackerInfo: any): void {\n        if (!this.isActive()) {\n            this.timer.setActiveFor(ticks);\n            this.attackerInfo = attackerInfo;\n        }\n    }\n    [NotifyTick.onTick](target: any, context: any): void {\n        if (this.timer.isActive() && this.timer.tick(context.currentTick) === true) {\n            if (!target.invulnerableTrait.isActive() &&\n                !(target.isBuilding() && target.cabHutTrait)) {\n                context.destroyObject(target, this.attackerInfo, true, true);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/DeployerTrait.ts",
    "content": "import { StanceType } from \"../infantry/StanceType\";\nimport { NotifyTick } from \"./interface/NotifyTick\";\nenum DeployFireState {\n    None = 0,\n    PreparingToFire = 1,\n    FiringUp = 2,\n    Firing = 3\n}\ninterface GameObject {\n    isInfantry(): boolean;\n    stance: StanceType;\n    ammo: number;\n    art: {\n        fireUp: number;\n    };\n    isFiring: boolean;\n    onBridge: boolean;\n    tile: any;\n    primaryWeapon?: Weapon;\n    secondaryWeapon?: Weapon;\n    armedTrait?: {\n        getDeployFireWeapon(): Weapon | undefined;\n    };\n    rules: {\n        undeployDelay?: number;\n    };\n}\ninterface Weapon {\n    rules: {\n        areaFire?: boolean;\n        fireOnce?: boolean;\n        radLevel?: number;\n    };\n    fire(target: any, context: any): void;\n    getCooldownTicks(): number;\n    resetCooldown(): void;\n}\ninterface GameContext {\n    map: {\n        tileOccupation: {\n            getBridgeOnTile(tile: any): any;\n        };\n    };\n    mapRadiationTrait: {\n        getRadSiteLevel(tile: any): number;\n    };\n    rules: {\n        radiation: {\n            radDurationMultiple: number;\n            radLevelDelay: number;\n        };\n    };\n    createTarget(bridge: any, tile: any): any;\n}\nexport class DeployerTrait implements NotifyTick {\n    private gameObject: GameObject;\n    private deployed: boolean = false;\n    private deployFireDelay: number = 0;\n    private deployFireState: DeployFireState = DeployFireState.None;\n    private fireUpDelay: number = 0;\n    private deployFireCount: number = 0;\n    private deployWeapon?: Weapon;\n    private undeployDelay?: number;\n    constructor(gameObject: GameObject) {\n        this.gameObject = gameObject;\n    }\n    isDeployed(): boolean {\n        return this.deployed;\n    }\n    setDeployed(deployed: boolean): void {\n        const wasDeployed = this.deployed;\n        if ((this.deployed = deployed) !== wasDeployed) {\n            const gameObject = this.gameObject;\n            if (gameObject.isInfantry()) {\n                gameObject.stance = deployed ? StanceType.Deployed : StanceType.None;\n            }\n            if (deployed) {\n                this.deployFireState = DeployFireState.PreparingToFire;\n                const deployWeapon = gameObject.armedTrait?.getDeployFireWeapon();\n                this.deployWeapon = deployWeapon?.rules.areaFire ? deployWeapon : undefined;\n                const otherWeapon = deployWeapon === gameObject.primaryWeapon\n                    ? gameObject.secondaryWeapon\n                    : gameObject.primaryWeapon;\n                this.deployFireDelay = 15 + (otherWeapon?.getCooldownTicks() ?? 0);\n                this.deployFireCount = 0;\n                this.undeployDelay = gameObject.rules.undeployDelay || undefined;\n            }\n            else {\n                if (this.deployFireState === DeployFireState.FiringUp) {\n                    gameObject.isFiring = false;\n                }\n                this.deployFireState = DeployFireState.None;\n                this.deployWeapon = undefined;\n            }\n        }\n    }\n    toggleDeployed(): void {\n        this.setDeployed(!this.isDeployed());\n    }\n    [NotifyTick.onTick](gameObject: GameObject, context: GameContext): void {\n        if (this.undeployDelay !== undefined) {\n            if (this.undeployDelay > 0) {\n                this.undeployDelay--;\n            }\n            if (this.undeployDelay <= 0 &&\n                [DeployFireState.None, DeployFireState.PreparingToFire].includes(this.deployFireState)) {\n                this.undeployDelay = undefined;\n                this.setDeployed(false);\n                return;\n            }\n        }\n        if (this.deployWeapon && this.deployFireState !== DeployFireState.None) {\n            if (this.deployFireState === DeployFireState.PreparingToFire) {\n                if (this.deployFireDelay > 0) {\n                    this.deployFireDelay--;\n                    return;\n                }\n                if (gameObject.ammo === 0) {\n                    return;\n                }\n                if (this.computeDeployFireCooldown(this.deployWeapon, context) > 0) {\n                    return;\n                }\n                this.fireUpDelay = Math.max(1, gameObject.art.fireUp);\n                this.deployFireState = DeployFireState.FiringUp;\n            }\n            if (this.deployFireState === DeployFireState.FiringUp) {\n                gameObject.isFiring = true;\n                if (this.fireUpDelay > 0) {\n                    this.fireUpDelay--;\n                    return;\n                }\n                this.deployFireState = DeployFireState.Firing;\n            }\n            if (this.deployFireState === DeployFireState.Firing) {\n                gameObject.isFiring = false;\n                const bridge = gameObject.onBridge\n                    ? context.map.tileOccupation.getBridgeOnTile(gameObject.tile)\n                    : undefined;\n                this.deployWeapon.fire(context.createTarget(bridge, gameObject.tile), context);\n                this.deployFireCount++;\n                const otherWeapon = this.deployWeapon === gameObject.primaryWeapon\n                    ? gameObject.secondaryWeapon\n                    : gameObject.primaryWeapon;\n                otherWeapon?.resetCooldown();\n                if (this.deployWeapon.rules.fireOnce) {\n                    this.deployFireState = DeployFireState.None;\n                    this.deployWeapon = undefined;\n                }\n                else {\n                    this.deployFireState = DeployFireState.PreparingToFire;\n                }\n            }\n        }\n    }\n    private computeDeployFireCooldown(weapon: Weapon, context: GameContext): number {\n        if (weapon.rules.radLevel && weapon.rules.areaFire) {\n            const tile = this.gameObject.tile;\n            const radLevel = context.mapRadiationTrait.getRadSiteLevel(tile);\n            if (!radLevel) {\n                return 0;\n            }\n            const radiation = context.rules.radiation;\n            let cooldown = Math.max(0, radLevel * radiation.radDurationMultiple - radiation.radLevelDelay);\n            if (this.deployFireCount === 1) {\n                const radDuration = radiation.radDurationMultiple * weapon.rules.radLevel!;\n                cooldown = Math.max(0, cooldown - Math.floor(0.25 * radDuration));\n            }\n            return cooldown;\n        }\n        return weapon.getCooldownTicks();\n    }\n    getHash(): number {\n        return this.deployed ? 1 : 0;\n    }\n    debugGetState(): {\n        deployed: boolean;\n    } {\n        return { deployed: this.deployed };\n    }\n    dispose(): void {\n        this.gameObject = undefined as any;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/DisguiseTrait.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { ObjectDisguiseChangeEvent } from \"@/game/event/ObjectDisguiseChangeEvent\";\nimport { SideType } from \"@/game/SideType\";\nimport { AttackTrait, AttackState } from \"@/game/gameobject/trait/AttackTrait\";\nimport { NotifyDamage } from \"@/game/gameobject/trait/interface/NotifyDamage\";\nimport { NotifySpawn } from \"@/game/gameobject/trait/interface/NotifySpawn\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { MoveTrait, MoveState } from \"@/game/gameobject/trait/MoveTrait\";\nexport class DisguiseTrait {\n    private isActive: boolean;\n    private cooldownTicks: number;\n    private disguisedAs?: {\n        rules: any;\n        owner: any;\n    };\n    constructor() {\n        this.isActive = false;\n        this.cooldownTicks = 0;\n    }\n    isDisguised(): boolean {\n        return this.isActive;\n    }\n    getDisguise(): {\n        rules: any;\n        owner: any;\n    } | undefined {\n        return this.isActive ? this.disguisedAs : undefined;\n    }\n    hasTerrainDisguise(): boolean {\n        return this.getDisguise()?.rules.type === ObjectType.Terrain;\n    }\n    disguiseAs(target: any, gameObject: any, context: any): void {\n        this.disguisedAs = { rules: target.rules, owner: target.owner };\n        this.isActive = true;\n        context.events.dispatch(new ObjectDisguiseChangeEvent(gameObject));\n    }\n    revealDisguise(gameObject: any, context: any): void {\n        this.cooldownTicks = context.rules.general.infantryBlinkDisguiseTime;\n        this.isActive = false;\n        context.events.dispatch(new ObjectDisguiseChangeEvent(gameObject));\n    }\n    [NotifySpawn.onSpawn](gameObject: any, context: any): void {\n        if (!this.disguisedAs &&\n            gameObject.rules.permaDisguise &&\n            gameObject.isInfantry() &&\n            gameObject.owner.country) {\n            const defaultDisguise = this.getDefaultInfantryDisguise(gameObject.owner.country.side, context.rules.general);\n            if (defaultDisguise) {\n                const infantryRules = context.rules.getObject(defaultDisguise, ObjectType.Infantry);\n                this.disguisedAs = { rules: infantryRules, owner: gameObject.owner };\n                this.isActive = true;\n            }\n        }\n    }\n    getDefaultInfantryDisguise(side: SideType, generalRules: any): string | undefined {\n        switch (side) {\n            case SideType.GDI:\n                return generalRules.alliedDisguise;\n            case SideType.Nod:\n                return generalRules.sovietDisguise;\n            default:\n                return undefined;\n        }\n    }\n    [NotifyTick.onTick](gameObject: any, context: any): void {\n        if (!gameObject.rules.permaDisguise) {\n            if (gameObject.attackTrait?.attackState === AttackState.JustFired ||\n                gameObject.moveTrait.moveState !== MoveState.Idle) {\n                this.revealDisguise(gameObject, context);\n            }\n            else if (this.cooldownTicks > 0) {\n                this.cooldownTicks--;\n            }\n            else if (!this.isActive && gameObject.rules.disguiseWhenStill) {\n                this.isActive = true;\n                this.disguisedAs = {\n                    rules: this.selectRandomMirageDisguise(context),\n                    owner: undefined\n                };\n                context.events.dispatch(new ObjectDisguiseChangeEvent(gameObject));\n            }\n        }\n    }\n    [NotifyDamage.onDamage](gameObject: any, context: any): void {\n        this.revealDisguise(gameObject, context);\n    }\n    selectRandomMirageDisguise(context: any): any {\n        const disguises = context.rules.general.defaultMirageDisguises;\n        if (!disguises.length) {\n            throw new Error(\"No default mirage disguises are defined\");\n        }\n        const randomDisguise = disguises[context.generateRandomInt(0, disguises.length - 1)];\n        return context.rules.getObject(randomDisguise, ObjectType.Terrain);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/DockTrait.ts",
    "content": "import { NotifyDestroy } from './interface/NotifyDestroy';\nimport { NotifyOwnerChange } from './interface/NotifyOwnerChange';\nimport { NotifySell } from './interface/NotifySell';\nimport { DockableTrait } from './DockableTrait';\nimport { Coords } from '@/game/Coords';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { NotifySpawn } from './interface/NotifySpawn';\nimport { isNotNullOrUndefined } from '@/util/typeGuard';\nimport { MoveToDockTask } from '../task/MoveToDockTask';\nimport { MoveTask } from '../task/move/MoveTask';\nimport { CallbackTask } from '../task/system/CallbackTask';\nimport { TaskGroup } from '../task/system/TaskGroup';\nimport { NotifyUnspawn } from './interface/NotifyUnspawn';\ninterface Building {\n    name: string;\n    position: {\n        getMapPosition(): {\n            x: number;\n            y: number;\n        };\n    };\n    owner: {\n        buildings: Set<Building>;\n    };\n    rules: {\n        unitRepair?: boolean;\n        naval: boolean;\n    };\n    helipadTrait?: any;\n    unitOrderTrait?: any;\n    unitRepairTrait?: any;\n    warpedOutTrait: {\n        isActive(): boolean;\n    };\n    traits: {\n        find<T>(trait: new (...args: any[]) => T): T | undefined;\n    };\n}\ninterface Unit {\n    name: string;\n    tile: Tile;\n    isDestroyed: boolean;\n    owner: any;\n    rules: {\n        consideredAircraft?: boolean;\n        landable?: boolean;\n        dock: string[];\n        naval: boolean;\n    };\n    traits: {\n        find<T>(trait: new (...args: any[]) => T): T | undefined;\n        get<T>(trait: new (...args: any[]) => T): T;\n    };\n    unitOrderTrait: {\n        addTask(task: any): void;\n    };\n    crashableTrait?: {\n        crash(context: {\n            player: any;\n        }): void;\n    };\n    isVehicle(): boolean;\n    isAircraft(): boolean;\n}\ninterface Tile {\n}\ninterface Tiles {\n    getByMapCoords(x: number, y: number): Tile | undefined;\n}\ninterface DockOffset {\n    x: number;\n    z: number;\n}\ninterface GameContext {\n    destroyObject(obj: Unit, attacker?: any, weapon?: any): void;\n    sellTrait: {\n        sell(unit: Unit): void;\n    };\n    changeObjectOwner(obj: Unit, newOwner: any): void;\n}\nexport class DockTrait implements NotifyDestroy, NotifyOwnerChange, NotifySell, NotifyTick, NotifySpawn, NotifyUnspawn {\n    private building: Building;\n    private tiles: Tiles;\n    private numberOfDocks: number;\n    private dockingOffsets: DockOffset[];\n    private ticksWhenWarpedOut: boolean = true;\n    private unitsByDockNumber: (Unit | undefined)[];\n    private reservedDocks: (Unit | undefined)[];\n    private dockTiles: Tile[] = [];\n    constructor(building: Building, tiles: Tiles, numberOfDocks: number, dockingOffsets: DockOffset[]) {\n        this.building = building;\n        this.tiles = tiles;\n        this.numberOfDocks = numberOfDocks;\n        this.dockingOffsets = dockingOffsets;\n        this.unitsByDockNumber = new Array(numberOfDocks).fill(undefined);\n        this.reservedDocks = new Array(numberOfDocks).fill(undefined);\n    }\n    [NotifySpawn.onSpawn](): void {\n        this.dockTiles = [];\n        for (let i = 0; i < this.numberOfDocks; i++) {\n            const tile = this.findDockTile(i);\n            if (!tile) {\n                throw new Error(`Docking tile ${i} not found for object \"${this.building.name}\"`);\n            }\n            this.dockTiles[i] = tile;\n        }\n    }\n    [NotifyUnspawn.onUnspawn](): void {\n        for (let i = 0; i < this.numberOfDocks; i++) {\n            this.unreserveDockAt(i);\n        }\n    }\n    [NotifyTick.onTick](): void {\n        for (let i = 0; i < this.numberOfDocks; i++) {\n            const unit = this.unitsByDockNumber[i];\n            if (unit && unit.tile !== this.getDockTile(i)) {\n                this.undockUnit(unit);\n            }\n        }\n    }\n    [NotifyDestroy.onDestroy](target: Building, context: GameContext, attacker?: any, weapon?: any): void {\n        const shouldRepairUnits = (target.rules.unitRepair || target.helipadTrait) &&\n            !target.rules.naval &&\n            !attacker?.weapon?.warhead.rules.temporal;\n        if (shouldRepairUnits) {\n            for (const unit of this.unitsByDockNumber) {\n                if (unit && !unit.isDestroyed) {\n                    if (shouldRepairUnits) {\n                        context.destroyObject(unit, attacker, weapon);\n                    }\n                    else {\n                        this.undockUnit(unit);\n                    }\n                }\n            }\n        }\n    }\n    [NotifySell.onSell](building: Building, context: GameContext): void {\n        if (building.helipadTrait && this.hasDockedUnits()) {\n            const availableHelipads: Building[] = [];\n            let unitsToRelocate = 0;\n            for (const otherBuilding of [...building.owner.buildings].filter(b => b.helipadTrait &&\n                ((b as any).dockTrait?.getAvailableDockCount() ?? false) &&\n                b !== building)) {\n                let availableDocks = (otherBuilding as any).dockTrait?.getAvailableDockCount() ?? 0;\n                while (availableDocks > 0 && unitsToRelocate < this.unitsByDockNumber.length) {\n                    availableHelipads.push(otherBuilding);\n                    availableDocks--;\n                    unitsToRelocate++;\n                }\n                if (unitsToRelocate === this.unitsByDockNumber.length)\n                    break;\n            }\n            let helipadIndex = 0;\n            for (const unit of this.unitsByDockNumber) {\n                if (unit) {\n                    const targetHelipad = availableHelipads[helipadIndex];\n                    if (targetHelipad) {\n                        unit.unitOrderTrait.addTask(new MoveToDockTask(context as any, targetHelipad));\n                    }\n                    else {\n                        unit.unitOrderTrait.addTask(new TaskGroup(new MoveTask(context as any, unit.tile as any, false), new CallbackTask((unit: Unit) => {\n                            if (unit.crashableTrait) {\n                                unit.crashableTrait.crash({ player: building.owner });\n                            }\n                            else {\n                                context.destroyObject(unit, { player: building.owner });\n                            }\n                        })).setCancellable(false));\n                    }\n                    helipadIndex++;\n                }\n            }\n        }\n        else {\n            const shouldSellUnits = building.rules.unitRepair && !building.rules.naval;\n            for (const unit of this.unitsByDockNumber) {\n                if (unit) {\n                    if (shouldSellUnits) {\n                        context.sellTrait.sell(unit);\n                    }\n                    else {\n                        this.undockUnit(unit);\n                    }\n                }\n            }\n        }\n    }\n    [NotifyOwnerChange.onChange](building: Building, oldOwner: any, context: GameContext): void {\n        for (const unit of this.unitsByDockNumber) {\n            if (unit) {\n                context.changeObjectOwner(unit, building.owner);\n            }\n        }\n    }\n    getFirstAvailableDockNumber(): number | undefined {\n        if (!this.building?.warpedOutTrait.isActive()) {\n            const index = this.unitsByDockNumber.findIndex((unit, i) => !unit && !this.reservedDocks[i]);\n            return index !== -1 ? index : undefined;\n        }\n        return undefined;\n    }\n    getAvailableDockCount(): number {\n        if (this.building?.warpedOutTrait.isActive()) {\n            return 0;\n        }\n        return this.unitsByDockNumber.filter((unit, i) => !unit && !this.reservedDocks[i]).length;\n    }\n    getFirstEmptyDockNumber(): number | undefined {\n        if (!this.building?.warpedOutTrait.isActive()) {\n            const index = this.unitsByDockNumber.findIndex(unit => !unit);\n            return index !== -1 ? index : undefined;\n        }\n        return undefined;\n    }\n    getDockOffset(dockIndex: number): DockOffset {\n        if (dockIndex > this.numberOfDocks - 1) {\n            throw new RangeError(`Index ${dockIndex} exceeds available docks (${this.numberOfDocks})`);\n        }\n        return this.dockingOffsets[dockIndex];\n    }\n    getDockTile(dockIndex: number): Tile {\n        if (dockIndex > this.numberOfDocks - 1) {\n            throw new RangeError(`Index ${dockIndex} exceeds available docks (${this.numberOfDocks})`);\n        }\n        return this.dockTiles[dockIndex];\n    }\n    getDockNumberByTile(tile: Tile): number | undefined {\n        const index = this.dockTiles.indexOf(tile);\n        return index !== -1 ? index : undefined;\n    }\n    getAllDockTiles(): Tile[] {\n        return [...this.dockTiles];\n    }\n    private findDockTile(dockIndex: number): Tile | undefined {\n        if (dockIndex > this.numberOfDocks - 1) {\n            throw new RangeError(`Index ${dockIndex} exceeds available docks (${this.numberOfDocks})`);\n        }\n        const mapPos = this.building.position.getMapPosition();\n        const offset = this.getDockOffset(dockIndex);\n        return this.tiles.getByMapCoords(Math.floor((mapPos.x + offset.x) / Coords.LEPTONS_PER_TILE), Math.floor((mapPos.y + offset.z) / Coords.LEPTONS_PER_TILE));\n    }\n    isValidUnitForDock(unit: Unit): boolean {\n        const isRepairableVehicle = this.building.unitRepairTrait &&\n            unit.isVehicle() &&\n            !this.building.helipadTrait &&\n            (!unit.rules.consideredAircraft || unit.rules.landable);\n        const isDockableUnit = unit.rules.dock.includes(this.building.name) &&\n            !(unit.isAircraft() && !this.building.helipadTrait);\n        const navalMatch = this.building.rules.naval === unit.rules.naval;\n        return (isRepairableVehicle || isDockableUnit) && navalMatch;\n    }\n    dockUnitAt(unit: Unit, dockIndex: number): void {\n        if (dockIndex > this.numberOfDocks - 1) {\n            throw new RangeError(`Index ${dockIndex} exceeds available docks (${this.numberOfDocks})`);\n        }\n        if (this.unitsByDockNumber[dockIndex]) {\n            throw new Error(`Another unit is already docked at dock #${dockIndex}`);\n        }\n        const dockableTrait = unit.traits.find(DockableTrait);\n        if (!dockableTrait) {\n            throw new Error(`Unit \"${unit.name}\" cannot be docked to ${this.building.name}`);\n        }\n        this.unitsByDockNumber[dockIndex] = unit;\n        dockableTrait.dock = this.building;\n    }\n    undockUnitAt(dockIndex: number): void {\n        if (dockIndex > this.numberOfDocks - 1) {\n            throw new RangeError(`Index ${dockIndex} exceeds available docks (${this.numberOfDocks})`);\n        }\n        const unit = this.unitsByDockNumber[dockIndex];\n        if (unit) {\n            this.unitsByDockNumber[dockIndex] = undefined;\n            unit.traits.get(DockableTrait).dock = undefined;\n        }\n    }\n    undockUnit(unit: Unit): void {\n        const index = this.unitsByDockNumber.indexOf(unit);\n        if (index !== -1) {\n            this.undockUnitAt(index);\n        }\n    }\n    isDocked(unit: Unit): boolean {\n        return this.unitsByDockNumber.includes(unit);\n    }\n    hasDockedUnits(): boolean {\n        return !!this.unitsByDockNumber.find(unit => unit);\n    }\n    getDockedUnits(): Unit[] {\n        return this.unitsByDockNumber.filter(isNotNullOrUndefined);\n    }\n    reserveDockAt(unit: Unit, dockIndex: number): void {\n        if (dockIndex > this.numberOfDocks - 1) {\n            throw new RangeError(`Index ${dockIndex} exceeds available docks (${this.numberOfDocks})`);\n        }\n        if (this.reservedDocks[dockIndex]) {\n            throw new Error(`Dock #${dockIndex} is already reserved by ${this.reservedDocks[dockIndex]!.name}`);\n        }\n        this.reservedDocks[dockIndex] = unit;\n        const dockableTrait = unit.traits.get(DockableTrait);\n        dockableTrait.reservedDock?.dockTrait.unreserveDockForUnit(unit);\n        dockableTrait.reservedDock = this.building;\n    }\n    unreserveDockAt(dockIndex: number): void {\n        if (dockIndex > this.numberOfDocks - 1) {\n            throw new RangeError(`Index ${dockIndex} exceeds available docks (${this.numberOfDocks})`);\n        }\n        const unit = this.reservedDocks[dockIndex];\n        if (unit) {\n            this.reservedDocks[dockIndex] = undefined;\n            unit.traits.get(DockableTrait).reservedDock = undefined;\n        }\n    }\n    unreserveDockForUnit(unit: Unit): void {\n        const index = this.reservedDocks.indexOf(unit);\n        if (index !== -1) {\n            this.unreserveDockAt(index);\n        }\n    }\n    hasReservedDockForUnit(unit: Unit): boolean {\n        return this.reservedDocks.includes(unit);\n    }\n    hasReservedDockAt(dockIndex: number): boolean {\n        return !!this.reservedDocks[dockIndex];\n    }\n    getReservedDockForUnit(unit: Unit): number | undefined {\n        const index = this.reservedDocks.indexOf(unit);\n        return index !== -1 ? index : undefined;\n    }\n    dispose(): void {\n        this.building = undefined as any;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/DockableTrait.ts",
    "content": "import { NotifyUnspawn } from \"@/game/gameobject/trait/interface/NotifyUnspawn\";\nimport { NotifyOwnerChange } from \"@/game/gameobject/trait/interface/NotifyOwnerChange\";\nimport { NotifyTeleport } from \"@/game/gameobject/trait/interface/NotifyTeleport\";\nexport class DockableTrait {\n    public dock?: any;\n    public reservedDock?: any;\n    [NotifyUnspawn.onUnspawn](target: any): void {\n        this.undock(target);\n        this.reservedDock?.dockTrait.unreserveDockForUnit(target);\n    }\n    [NotifyOwnerChange.onChange](target: any): void {\n        if (target.owner !== this.dock?.owner) {\n            this.undock(target);\n        }\n        if (target.owner !== this.reservedDock?.owner) {\n            this.reservedDock?.dockTrait.unreserveDockForUnit(target);\n        }\n    }\n    [NotifyTeleport.onBeforeTeleport](target: any, context: any, tile: any, keepDock: boolean): void {\n        if (!keepDock) {\n            this.undock(target);\n        }\n    }\n    undock(target: any): void {\n        if (this.dock && !this.dock.isDisposed) {\n            this.dock.dockTrait.undockUnit(target);\n        }\n    }\n    dispose(): void {\n        this.dock = undefined;\n        this.reservedDock = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/FactoryTrait.ts",
    "content": "import { Building, BuildStatus } from \"@/game/gameobject/Building\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { ProductionQueue, QueueType, QueueStatus } from \"@/game/player/production/ProductionQueue\";\nimport { TechnoRules, FactoryType } from \"@/game/rules/TechnoRules\";\nimport { ExitFactoryTask } from \"@/game/gameobject/task/move/ExitFactoryTask\";\nimport { TerrainType } from \"@/engine/type/TerrainType\";\nimport { NotifySpawn } from \"@/game/gameobject/trait/interface/NotifySpawn\";\nimport { MoveTrait, MoveState } from \"@/game/gameobject/trait/MoveTrait\";\nimport { CardinalTileFinder } from \"@/game/map/tileFinder/CardinalTileFinder\";\nimport { DockTrait } from \"@/game/gameobject/trait/DockTrait\";\nimport { FactoryProduceUnitEvent } from \"@/game/event/FactoryProduceUnitEvent\";\nimport { Infantry } from \"@/game/gameobject/Infantry\";\nimport { TileOccupation, LayerType } from \"@/game/map/TileOccupation\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { NotifyWarpChange } from \"@/game/gameobject/trait/interface/NotifyWarpChange\";\nimport { VeteranLevel } from \"@/game/gameobject/unit/VeteranLevel\";\nimport { NotifyProduceUnit } from \"@/game/trait/interface/NotifyProduceUnit\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { NotifyOwnerChange } from \"@/game/gameobject/trait/interface/NotifyOwnerChange\";\nimport { NotifyDestroy } from \"@/game/gameobject/trait/interface/NotifyDestroy\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { WaitMinutesTask } from \"@/game/gameobject/task/system/WaitMinutesTask\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { TaskGroup } from \"@/game/gameobject/task/system/TaskGroup\";\nexport enum FactoryStatus {\n    Idle = 0,\n    Delivering = 1\n}\nexport class FactoryTrait {\n    public type: FactoryType;\n    public isCloningVats: boolean;\n    public status: FactoryStatus;\n    public deliveringUnit?: any;\n    public buildingProductionTicks?: number;\n    constructor(type: FactoryType, isCloningVats: boolean = false) {\n        this.type = type;\n        this.isCloningVats = isCloningVats;\n        this.status = FactoryStatus.Idle;\n    }\n    [NotifySpawn.onSpawn](building: any, world: any): void {\n        this.resetRallyPoint(building, world);\n    }\n    resetRallyPoint(building: any, world: any): void {\n        if (![FactoryType.BuildingType, FactoryType.AircraftType].includes(this.type)) {\n            const rallyPoint = this.computeDefaultRallyPoint(building, this.type, world.map);\n            building.rallyTrait?.changeRallyPoint(rallyPoint, building, world);\n        }\n    }\n    [NotifyWarpChange.onChange](building: any, oldValue: any, world: any): void {\n        if (building.owner.production) {\n            let queueTypes: QueueType[] = [];\n            if (this.type === FactoryType.BuildingType) {\n                queueTypes = [QueueType.Structures, QueueType.Armory];\n            }\n            else {\n                queueTypes = [building.owner.production.getQueueTypeForFactory(this.type)];\n            }\n            for (const queueType of queueTypes) {\n                building.owner.production.getQueue(queueType).notifyUpdated();\n            }\n        }\n    }\n    [NotifyOwnerChange.onChange](building: any, oldOwner: any, world: any): void {\n        if (this.status === FactoryStatus.Delivering &&\n            building.rules.deployTime &&\n            this.deliveringUnit &&\n            !this.deliveringUnit.isDestroyed &&\n            this.unitIsInsideFactory(this.deliveringUnit, building, world)) {\n            world.changeObjectOwner(this.deliveringUnit, building.owner);\n        }\n    }\n    [NotifyDestroy.onDestroy](building: any, world: any, attacker: any, weapon: any): void {\n        if (this.status === FactoryStatus.Delivering &&\n            building.rules.deployTime &&\n            this.deliveringUnit &&\n            !this.deliveringUnit.isDestroyed &&\n            this.unitIsInsideFactory(this.deliveringUnit, building, world)) {\n            world.destroyObject(this.deliveringUnit, attacker, weapon);\n        }\n    }\n    [NotifyTick.onTick](building: any, world: any): void {\n        if (this.status === FactoryStatus.Delivering) {\n            if (!this.deliveringUnit || this.deliveringUnit.isDestroyed) {\n                this.buildingProductionTicks = this.buildingProductionTicks ?? 1;\n                if (this.buildingProductionTicks-- > 0) {\n                    return;\n                }\n                this.buildingProductionTicks = undefined;\n            }\n            else if (!this.unitHasClearedFactory(this.deliveringUnit, building, world)) {\n                return;\n            }\n            this.status = FactoryStatus.Idle;\n            this.deliveringUnit = undefined;\n            return;\n        }\n        if (building.owner.production && !building.warpedOutTrait.isActive()) {\n            const primaryFactory = building.owner.production.getPrimaryFactory(this.type);\n            if ((primaryFactory?.warpedOutTrait.isActive() ||\n                primaryFactory === building ||\n                (primaryFactory?.factoryTrait?.deliveringUnit &&\n                    primaryFactory.factoryTrait.type === FactoryType.UnitType)) &&\n                this.type !== FactoryType.BuildingType) {\n                const queue = building.owner.production.getQueueForFactory(this.type);\n                if (queue && queue.status === QueueStatus.Ready) {\n                    const item = queue.getFirst();\n                    if (this.type === FactoryType.AircraftType) {\n                        let produced = this.produceAircraftAt(building, item, world);\n                        if (!produced) {\n                            const otherAirports = [...building.owner.buildings].filter((b: any) => b.factoryTrait?.type === FactoryType.AircraftType && b.helipadTrait);\n                            for (const airport of otherAirports) {\n                                if (produced)\n                                    break;\n                                produced = this.produceAircraftAt(airport, item, world);\n                            }\n                        }\n                        if (!produced)\n                            return;\n                    }\n                    else {\n                        this.produceGroundUnitAt(building, item, world);\n                        if (!this.isCloningVats && this.type === FactoryType.InfantryType) {\n                            const cloningVats = [...building.owner.buildings].filter((b: any) => b.factoryTrait && b.rules.cloning);\n                            for (const vat of cloningVats) {\n                                if (vat.factoryTrait.status === FactoryStatus.Idle) {\n                                    vat.factoryTrait.produceGroundUnitAt(vat, item, world);\n                                }\n                            }\n                        }\n                    }\n                    building.owner.addUnitsBuilt(item.rules, 1);\n                    item.creditsSpent = 0;\n                    item.progress = 0;\n                    queue.shift(item.rules, 1);\n                    if (queue.currentSize) {\n                        queue.status = QueueStatus.Active;\n                    }\n                }\n            }\n        }\n    }\n    unitIsInsideFactory(unit: any, building: any, world: any): boolean {\n        return world.map.tileOccupation.isTileOccupiedBy(unit.tile, building) &&\n            unit.zone !== ZoneType.Air;\n    }\n    unitHasClearedFactory(unit: any, building: any, world: any): boolean {\n        return !world.map.tileOccupation.isTileOccupiedBy(unit.tile, building) ||\n            (unit.rules.consideredAircraft && unit.position.tileElevation >= building.art.height);\n    }\n    produceGroundUnitAt(building: any, item: any, world: any): void {\n        const unit = world.createUnitForPlayer(item.rules, building.owner);\n        if (item.rules.trainable && building.owner.canProduceVeteran(unit.rules)) {\n            unit.veteranTrait?.setVeteranLevel(VeteranLevel.Veteran);\n        }\n        if (unit.isInfantry()) {\n            unit.position.subCell = Infantry.SUB_CELLS[0];\n        }\n        let rallyPoint = this.computeInternalRallyPoint(building, this.type, building.rallyTrait.getRallyPoint(), world.map);\n        if (this.type !== FactoryType.UnitType) {\n            rallyPoint = building.rallyTrait.findRallyPointforUnit(unit, rallyPoint, world.map, false, building.tile.z);\n        }\n        let spawnTile: any;\n        if (this.type === FactoryType.NavalUnitType) {\n            spawnTile = rallyPoint;\n        }\n        else {\n            const exitCoords = this.computeExitCoords(building, this.type);\n            spawnTile = world.map.tiles.getByMapCoords(Math.floor(exitCoords.rx), Math.floor(exitCoords.ry));\n        }\n        if (unit.rules.consideredAircraft) {\n            rallyPoint = spawnTile;\n        }\n        let rallyNode: any;\n        if (building.rallyTrait.getRallyPoint() !== rallyPoint) {\n            rallyNode = building.rallyTrait.findRallyNodeForUnit(unit, world.map);\n        }\n        if (unit.isInfantry()) {\n            const occupiedSubCells = world.map.tileOccupation\n                .getObjectsOnTileByLayer(rallyNode?.tile ?? rallyPoint, unit.rules.consideredAircraft ? LayerType.Air : LayerType.Ground)\n                .filter((obj: any) => obj.isInfantry() && obj.moveTrait.moveState !== MoveState.Moving)\n                .map((obj: any) => obj.position.subCell);\n            unit.position.subCell = Infantry.SUB_CELLS.find(sc => !occupiedSubCells.includes(sc)) ?? Infantry.SUB_CELLS[0];\n        }\n        const createMoveTask = () => {\n            if (unit.rules.consideredAircraft) {\n                const target = rallyNode ?? { tile: rallyPoint, onBridge: undefined };\n                unit.unitOrderTrait.addTaskNext(new MoveTask(world, target.tile, !!target.onBridge, {\n                    closeEnoughTiles: world.rules.general.closeEnough\n                }));\n            }\n            else {\n                unit.unitOrderTrait.addTaskNext(new ExitFactoryTask(world, building, rallyPoint, rallyNode));\n            }\n        };\n        unit.direction = 270;\n        world.spawnObject(unit, spawnTile);\n        world.traits.filter(NotifyProduceUnit).forEach((trait: any) => {\n            trait[NotifyProduceUnit.onProduce](unit, world);\n        });\n        world.events.dispatch(new FactoryProduceUnitEvent(unit));\n        if (building.rules.deployTime) {\n            unit.unitOrderTrait.addTask(new TaskGroup(new WaitMinutesTask(building.rules.deployTime), new CallbackTask(() => {\n                if (building.isSpawned && building.buildStatus !== BuildStatus.BuildDown) {\n                    createMoveTask();\n                }\n            })).setCancellable(false));\n        }\n        else {\n            createMoveTask();\n        }\n        this.status = FactoryStatus.Delivering;\n        this.deliveringUnit = unit;\n    }\n    produceAircraftAt(building: any, item: any, world: any): boolean {\n        const dockTrait = building.traits.find(DockTrait);\n        if (!dockTrait)\n            return false;\n        const dockNumber = dockTrait.getFirstAvailableDockNumber();\n        if (dockNumber === undefined)\n            return false;\n        const unit = world.createUnitForPlayer(item.rules, building.owner);\n        if (item.rules.trainable && building.owner.canProduceVeteran(unit.rules)) {\n            unit.veteranTrait?.setVeteranLevel(VeteranLevel.Veteran);\n        }\n        const dockOffset = dockTrait.getDockOffset(dockNumber);\n        unit.position.moveToLeptons(building.position.getMapPosition());\n        unit.position.moveByLeptons3(dockOffset);\n        world.spawnObject(unit, unit.position.tile);\n        dockTrait.dockUnitAt(unit, dockNumber);\n        if (unit.isAircraft() && unit.airportBoundTrait) {\n            unit.airportBoundTrait.preferredAirport = building;\n        }\n        world.traits.filter(NotifyProduceUnit).forEach((trait: any) => {\n            trait[NotifyProduceUnit.onProduce](unit, world);\n        });\n        world.events.dispatch(new FactoryProduceUnitEvent(unit));\n        return true;\n    }\n    computeExitCoords(building: any, factoryType: FactoryType): {\n        rx: number;\n        ry: number;\n    } {\n        if (factoryType === FactoryType.InfantryType) {\n            return this.computeBarracksDefaultExitCoords(building);\n        }\n        if (factoryType === FactoryType.UnitType) {\n            return this.computeWarFactoryExitCoords(building);\n        }\n        throw new Error(\"Unsupported factory type \" + FactoryType[factoryType]);\n    }\n    computeInternalRallyPoint(building: any, factoryType: FactoryType, rallyPoint: any, map: any): any {\n        let coords: {\n            rx: number;\n            ry: number;\n        };\n        let tile: any;\n        if (factoryType === FactoryType.NavalUnitType) {\n            tile = this.computeNavalInternalRallyPoint(building, rallyPoint, map);\n        }\n        else {\n            if (factoryType === FactoryType.InfantryType) {\n                coords = this.computeBarracksInternalRallyCoords(building);\n            }\n            else if (factoryType === FactoryType.UnitType) {\n                coords = this.computeWarFactoryInternalRallyCoords(building);\n            }\n            else {\n                throw new Error(\"Unsupported factory type \" + FactoryType[factoryType]);\n            }\n            tile = map.tiles.getByMapCoords(coords.rx, coords.ry);\n        }\n        return tile ?? this.findTileAdjacentToBuilding(building, map);\n    }\n    computeDefaultRallyPoint(building: any, factoryType: FactoryType, map: any): any {\n        let coords: {\n            rx: number;\n            ry: number;\n        };\n        let tile: any;\n        if (factoryType === FactoryType.NavalUnitType) {\n            tile = this.computeNavalDefaultRallyPoint(building, map);\n        }\n        else {\n            if (factoryType === FactoryType.InfantryType) {\n                coords = this.computeBarracksInternalRallyCoords(building);\n            }\n            else if (factoryType === FactoryType.UnitType) {\n                coords = this.computeWarFactoryDefaultRallyCoords(building);\n            }\n            else {\n                throw new Error(\"Unsupported factory type \" + FactoryType[factoryType]);\n            }\n            tile = map.tiles.getByMapCoords(coords.rx, coords.ry);\n        }\n        return tile ?? this.findTileAdjacentToBuilding(building, map);\n    }\n    findTileAdjacentToBuilding(building: any, map: any): any {\n        return new RadialTileFinder(map.tiles, map.mapBounds, building.tile, building.getFoundation(), 1, 1, () => true).getNextTile();\n    }\n    computeBarracksDefaultExitCoords(building: any): {\n        rx: number;\n        ry: number;\n    } {\n        const foundation = building.getFoundation();\n        let x: number, y: number;\n        if (foundation.width <= 2 || foundation.height <= 2) {\n            x = foundation.width - 1;\n            y = foundation.height - 1;\n            if (building.rules.gdiBarracks && foundation.width > 2) {\n                x = Math.floor(foundation.width / 2);\n            }\n        }\n        else {\n            x = 0;\n            y = foundation.height - 1;\n        }\n        return { rx: building.tile.rx + x, ry: building.tile.ry + y };\n    }\n    computeBarracksInternalRallyCoords(building: any): {\n        rx: number;\n        ry: number;\n    } {\n        const foundation = building.getFoundation();\n        let { rx, ry } = this.computeBarracksDefaultExitCoords(building);\n        if (foundation.width <= 2 || foundation.height <= 2 || building.rules.gdiBarracks) {\n            ry += 1;\n        }\n        else if (building.rules.nodBarracks) {\n            rx += foundation.width <= 2 ? 1 : 0;\n            ry += foundation.height <= 2 ? 1 : 0;\n        }\n        return { rx, ry };\n    }\n    computeWarFactoryExitCoords(building: any): {\n        rx: number;\n        ry: number;\n    } {\n        const foundation = building.getFoundation();\n        return {\n            rx: building.tile.rx + Math.floor(foundation.width / 2),\n            ry: building.tile.ry + Math.floor(foundation.height / 2)\n        };\n    }\n    computeWarFactoryInternalRallyCoords(building: any): {\n        rx: number;\n        ry: number;\n    } {\n        const foundation = building.getFoundation();\n        return {\n            rx: building.tile.rx + foundation.width - 1,\n            ry: building.tile.ry + Math.floor(foundation.height / 2)\n        };\n    }\n    computeWarFactoryDefaultRallyCoords(building: any): {\n        rx: number;\n        ry: number;\n    } {\n        const foundation = building.getFoundation();\n        return {\n            rx: building.tile.rx + foundation.width,\n            ry: building.tile.ry + Math.floor(foundation.height / 2)\n        };\n    }\n    computeNavalDefaultRallyPoint(building: any, map: any): any {\n        const finder = new CardinalTileFinder(map.tiles, map.mapBounds, building.centerTile, 5, 5, (tile: any) => tile.terrainType === TerrainType.Water &&\n            !map.getObjectsOnTile(tile).find((obj: any) => obj.isBuilding() || (obj.isOverlay() && obj.isBridge())));\n        finder.diagonal = false;\n        return finder.getNextTile() ?? map.tiles.getByMapCoords(building.tile.rx + building.getFoundation().width, building.tile.ry + building.getFoundation().height);\n    }\n    computeNavalInternalRallyPoint(building: any, rallyPoint: any, map: any): any {\n        const direction = new Vector2(rallyPoint.rx, rallyPoint.ry).sub(new Vector2(building.centerTile.rx, building.centerTile.ry));\n        return map.tiles.getByMapCoords(building.centerTile.rx + Math.sign(direction.x) * (Math.floor(building.getFoundation().width / 2) + 1), building.centerTile.ry + Math.sign(direction.y) * (Math.floor(building.getFoundation().height / 2) + 1));\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/FreeUnitTrait.ts",
    "content": "import { NotifyBuildStatus } from './interface/NotifyBuildStatus';\nimport { Building, BuildStatus } from '@/game/gameobject/Building';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { RadialBackFirstTileFinder } from '@/game/map/tileFinder/RadialBackFirstTileFinder';\nexport class FreeUnitTrait {\n    [NotifyBuildStatus.onStatusChange](oldStatus: BuildStatus, building: Building, context: GameContext) {\n        if (building.buildStatus === BuildStatus.Ready &&\n            oldStatus === BuildStatus.BuildUp &&\n            !building.owner.isNeutral) {\n            let unitRules;\n            if (context.rules.hasObject(building.rules.freeUnit, ObjectType.Vehicle)) {\n                unitRules = context.rules.getObject(building.rules.freeUnit, ObjectType.Vehicle);\n            }\n            else {\n                if (!context.rules.hasObject(building.rules.freeUnit, ObjectType.Infantry)) {\n                    console.warn(`Free unit \"${building.rules.freeUnit}\" is not a vehicle or infantry type.`);\n                    return;\n                }\n                unitRules = context.rules.getObject(building.rules.freeUnit, ObjectType.Infantry);\n            }\n            const unit = context.createUnitForPlayer(unitRules, building.owner);\n            let fallbackTile: Tile | undefined;\n            const spawnTile = new RadialBackFirstTileFinder(context.map.tiles, context.map.mapBounds, building.tile, building.getFoundation(), 1, 1, (tile) => {\n                const isValidTile = context.map.terrain.getPassableSpeed(tile, unit.rules.speedType, unit.isInfantry(), false) > 0 &&\n                    Math.abs(tile.z - building.tile.z) < 2 &&\n                    !context.map.terrain.findObstacles({ tile, onBridge: undefined }, unit).length;\n                if (!fallbackTile && isValidTile) {\n                    fallbackTile = tile;\n                }\n                return isValidTile && !context.map.getObjectsOnTile(tile).find(obj => obj.isOverlay());\n            }).getNextTile() ?? fallbackTile;\n            if (!spawnTile) {\n                building.owner.removeOwnedObject(unit);\n                unit.dispose();\n                building.owner.credits += unit.purchaseValue;\n                console.warn(`[FreeUnitTrait] failed to find spawn tile for \"${unit.name}\" from \"${building.name}\"#${building.id}; refunded ${unit.purchaseValue}`);\n                return;\n            }\n            console.log(`[FreeUnitTrait] spawning \"${unit.name}\" for \"${building.name}\"#${building.id} at (${spawnTile.rx}, ${spawnTile.ry}, ${spawnTile.z})`);\n            context.spawnObject(unit, spawnTile);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/GapGeneratorTrait.ts",
    "content": "import { GameSpeed } from '@/game/GameSpeed';\nimport { MapShroud, ShroudFlag } from '@/game/map/MapShroud';\nimport { Box2 } from '@/game/math/Box2';\nimport { Vector2 } from '@/game/math/Vector2';\nimport { TechnoRules } from '@/game/rules/TechnoRules';\nimport { RangeHelper } from '@/game/gameobject/unit/RangeHelper';\nimport { NotifyOwnerChange } from './interface/NotifyOwnerChange';\nimport { NotifySpawn } from './interface/NotifySpawn';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { NotifyUnspawn } from './interface/NotifyUnspawn';\nimport { NotifyWarpChange } from './interface/NotifyWarpChange';\nexport class GapGeneratorTrait {\n    private radiusTiles: number;\n    private refreshTicks: number;\n    constructor(radiusTiles: number) {\n        this.radiusTiles = radiusTiles;\n        this.refreshTicks = 0;\n    }\n    [NotifyTick.onTick](building: Building, context: GameContext): void {\n        if (this.refreshTicks > 0) {\n            this.refreshTicks--;\n        }\n        if (this.refreshTicks <= 0) {\n            this.update(building, context);\n        }\n    }\n    [NotifySpawn.onSpawn](building: Building, context: GameContext): void {\n        this.markGapTilesForFriendlies(building, building.owner, context, true);\n    }\n    [NotifyUnspawn.onUnspawn](building: Building, context: GameContext): void {\n        this.markGapTilesForFriendlies(building, building.owner, context, false);\n        this.update(building, context);\n    }\n    [NotifyOwnerChange.onChange](building: Building, oldOwner: Player, context: GameContext): void {\n        this.markGapTilesForFriendlies(building, oldOwner, context, false);\n        this.markGapTilesForFriendlies(building, building.owner, context, true);\n        this.update(building, context);\n    }\n    [NotifyWarpChange.onChange](building: Building, context: GameContext, isWarpedIn: boolean): void {\n        this.markGapTilesForFriendlies(building, building.owner, context, !isWarpedIn);\n        if (isWarpedIn) {\n            this.update(building, context);\n        }\n    }\n    private markGapTilesForFriendlies(building: Building, player: Player, context: GameContext, isActive: boolean): void {\n        const players = [player, ...context.alliances.getAllies(player)];\n        let nearbyGapGenerators: Building[] | undefined;\n        for (const player of players) {\n            const shroud = context.mapShroudTrait.getPlayerShroud(player);\n            if (shroud) {\n                shroud.toggleFlagsAround(building.tile, this.radiusTiles, ShroudFlag.Darken, isActive);\n                if (!isActive) {\n                    if (!nearbyGapGenerators) {\n                        const rangeHelper = new RangeHelper(context.map.tileOccupation);\n                        nearbyGapGenerators = players\n                            .map(p => [...p.buildings])\n                            .flat()\n                            .filter(b => b.gapGeneratorTrait &&\n                            b !== building &&\n                            rangeHelper.tileDistance(b, building) <= b.gapGeneratorTrait.radiusTiles + this.radiusTiles);\n                    }\n                    for (const gapGenerator of nearbyGapGenerators) {\n                        shroud.toggleFlagsAround(gapGenerator.tile, gapGenerator.gapGeneratorTrait.radiusTiles, ShroudFlag.Darken, true);\n                    }\n                }\n            }\n        }\n    }\n    private update(building: Building, context: GameContext): void {\n        this.refreshTicks = 5 * GameSpeed.BASE_TICKS_PER_SECOND;\n        let technosInRange: GameObject[] | undefined;\n        const isActive = building.owner.buildings.has(building) && building.poweredTrait?.isPoweredOn();\n        for (const combatant of context.getCombatants()) {\n            if (combatant !== building.owner && !context.alliances.areAllied(building.owner, combatant)) {\n                const shroud = context.mapShroudTrait.getPlayerShroud(combatant);\n                if (shroud) {\n                    if (isActive) {\n                        shroud.unrevealAround(building.tile, this.radiusTiles);\n                        if (!technosInRange) {\n                            const range = this.radiusTiles + TechnoRules.MAX_SIGHT;\n                            const minPos = new Vector2(building.tile.rx, building.tile.ry).addScalar(-range);\n                            const maxPos = new Vector2(building.tile.rx, building.tile.ry).addScalar(range);\n                            technosInRange = context.map.technosByTile.queryRange(new Box2(minPos, maxPos));\n                        }\n                        for (const techno of technosInRange) {\n                            if (techno.owner === combatant || context.alliances.areAllied(techno.owner, combatant)) {\n                                shroud.revealFrom(techno);\n                            }\n                            else if (techno.rules.revealToAll) {\n                                shroud.revealObject(techno);\n                            }\n                        }\n                    }\n                    else if ([...combatant.buildings].some(b => b.rules.spySat)) {\n                        shroud.revealAround(building.tile, this.radiusTiles);\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/GarrisonTrait.ts",
    "content": "import { NotifyDestroy } from './interface/NotifyDestroy';\nimport { RadialTileFinder } from '@/game/map/tileFinder/RadialTileFinder';\nimport { NotifyDamage } from './interface/NotifyDamage';\nimport { fnv32a } from '@/util/math';\nimport { BuildingEvacuateEvent } from '@/game/event/BuildingEvacuateEvent';\nimport { ScatterTask } from '@/game/gameobject/task/ScatterTask';\nexport class GarrisonTrait {\n    private building: Building;\n    private evacThreshold: number;\n    private maxOccupants: number;\n    private units: Unit[] = [];\n    constructor(building: Building, evacThreshold: number, maxOccupants: number) {\n        this.building = building;\n        this.evacThreshold = evacThreshold;\n        this.maxOccupants = maxOccupants;\n    }\n    isOccupied(): boolean {\n        return this.units.length > 0;\n    }\n    canBeOccupied(): boolean {\n        return this.building.healthTrait.health > 100 * this.evacThreshold;\n    }\n    [NotifyDamage.onDamage](building: Building, context: GameContext): void {\n        if (building.healthTrait.health <= 100 * this.evacThreshold) {\n            this.evacuate(context);\n        }\n    }\n    [NotifyDestroy.onDestroy](building: Building, context: GameContext, reason: any, isImmediate: boolean): void {\n        if (isImmediate) {\n            for (const unit of this.units) {\n                context.destroyObject(unit, reason, true);\n            }\n            this.units = [];\n        }\n        else {\n            this.evacuate(context);\n        }\n    }\n    getHash(): number {\n        return fnv32a(this.units.map(unit => unit.getHash()));\n    }\n    debugGetState(): {\n        units: any[];\n    } {\n        return { units: this.units.map(unit => unit.debugGetState()) };\n    }\n    dispose(): void {\n        this.building = undefined;\n    }\n    evacuate(context: GameContext, forceDestroy: boolean = false): void {\n        const building = this.building;\n        const units = this.units;\n        if (units.length) {\n            const speedTypeMap = new Map<string, Unit[]>();\n            for (const unit of units) {\n                speedTypeMap.set(unit.rules.speedType, (speedTypeMap.get(unit.rules.speedType) || []).concat(unit));\n            }\n            for (const [speedType, typeUnits] of speedTypeMap) {\n                const finder = new RadialTileFinder(context.map.tiles, context.map.mapBounds, building.tile, building.art.foundation, 1, 1, (tile) => {\n                    return context.map.terrain.getPassableSpeed(tile, speedType, true, false) > 0 &&\n                        Math.abs(tile.z - building.tile.z) < 2 &&\n                        !context.map.terrain.findObstacles({ tile, onBridge: undefined }, typeUnits[0]).length;\n                });\n                const exitTile = finder.getNextTile();\n                for (const unit of typeUnits) {\n                    const unitIndex = units.indexOf(unit);\n                    if (exitTile) {\n                        units.splice(unitIndex, 1);\n                        context.unlimboObject(unit, exitTile);\n                        unit.unitOrderTrait.addTask(new ScatterTask(context));\n                    }\n                    else if (!forceDestroy) {\n                        context.destroyObject(unit, { player: unit.owner });\n                        units.splice(unitIndex, 1);\n                    }\n                }\n            }\n            const oldOwner = building.owner;\n            if (!units.length && !building.isDestroyed) {\n                context.changeObjectOwner(building, context.getCivilianPlayer());\n            }\n            context.events.dispatch(new BuildingEvacuateEvent(building, oldOwner));\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/GunnerTrait.ts",
    "content": "import { VeteranLevel } from '@/game/gameobject/unit/VeteranLevel';\nimport { NotifyTick } from './interface/NotifyTick';\nexport class GunnerTrait {\n    private lastHadGunner: boolean = false;\n    [NotifyTick.onTick](unit: Unit): void {\n        const hasGunner = !!unit.transportTrait.units.length;\n        if (hasGunner !== this.lastHadGunner) {\n            this.lastHadGunner = hasGunner;\n            const ifvMode = unit.transportTrait.units[0]?.rules.ifvMode ?? 0;\n            const turretIndex = unit.rules.turretIndexesByIfvMode.get(ifvMode) ?? 0;\n            if (turretIndex < unit.rules.turretCount) {\n                unit.turretNo = turretIndex;\n                unit.armedTrait?.selectSpecialWeapon(ifvMode, unit.veteranLevel === VeteranLevel.Elite);\n            }\n        }\n    }\n    getUiNameForIfvMode(mode: number, name?: string): string | undefined {\n        switch (mode) {\n            case 0:\n                return \"tip:rocket\";\n            case 1:\n                return \"tip:repair\";\n            case 2:\n            case 4:\n            case 5:\n                return \"tip:machinegun\";\n            default:\n                return name ? `name:${name.toLowerCase()}` : undefined;\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/HarvesterTrait.ts",
    "content": "import { NotifyTick } from './interface/NotifyTick';\nimport { ReturnOreTask } from '../task/harvester/ReturnOreTask';\nimport { GatherOreTask } from '../task/harvester/GatherOreTask';\nimport { NotifySpawn } from './interface/NotifySpawn';\nimport { NotifyOwnerChange } from './interface/NotifyOwnerChange';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { NotifyTeleport } from './interface/NotifyTeleport';\nimport { NotifyOrder } from './interface/NotifyOrder';\nimport { OrderType } from '@/game/order/OrderType';\nimport { LandType } from '@/game/type/LandType';\nexport enum HarvesterStatus {\n    Idle = 0,\n    LookingForOreSite = 1,\n    MovingToOreSite = 2,\n    Harvesting = 3,\n    LookingForRefinery = 4,\n    MovingToRefinery = 5,\n    Docking = 6,\n    PreparingToUnload = 7,\n    Unloading = 8\n}\nexport class HarvesterTrait {\n    private storage: number;\n    private ore: number;\n    private gems: number;\n    private status: HarvesterStatus;\n    private lastGatherExplicit: boolean;\n    private autoGatherOnNextIdle: boolean;\n    private ticksSinceLastRefineryCheck: number;\n    private ticksSinceLastOreCheck: number;\n    private lastOreSite?: any;\n    constructor(storage: number) {\n        this.storage = storage;\n        this.ore = 0;\n        this.gems = 0;\n        this.status = HarvesterStatus.Idle;\n        this.lastGatherExplicit = false;\n        this.autoGatherOnNextIdle = false;\n        this.ticksSinceLastRefineryCheck = 0;\n        this.ticksSinceLastOreCheck = 0;\n    }\n    [NotifySpawn.onSpawn](unit: any, world: any): void {\n        if (unit.owner.isCombatant()) {\n            world.afterTick(() => {\n                unit.unitOrderTrait.addTask(new GatherOreTask(world, undefined));\n            });\n            unit.attackTrait?.increasePassiveScanCooldown(1);\n        }\n    }\n    [NotifyOwnerChange.onChange](unit: any, oldOwner: any, world: any): void {\n        if ((!oldOwner.isCombatant() && unit.owner.isCombatant()) ||\n            world.alliances.areAllied(unit.owner, oldOwner)) {\n            world.afterTick(() => {\n                unit.unitOrderTrait.addTask(new GatherOreTask(world, undefined));\n            });\n        }\n    }\n    [NotifyTick.onTick](unit: any, world: any): void {\n        if (this.status === HarvesterStatus.LookingForRefinery) {\n            if (this.ticksSinceLastRefineryCheck++ > 5 * GameSpeed.BASE_TICKS_PER_SECOND) {\n                this.ticksSinceLastRefineryCheck = 0;\n                if (unit.unitOrderTrait.hasTasks()) {\n                    this.ticksSinceLastRefineryCheck = -25 * GameSpeed.BASE_TICKS_PER_SECOND;\n                }\n                else if ([...unit.owner.buildings].some(b => b.rules.refinery) || this.lastGatherExplicit) {\n                    unit.unitOrderTrait.addTask(new ReturnOreTask(world, undefined));\n                }\n                else {\n                    this.status = HarvesterStatus.Idle;\n                }\n            }\n        }\n        else if (this.status === HarvesterStatus.LookingForOreSite) {\n            if (this.ticksSinceLastOreCheck++ > 20 * GameSpeed.BASE_TICKS_PER_SECOND) {\n                this.ticksSinceLastOreCheck = 0;\n                if (!unit.unitOrderTrait.hasTasks()) {\n                    unit.unitOrderTrait.addTask(new GatherOreTask(world, undefined));\n                }\n            }\n        }\n        else if (this.status === HarvesterStatus.Idle &&\n            this.autoGatherOnNextIdle &&\n            unit.unitOrderTrait.isIdle() &&\n            unit.tile.landType === LandType.Tiberium) {\n            this.autoGatherOnNextIdle = false;\n            unit.unitOrderTrait.addTask(new GatherOreTask(world, unit.tile, true));\n        }\n    }\n    [NotifyTeleport.onBeforeTeleport](unit: any, world: any, tile: any, keepDock: boolean): void {\n        if (!keepDock && unit.owner.isCombatant()) {\n            this.status = HarvesterStatus.Idle;\n            this.lastOreSite = undefined;\n            if (tile && unit.rules.teleporter) {\n                world.afterTick(() => {\n                    unit.unitOrderTrait.addTask(new (this.isFull() ? ReturnOreTask : GatherOreTask)(world, undefined));\n                });\n            }\n        }\n    }\n    [NotifyOrder.onPush](unit: any, orderType: OrderType): void {\n        this.autoGatherOnNextIdle = [\n            OrderType.AttackMove,\n            OrderType.Move,\n            OrderType.ForceMove,\n            OrderType.Scatter\n        ].includes(orderType);\n        if ([HarvesterStatus.LookingForRefinery, HarvesterStatus.LookingForOreSite].includes(this.status)) {\n            this.status = HarvesterStatus.Idle;\n        }\n    }\n    isFull(): boolean {\n        return this.ore + this.gems >= this.storage;\n    }\n    isEmpty(): boolean {\n        return !this.ore && !this.gems;\n    }\n    getHash(): number {\n        return 100 * this.ore + this.gems;\n    }\n    debugGetState(): {\n        ore: number;\n        gems: number;\n    } {\n        return { ore: this.ore, gems: this.gems };\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/HealthTrait.ts",
    "content": "import { clamp } from '@/util/math';\nimport { NotifyDamage } from './interface/NotifyDamage';\nimport { NotifyHealthChange } from './interface/NotifyHealthChange';\nimport { InflictDamageEvent } from '@/game/event/InflictDamageEvent';\nimport { NotifyHeal } from './interface/NotifyHeal';\nimport { HealthLevel } from '@/game/gameobject/unit/HealthLevel';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { HealthChangeEvent } from '@/game/event/HealthChangeEvent';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class HealthTrait {\n    private maxHitPoints: number;\n    private hitPoints: number;\n    private _computedHealth: number;\n    private projectedHitPoints: number;\n    private gameObject: GameObject;\n    private conditionYellow: number;\n    private conditionRed: number;\n    get health(): number {\n        return this._computedHealth;\n    }\n    set health(value: number) {\n        this.setHitPoints(value > 0 ? Math.max(1, Math.floor((value * this.maxHitPoints) / 100)) : 0);\n        this.projectedHitPoints = this.hitPoints;\n    }\n    get level(): HealthLevel {\n        return this.health > 100 * this.conditionYellow\n            ? HealthLevel.Green\n            : this.health > 100 * this.conditionRed\n                ? HealthLevel.Yellow\n                : HealthLevel.Red;\n    }\n    constructor(maxHitPoints: number, gameObject: GameObject, conditionYellow: number, conditionRed: number) {\n        this.maxHitPoints = maxHitPoints;\n        this.gameObject = gameObject;\n        this.conditionYellow = conditionYellow;\n        this.conditionRed = conditionRed;\n        this.setHitPoints(maxHitPoints);\n        this.projectedHitPoints = this.hitPoints;\n    }\n    setHitPoints(value: number): void {\n        if (value !== Math.floor(value)) {\n            throw new Error(`Value ${value} is not an integer`);\n        }\n        this.hitPoints = clamp(value, 0, this.maxHitPoints);\n        this._computedHealth = (this.hitPoints / this.maxHitPoints) * 100;\n    }\n    getHitPoints(): number {\n        return this.hitPoints;\n    }\n    getProjectedHitPoints(): number {\n        return this.projectedHitPoints;\n    }\n    inflictDamage(amount: number, source: GameObject, world: World): void {\n        const oldHitPoints = this.hitPoints;\n        const oldHealth = this.health;\n        this.applyHitPoints(oldHitPoints - amount, world);\n        if (oldHitPoints !== this.hitPoints && amount > 0) {\n            this.gameObject.traits\n                .filter(NotifyDamage)\n                .forEach((trait) => {\n                trait[NotifyDamage.onDamage](this.gameObject, world, amount, source);\n            });\n            world.events.dispatch(new InflictDamageEvent(this.gameObject, source, amount, this.health, oldHealth));\n        }\n    }\n    healBy(amount: number, source: GameObject, world: World): void {\n        if (amount < 0) {\n            throw new Error(\"Can't heal by negative value \" + amount);\n        }\n        if (this.hitPoints < this.maxHitPoints) {\n            const oldHitPoints = this.hitPoints;\n            this.applyHitPoints(this.hitPoints + amount, world);\n            this.projectedHitPoints = this.hitPoints;\n            const healedAmount = this.hitPoints - oldHitPoints;\n            this.gameObject.traits\n                .filter(NotifyHeal)\n                .forEach((trait) => {\n                trait[NotifyHeal.onHeal]?.(this.gameObject, world, healedAmount, source);\n            });\n        }\n    }\n    healToFull(source: GameObject, world: World): void {\n        if (this.hitPoints < this.maxHitPoints) {\n            const oldHitPoints = this.hitPoints;\n            this.applyHitPoints(this.maxHitPoints, world);\n            this.projectedHitPoints = this.hitPoints;\n            const healedAmount = this.hitPoints - oldHitPoints;\n            this.gameObject.traits\n                .filter(NotifyHeal)\n                .forEach((trait) => {\n                trait[NotifyHeal.onHeal]?.(this.gameObject, world, healedAmount, source);\n            });\n        }\n    }\n    applyHitPoints(value: number, world: World): void {\n        const oldHealth = this.health;\n        this.setHitPoints(value);\n        if (oldHealth !== this.health) {\n            world.traits\n                .filter(NotifyHealthChange)\n                .forEach((trait) => {\n                trait[NotifyHealthChange.onChange](this.gameObject, world, oldHealth);\n            });\n            this.gameObject.traits\n                .filter(NotifyHealthChange)\n                .forEach((trait) => {\n                trait[NotifyHealthChange.onChange](this.gameObject, world, oldHealth);\n            });\n            world.events.dispatch(new HealthChangeEvent(this.gameObject, this.health, oldHealth));\n        }\n    }\n    projectDamage(amount: number): void {\n        if (amount < 0) {\n            throw new Error(\"Projected damage must be positive\");\n        }\n        this.projectedHitPoints = Math.max(-30, this.projectedHitPoints - amount);\n    }\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        if (world.currentTick % 4 === 0) {\n            this.projectedHitPoints = Math.min(this.projectedHitPoints + 1, this.hitPoints);\n        }\n    }\n    getHash(): number {\n        return this.hitPoints;\n    }\n    debugGetState(): {\n        hitPoints: number;\n    } {\n        return { hitPoints: this.hitPoints };\n    }\n    dispose(): void {\n        this.gameObject = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/HelipadTrait.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { NotifyOwnerChange } from './interface/NotifyOwnerChange';\nimport { NotifyUnspawn } from './interface/NotifyUnspawn';\nexport class HelipadTrait {\n    [NotifyOwnerChange.onChange](unit: any, oldOwner: any, world: any): void {\n        this.checkAircraftsForPlayer(oldOwner, world);\n    }\n    [NotifyUnspawn.onUnspawn](unit: any, world: any): void {\n        this.checkAircraftsForPlayer(unit.owner, world);\n    }\n    private checkAircraftsForPlayer(player: any, world: any): void {\n        const padAircraft = world.rules.general.padAircraft;\n        for (const aircraft of player\n            .getOwnedObjectsByType(ObjectType.Aircraft)\n            .filter((aircraft: any) => padAircraft.includes(aircraft.name))) {\n            if (aircraft.airportBoundTrait) {\n                aircraft.airportBoundTrait.preferredAirport = undefined;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/HospitalTrait.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { UnitRepairFinishEvent } from '@/game/event/UnitRepairFinishEvent';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { RadialTileFinder } from '@/game/map/tileFinder/RadialTileFinder';\nimport { ScatterTask } from '@/game/gameobject/task/ScatterTask';\nimport { NotifyDestroy } from '@/game/gameobject/trait/interface/NotifyDestroy';\nimport { NotifyTick } from '@/game/gameobject/trait/interface/NotifyTick';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class HospitalTrait {\n    private healQueue: GameObject[] = [];\n    private unit?: GameObject;\n    private healTicks?: number;\n    addToHealQueue(unit: GameObject): number {\n        this.healQueue.push(unit);\n        return this.healQueue.length - 1;\n    }\n    unitIsFirstInHealQueue(unit: GameObject): boolean {\n        return this.healQueue[0] === unit;\n    }\n    removeFromHealQueue(unit: GameObject): void {\n        const index = this.healQueue.indexOf(unit);\n        if (index !== -1) {\n            this.healQueue.splice(index, 1);\n        }\n    }\n    startHealing(unit: GameObject): void {\n        if (this.unit) {\n            throw new Error(`Already busy healing unit ${ObjectType[this.unit.type]}#${this.unit.id}`);\n        }\n        this.unit = unit;\n        this.healTicks = 5 * GameSpeed.BASE_TICKS_PER_SECOND;\n    }\n    [NotifyTick.onTick](hospital: GameObject, world: World): void {\n        this.healQueue = this.healQueue.filter((unit) => !unit.isDestroyed && !unit.isCrashing);\n        if (this.unit && this.healTicks !== undefined) {\n            if (this.healTicks > 0) {\n                this.healTicks--;\n            }\n            if (this.healTicks <= 0) {\n                this.healTicks = undefined;\n                this.removeFromHealQueue(this.unit);\n                this.unit.healthTrait.healToFull(hospital, world);\n                if (hospital.ammoTrait) {\n                    hospital.ammoTrait.ammo--;\n                }\n                this.evacuate(this.unit, hospital, world);\n                const healedUnit = this.unit;\n                this.unit = undefined;\n                world.events.dispatch(new UnitRepairFinishEvent(healedUnit, hospital));\n            }\n        }\n    }\n    [NotifyDestroy.onDestroy](hospital: GameObject, world: World, source: any): void {\n        if (this.unit) {\n            world.destroyObject(this.unit, source, true);\n            this.unit = undefined;\n        }\n    }\n    private evacuate(unit: GameObject, hospital: GameObject, world: World): void {\n        let targetTile;\n        const exitPoint = {\n            x: hospital.tile.rx,\n            y: hospital.tile.ry + hospital.art.foundation.height\n        };\n        let tile = world.map.tiles.getByMapCoords(exitPoint.x, exitPoint.y);\n        if (tile &&\n            world.map.isWithinBounds(tile) &&\n            this.canEvacuateTo(tile, unit, hospital, world)) {\n            targetTile = tile;\n        }\n        if (!targetTile) {\n            targetTile = new RadialTileFinder(world.map.tiles, world.map.mapBounds, hospital.tile, hospital.art.foundation, 1, 1, (tile) => this.canEvacuateTo(tile, unit, hospital, world)).getNextTile();\n        }\n        if (targetTile) {\n            world.unlimboObject(unit, targetTile);\n            unit.unitOrderTrait.addTask(new ScatterTask(world));\n        }\n        else {\n            world.destroyObject(unit, { player: unit.owner });\n        }\n    }\n    private canEvacuateTo(tile: any, unit: GameObject, hospital: GameObject, world: World): boolean {\n        return (world.map.terrain.getPassableSpeed(tile, unit.rules.speedType, unit.isInfantry(), false) > 0 &&\n            Math.abs(tile.z - hospital.tile.z) < 2 &&\n            !world.map.terrain.findObstacles({ tile, onBridge: undefined }, unit).length);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/HoverBobTrait.ts",
    "content": "import { GameSpeed } from '@/game/GameSpeed';\nimport { NotifyTick } from '@/game/gameobject/trait/interface/NotifyTick';\nimport { NotifySpawn } from '@/game/gameobject/trait/interface/NotifySpawn';\nimport { Coords } from '@/game/Coords';\nimport { NotifyTileChange } from '@/game/gameobject/trait/interface/NotifyTileChange';\nimport { GameMath } from '@/game/math/GameMath';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class HoverBobTrait {\n    private prevHoverBobLeptons: number = 0;\n    private spawnTick: number = 0;\n    [NotifySpawn.onSpawn](gameObject: GameObject, world: World): void {\n        this.setBaseElevation(gameObject, world);\n        this.spawnTick = world.currentTick;\n    }\n    [NotifyTileChange.onTileChange](gameObject: GameObject, world: World, oldTile: any, isTeleport: boolean): void {\n        if (isTeleport) {\n            this.prevHoverBobLeptons = 0;\n            this.setBaseElevation(gameObject, world);\n        }\n    }\n    private setBaseElevation(gameObject: GameObject, world: World): void {\n        gameObject.position.tileElevation =\n            (gameObject.onBridge\n                ? world.map.tileOccupation.getBridgeOnTile(gameObject.tile)?.tileElevation ?? 0\n                : 0) +\n                Coords.worldToTileHeight(world.rules.general.hover.height);\n    }\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        const hoverBobLeptons = this.computeHoverBobLeptons(world.currentTick, world.rules.general.hover);\n        const deltaLeptons = hoverBobLeptons - this.prevHoverBobLeptons;\n        this.prevHoverBobLeptons = hoverBobLeptons;\n        const worldHeight = Coords.tileHeightToWorld(gameObject.position.tileElevation);\n        gameObject.position.tileElevation = Coords.worldToTileHeight(worldHeight + deltaLeptons);\n    }\n    private computeHoverBobLeptons(currentTick: number, hoverRules: any): number {\n        const timeInSeconds = (currentTick - this.spawnTick) /\n            GameSpeed.BASE_TICKS_PER_SECOND /\n            (60 * hoverRules.bob);\n        return 0.1 * hoverRules.height * GameMath.sin(2 * timeInSeconds * Math.PI);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/IdleActionTrait.ts",
    "content": "import { NotifyTick } from './interface/NotifyTick';\nimport { ScatterTask } from '../task/ScatterTask';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class IdleActionTrait {\n    private cooldownTicks: number = Number.POSITIVE_INFINITY;\n    private _actionDueThisTick: boolean = false;\n    private idle: boolean = false;\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        this._actionDueThisTick = false;\n        const isIdle = !gameObject.unitOrderTrait.hasTasks();\n        if (isIdle && !this.idle) {\n            this.resetCooldown(world);\n        }\n        else if (isIdle) {\n            if (this.cooldownTicks === 0) {\n                this.doIdleAction(gameObject, world);\n                this.resetCooldown(world);\n            }\n            else {\n                this.cooldownTicks--;\n            }\n        }\n        else {\n            this.cooldownTicks = Number.POSITIVE_INFINITY;\n        }\n        this.idle = isIdle;\n    }\n    doIdleAction(gameObject: GameObject, world: World): void {\n        if (gameObject.isInfantry()) {\n            if (gameObject.rules.fraidycat) {\n                if (world.generateRandom() > 0.5) {\n                    gameObject.unitOrderTrait.addTask(new ScatterTask(world, undefined, { noSlopes: true }));\n                    return;\n                }\n            }\n            this._actionDueThisTick = true;\n        }\n    }\n    actionDueThisTick(): boolean {\n        return this._actionDueThisTick;\n    }\n    private resetCooldown(world: World): void {\n        const baseFrequency = world.rules.audioVisual.idleActionFrequency;\n        const randomOffset = world.generateRandom() * baseFrequency * 0.5;\n        const frequency = Math.max(0, baseFrequency - randomOffset);\n        this.cooldownTicks = Math.floor(frequency * GameSpeed.BASE_TICKS_PER_SECOND);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/InvulnerableTrait.ts",
    "content": "import { Timer } from '@/game/gameobject/unit/Timer';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class InvulnerableTrait {\n    private timer: Timer;\n    constructor() {\n        this.timer = new Timer();\n    }\n    isActive(): boolean {\n        return this.timer.isActive();\n    }\n    setActiveFor(duration: number, world: World): void {\n        this.timer.setActiveFor(duration, world as any);\n    }\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        this.timer.tick(world.currentTick);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/MindControllableTrait.ts",
    "content": "import { NotifyUnspawn } from './interface/NotifyUnspawn';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class MindControllableTrait {\n    private gameObject: GameObject;\n    private controller?: GameObject;\n    private prevOwner?: any;\n    constructor(gameObject: GameObject) {\n        this.gameObject = gameObject;\n    }\n    getOriginalOwner(): any {\n        return this.prevOwner;\n    }\n    isActive(): boolean {\n        return !!this.controller;\n    }\n    getController(): GameObject | undefined {\n        return this.controller;\n    }\n    controlBy(controller: GameObject, world: World): void {\n        if (this.controller) {\n            throw new Error(`Object \"${this.gameObject?.name}\" is already mind controlled by \"${controller.name}\"`);\n        }\n        this.controller = controller;\n        this.prevOwner = this.gameObject.owner;\n        world.changeObjectOwner(this.gameObject, controller.owner);\n    }\n    restore(world: World): void {\n        if (this.prevOwner) {\n            world.changeObjectOwner(this.gameObject, this.prevOwner);\n            this.prevOwner = undefined;\n            this.controller = undefined;\n        }\n    }\n    [NotifyUnspawn.onUnspawn](gameObject: GameObject, world: World): void {\n        if (this.controller) {\n            this.controller.mindControllerTrait.cleanTarget(gameObject);\n            if (!gameObject.isDestroyed && gameObject.limboData) {\n                this.restore(world);\n            }\n        }\n    }\n    dispose(): void {\n        this.gameObject = undefined as any;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/MindControllerTrait.ts",
    "content": "import { NotifyUnspawn } from './interface/NotifyUnspawn';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class MindControllerTrait {\n    private gameObject: GameObject;\n    private maxCapacity: number;\n    private targets: GameObject[];\n    constructor(gameObject: GameObject, maxCapacity: number = 1) {\n        this.gameObject = gameObject;\n        this.maxCapacity = maxCapacity;\n        this.targets = [];\n    }\n    isActive(): boolean {\n        return this.targets.length > 0;\n    }\n    isAtCapacity(): boolean {\n        return this.targets.length === this.maxCapacity;\n    }\n    getTargets(): GameObject[] {\n        return this.targets;\n    }\n    control(target: GameObject, world: World): void {\n        if (!this.gameObject) {\n            throw new Error(\"Trait already disposed\");\n        }\n        if (!target.mindControllableTrait) {\n            throw new Error(`Target \"${target.name}\" cannot be mind controlled`);\n        }\n        if (target.isDisposed) {\n            throw new Error(`Target \"${target.name}\" is disposed`);\n        }\n        target.mindControllableTrait.controlBy(this.gameObject, world);\n        this.targets.push(target);\n        while (this.targets.length > this.maxCapacity) {\n            const oldestTarget = this.targets.shift();\n            oldestTarget.mindControllableTrait.restore(world);\n        }\n    }\n    cleanTarget(target: GameObject): void {\n        const index = this.targets.indexOf(target);\n        if (index !== -1) {\n            this.targets.splice(index, 1);\n        }\n    }\n    [NotifyUnspawn.onUnspawn](gameObject: GameObject, world: World): void {\n        for (const target of this.targets) {\n            target.mindControllableTrait.restore(world);\n        }\n        this.targets.length = 0;\n    }\n    dispose(): void {\n        this.gameObject = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/MissileSpawnTrait.ts",
    "content": "import { CollisionType } from '@/game/gameobject/unit/CollisionType';\nimport { NotifyDestroy } from './interface/NotifyDestroy';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class MissileSpawnTrait {\n    private warhead?: any;\n    private damage?: number;\n    private launcher?: GameObject;\n    setWarhead(warhead: any): this {\n        this.warhead = warhead;\n        return this;\n    }\n    setDamage(damage: number): this {\n        this.damage = damage;\n        return this;\n    }\n    setLauncher(launcher: GameObject): this {\n        this.launcher = launcher;\n        return this;\n    }\n    [NotifyDestroy.onDestroy](gameObject: GameObject, world: World): void {\n        if (this.warhead && this.damage && this.launcher) {\n            this.warhead.detonate(world, this.damage, gameObject.tile, gameObject.tileElevation, gameObject.position.worldPosition, gameObject.zone, CollisionType.None, world.createTarget(undefined, gameObject.tile), { player: gameObject.owner, obj: this.launcher, weapon: undefined } as any, false, undefined, undefined);\n        }\n    }\n    dispose(): void {\n        this.launcher = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/MoveTrait.ts",
    "content": "import { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { NotifyDestroy } from \"@/game/gameobject/trait/interface/NotifyDestroy\";\nimport { ZoneType, getZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { InfDeathType } from \"@/game/gameobject/infantry/InfDeathType\";\nimport { ObjectTeleportEvent } from \"@/game/event/ObjectTeleportEvent\";\nimport { DeathType } from \"@/game/gameobject/common/DeathType\";\nimport { NotifyTeleport } from \"@/game/gameobject/trait/interface/NotifyTeleport\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\nimport { JumpjetLocomotor } from \"@/game/gameobject/locomotor/JumpjetLocomotor\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { WingedLocomotor } from \"@/game/gameobject/locomotor/WingedLocomotor\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { NotifyTileChange as GlobalNotifyTileChange } from \"@/game/trait/interface/NotifyTileChange\";\nimport { NotifyTileChange } from \"@/game/gameobject/trait/interface/NotifyTileChange\";\nimport { EnterTileEvent } from \"@/game/event/EnterTileEvent\";\nimport { Vector3 } from \"@/game/math/Vector3\";\nimport { NotifyElevationChange } from \"@/game/trait/interface/NotifyElevationChange\";\ninterface GameObject {\n    rules: any;\n    veteranTrait?: any;\n    crateBonuses: any;\n    healthTrait: any;\n    position: any;\n    tile: any;\n    tileElevation: number;\n    direction: number;\n    zone: ZoneType;\n    onBridge: boolean;\n    owner: any;\n    crusher: boolean;\n    spinVelocity: number;\n    traits: any[];\n    turretTrait?: any;\n    attackTrait?: any;\n    unitOrderTrait: any;\n    moveTrait: MoveTrait;\n    stance?: StanceType;\n    infDeathType?: InfDeathType;\n    deathType?: DeathType;\n    isDestroyed: boolean;\n    isVehicle(): boolean;\n    isAircraft(): boolean;\n    isUnit(): boolean;\n    isInfantry(): boolean;\n    isTechno(): boolean;\n    isOverlay(): boolean;\n    applyRocking(x: number, y: number): void;\n}\ninterface TileOccupation {\n    unoccupyTileRange(tile: any, obj: GameObject): void;\n    occupyTileRange(tile: any, obj: GameObject): void;\n    getBridgeOnTile(tile: any): any;\n    getGroundObjectsOnTile(tile: any): GameObject[];\n    unoccupySingleTile(tile: any, obj: GameObject): void;\n}\ninterface GameState {\n    currentTick: number;\n    map: any;\n    rules: any;\n    events: any;\n    traits: any[];\n    crateGeneratorTrait: any;\n    areFriendly(a: GameObject, b: GameObject): boolean;\n    destroyObject(obj: GameObject, source: any): void;\n}\ninterface Task {\n    children: Task[];\n}\ninterface PathNode {\n    tile: any;\n}\ninterface Waypoint {\n}\nexport enum MoveState {\n    Idle = 0,\n    ReachedNextWaypoint = 1,\n    PlanMove = 2,\n    Moving = 3\n}\nexport enum MoveResult {\n    Success = 0,\n    Cancel = 1,\n    CloseEnough = 2,\n    Fail = 3\n}\nexport enum CollisionState {\n    Waiting = 0,\n    Resolved = 1\n}\nconst isMoveTask = (task: Task): boolean => {\n    return task instanceof MoveTask || (task.children[0] && isMoveTask(task.children[0]));\n};\nexport class MoveTrait {\n    private gameObject: GameObject;\n    private tileOccupation: TileOccupation;\n    private disabled: boolean = false;\n    private speedPenalty: number = 0;\n    private velocity: Vector3 = new Vector3();\n    private reservedPathNodes: PathNode[] = [];\n    private moveState: MoveState = MoveState.Idle;\n    private collisionState: CollisionState = CollisionState.Resolved;\n    private locomotor?: any;\n    private currentWaypoint?: Waypoint;\n    private lastTargetOffset?: any;\n    private lastVelocity?: Vector3;\n    private lastMoveResult?: MoveResult;\n    private lastTeleportTick?: number;\n    get baseSpeed(): number {\n        return (this.gameObject.rules.speed *\n            (this.gameObject.veteranTrait?.getVeteranSpeedMultiplier() ?? 1) *\n            this.gameObject.crateBonuses.speed *\n            (this.gameObject.isVehicle() &&\n                this.gameObject.healthTrait.health <= 50 &&\n                this.gameObject.rules.locomotor !== LocomotorType.Hover\n                ? 0.75\n                : 1) *\n            (1 - this.speedPenalty));\n    }\n    constructor(gameObject: GameObject, tileOccupation: TileOccupation) {\n        this.gameObject = gameObject;\n        this.tileOccupation = tileOccupation;\n    }\n    isDisabled(): boolean {\n        return this.disabled;\n    }\n    setDisabled(disabled: boolean): void {\n        this.disabled = disabled;\n    }\n    isMoving(): boolean {\n        return this.moveState === MoveState.Moving;\n    }\n    isIdle(): boolean {\n        return this.moveState === MoveState.Idle;\n    }\n    isWaiting(): boolean {\n        return this.collisionState === CollisionState.Waiting;\n    }\n    [NotifyTick.onTick](gameObject: GameObject, gameState: GameState): void {\n        if (this.moveState !== MoveState.Idle && this.collisionState === CollisionState.Resolved) {\n            const currentTask = gameObject.unitOrderTrait.getCurrentTask();\n            if (!(currentTask && isMoveTask(currentTask))) {\n                this.velocity.set(0, 0, 0);\n                this.moveState = MoveState.Idle;\n                this.locomotor = undefined;\n                if (!currentTask &&\n                    !gameObject.attackTrait?.currentTarget &&\n                    gameObject.isVehicle() &&\n                    gameObject.turretTrait) {\n                    gameObject.turretTrait.desiredFacing = gameObject.direction;\n                }\n            }\n        }\n        if (this.moveState === MoveState.Idle) {\n            if (gameObject.rules.locomotor === LocomotorType.Jumpjet) {\n                JumpjetLocomotor.tickStationary(gameObject as any, gameState as any);\n            }\n            else if (gameObject.isAircraft() &&\n                gameObject.rules.locomotor === LocomotorType.Aircraft) {\n                WingedLocomotor.tickStationary(gameObject as any, gameState as any);\n            }\n        }\n    }\n    [NotifyDestroy.onDestroy](gameObject: GameObject, gameState: GameState): void {\n        this.unreservePathNodes();\n    }\n    teleportUnitToTile(targetTile: any, bridge: any, fromTile: any, preserveMovement: boolean, gameState: GameState): void {\n        const gameObject = this.gameObject;\n        const oldTile = gameObject.tile;\n        (gameObject.traits as any)\n            .filter(NotifyTeleport)\n            .forEach((trait: any) => {\n            trait[NotifyTeleport.onBeforeTeleport](gameObject, gameState, fromTile, preserveMovement);\n        });\n        gameObject.position.tileElevation = gameObject.tileElevation;\n        gameObject.position.tile = targetTile;\n        gameObject.position.subCell = gameObject.position.desiredSubCell;\n        this.handleTileChange(oldTile, bridge, true, gameState, true);\n        if (!preserveMovement) {\n            this.unreservePathNodes();\n            this.speedPenalty = 0;\n            this.velocity.set(0, 0, 0);\n            this.moveState = MoveState.Idle;\n            this.collisionState = CollisionState.Resolved;\n            this.locomotor = undefined;\n            this.currentWaypoint = undefined;\n            this.lastTargetOffset = undefined;\n            this.lastVelocity = undefined;\n            this.lastMoveResult = MoveResult.Cancel;\n            if (gameObject.isVehicle()) {\n                gameObject.spinVelocity = 0;\n                if (gameObject.turretTrait) {\n                    gameObject.turretTrait.desiredFacing = gameObject.direction;\n                }\n            }\n        }\n        this.lastTeleportTick = gameState.currentTick;\n        gameState.events.dispatch(new ObjectTeleportEvent(gameObject, fromTile, oldTile));\n    }\n    handleTileChange(oldTile: any, bridge: any, teleporting: boolean, gameState: GameState, isTeleport: boolean = false): void {\n        const gameObject = this.gameObject;\n        gameState.map.tileOccupation.unoccupyTileRange(oldTile, gameObject);\n        gameState.map.tileOccupation.occupyTileRange(gameObject.tile, gameObject);\n        gameState.map.technosByTile.updateObject(gameObject);\n        if (gameObject.zone !== ZoneType.Air) {\n            const oldBridge = gameObject.onBridge ?\n                gameState.map.tileOccupation.getBridgeOnTile(oldTile) : undefined;\n            const oldLandType = gameObject.onBridge ?\n                oldTile.onBridgeLandType : oldTile.landType;\n            const newLandType = bridge ?\n                gameObject.tile.onBridgeLandType : gameObject.tile.landType;\n            if (oldLandType !== newLandType) {\n                const speedModifier = gameState.rules\n                    .getLandRules(newLandType)\n                    .getSpeedModifier(gameObject.rules.speedType);\n                if (speedModifier > 0 ||\n                    gameObject.rules.speedType === SpeedType.Amphibious ||\n                    isTeleport) {\n                    gameObject.zone = getZoneType(newLandType);\n                }\n            }\n            if (bridge !== oldBridge) {\n                gameObject.position.tileElevation +=\n                    -(oldBridge?.tileElevation ?? 0) + (bridge?.tileElevation ?? 0);\n                gameObject.onBridge = !!bridge;\n            }\n            const nodeIndex = gameObject.moveTrait.reservedPathNodes.findIndex(node => node.tile === gameObject.tile);\n            if (nodeIndex !== -1) {\n                gameObject.moveTrait.reservedPathNodes.splice(nodeIndex, 1);\n            }\n            if (gameObject.crusher) {\n                const crushableObjects = gameState.map\n                    .getGroundObjectsOnTile(gameObject.tile)\n                    .filter(obj => (!obj.isUnit() || obj.onBridge === gameObject.onBridge) &&\n                    obj.rules.crushable &&\n                    !(obj.isInfantry() && obj.stance === StanceType.Paradrop) &&\n                    (!(obj.isTechno() && !teleporting) || !gameState.areFriendly(obj, gameObject)));\n                for (const crushable of crushableObjects) {\n                    if (!crushable.isDestroyed) {\n                        if (crushable.isInfantry()) {\n                            crushable.infDeathType = InfDeathType.None;\n                        }\n                        if (gameObject.isVehicle() &&\n                            crushable.isOverlay() &&\n                            crushable.rules.wall) {\n                            gameObject.applyRocking(0, 0.5);\n                        }\n                        crushable.deathType = DeathType.Crush;\n                        gameState.destroyObject(crushable, { player: gameObject.owner, obj: gameObject });\n                    }\n                }\n            }\n            if (!gameObject.onBridge) {\n                const crate = gameState.map.tileOccupation\n                    .getGroundObjectsOnTile(gameObject.tile)\n                    .find(obj => obj.isOverlay() && obj.rules.crate);\n                if (crate) {\n                    gameState.crateGeneratorTrait.pickupCrate(gameObject, crate, gameState);\n                }\n            }\n        }\n        (gameState.traits as any)\n            .filter(GlobalNotifyTileChange)\n            .forEach((trait: any) => {\n            trait[GlobalNotifyTileChange.onTileChange](gameObject, gameState, oldTile, isTeleport);\n        });\n        (gameObject.traits as any)\n            .filter(NotifyTileChange)\n            .forEach((trait: any) => {\n            trait[NotifyTileChange.onTileChange](gameObject, gameState, oldTile, isTeleport);\n        });\n        gameState.events.dispatch(new EnterTileEvent(gameObject.tile, gameObject));\n    }\n    handleElevationChange(oldElevation: number, gameState: GameState): void {\n        (gameState.traits as any)\n            .filter(NotifyElevationChange)\n            .forEach((trait: any) => {\n            trait[NotifyElevationChange.onElevationChange](this.gameObject, gameState, oldElevation);\n        });\n    }\n    unreservePathNodes(): void {\n        this.reservedPathNodes.forEach(node => {\n            if (node.tile !== this.gameObject.tile) {\n                this.tileOccupation.unoccupySingleTile(node.tile, this.gameObject);\n            }\n        });\n        this.reservedPathNodes.length = 0;\n    }\n    dispose(): void {\n        this.gameObject = undefined as any;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/OilDerrickTrait.ts",
    "content": "import { NotifyOwnerChange } from './interface/NotifyOwnerChange';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { NotifySpawn } from './interface/NotifySpawn';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class OilDerrickTrait {\n    private isActive: boolean = false;\n    private produceCashCooldown: number = 0;\n    [NotifySpawn.onSpawn](gameObject: GameObject): void {\n        if (!gameObject.owner.isNeutral) {\n            this.isActive = true;\n        }\n    }\n    [NotifyOwnerChange.onChange](gameObject: GameObject, world: World): void {\n        if (world.isNeutral && !gameObject.owner.isNeutral) {\n            gameObject.owner.credits = Math.max(0, gameObject.owner.credits + gameObject.rules.produceCashStartup);\n            this.isActive = true;\n            this.produceCashCooldown = gameObject.rules.produceCashDelay;\n        }\n    }\n    [NotifyTick.onTick](gameObject: GameObject): void {\n        if (this.isActive) {\n            this.produceCashCooldown--;\n            if (this.produceCashCooldown <= 0) {\n                this.produceCashCooldown = gameObject.rules.produceCashDelay;\n                gameObject.owner.credits = Math.max(0, gameObject.owner.credits + gameObject.rules.produceCashAmount);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/OverpoweredTrait.ts",
    "content": "import { NotifyTick } from './interface/NotifyTick';\nimport { AttackTask } from '../task/AttackTask';\nimport { GameObject } from '@/game/gameobject/GameObject';\nexport class OverpoweredTrait {\n    private obj: GameObject;\n    private chargers: Set<GameObject>;\n    constructor(obj: GameObject) {\n        this.obj = obj;\n        this.chargers = new Set();\n    }\n    isOverpowered(): boolean {\n        let requiredChargers = 1;\n        if (!this.obj?.poweredTrait?.isPoweredOn(true)) {\n            requiredChargers += 2;\n        }\n        return this.chargers.size >= requiredChargers;\n    }\n    hasChargersToPowerOn(): boolean {\n        return this.chargers.size >= 2;\n    }\n    chargeFrom(charger: GameObject): void {\n        this.chargers.add(charger);\n        this.swapAttackTaskWeapon();\n    }\n    [NotifyTick.onTick](gameObject: GameObject): void {\n        if (this.chargers.size > 0) {\n            let needsUpdate = false;\n            this.chargers.forEach((charger) => {\n                if (charger.isDestroyed ||\n                    charger.isCrashing ||\n                    charger.owner !== gameObject.owner ||\n                    charger.attackTrait?.currentTarget?.obj !== gameObject) {\n                    this.chargers.delete(charger);\n                    needsUpdate = true;\n                }\n            });\n            if (needsUpdate) {\n                this.swapAttackTaskWeapon();\n            }\n        }\n    }\n    private swapAttackTaskWeapon(): void {\n        const currentTask = this.obj?.unitOrderTrait.getCurrentTask();\n        if (currentTask instanceof AttackTask) {\n            const weapon = this.getWeapon();\n            if (weapon) {\n                currentTask.setWeapon(weapon);\n            }\n            else {\n                currentTask.cancel();\n            }\n        }\n    }\n    private getWeapon(): any {\n        return this.isOverpowered()\n            ? this.obj?.secondaryWeapon\n            : this.obj?.primaryWeapon;\n    }\n    dispose(): void {\n        this.obj = undefined as any;\n        this.chargers.clear();\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/ParasiteableTrait.ts",
    "content": "import { DeathType } from '../common/DeathType';\nimport { ZoneType } from '../unit/ZoneType';\nimport { Vehicle } from '../Vehicle';\nimport { NotifyAttack } from './interface/NotifyAttack';\nimport { NotifyDestroy } from './interface/NotifyDestroy';\nimport { NotifyHeal } from './interface/NotifyHeal';\nimport { NotifyDamage } from './interface/NotifyDamage';\nimport { NotifyTeleport } from './interface/NotifyTeleport';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { WaitMinutesTask } from '../task/system/WaitMinutesTask';\nimport { GameSpeed } from '../../GameSpeed';\nimport { RadialTileFinder } from '../../map/tileFinder/RadialTileFinder';\nimport { AttackTask } from '../task/AttackTask';\nconst getRockingTicks = (): number => (Vehicle as any).ROCKING_TICKS + 2;\nexport class ParasiteableTrait implements NotifyTick, NotifyHeal, NotifyDamage, NotifyAttack, NotifyDestroy, NotifyTeleport {\n    private gameObject: any;\n    private beingBoarded: boolean = false;\n    private parasite?: any;\n    private parasiteWeapon?: any;\n    private damageTickCooldown: number = 0;\n    private lastExternalDamageInflicted?: number;\n    private lastExternalDamageTick?: number;\n    constructor(gameObject: any) {\n        this.gameObject = gameObject;\n    }\n    infest(parasite: any, weapon: any): void {\n        this.beingBoarded = false;\n        this.parasite = parasite;\n        this.parasiteWeapon = weapon;\n        this.damageTickCooldown = parasite.rules.organic ? getRockingTicks() : 0;\n        this.lastExternalDamageInflicted = undefined;\n        this.lastExternalDamageTick = undefined;\n        if (weapon.warhead.rules.paralyzes) {\n            this.gameObject.moveTrait.setDisabled(true);\n        }\n    }\n    isInfested(): boolean {\n        return (!(!this.parasite || this.parasite.isDestroyed)) || this.beingBoarded;\n    }\n    isParalyzed(): boolean {\n        return !!this.parasiteWeapon?.warhead.rules.paralyzes;\n    }\n    uninfest(): void {\n        if (this.parasite) {\n            if (this.parasiteWeapon.warhead.rules.paralyzes) {\n                this.gameObject.moveTrait.setDisabled(false);\n            }\n            this.parasite = undefined;\n            this.parasiteWeapon = undefined;\n        }\n    }\n    getParasite(): any {\n        return this.parasite;\n    }\n    [NotifyTick.onTick](target: any, gameState: any): void {\n        if (!this.parasite)\n            return;\n        if (this.parasite.isDestroyed) {\n            this.uninfest();\n            return;\n        }\n        if (this.damageTickCooldown > 0) {\n            this.damageTickCooldown--;\n            return;\n        }\n        const weapon = this.parasiteWeapon;\n        this.damageTickCooldown = this.parasite.rules.organic\n            ? getRockingTicks()\n            : weapon.getCooldownTicks();\n        let damage = weapon.rules.damage;\n        if (this.parasite.veteranTrait) {\n            damage *= this.parasite.veteranTrait.getVeteranDamageMultiplier();\n        }\n        let computedDamage = weapon.warhead.computeDamage(damage, target, gameState);\n        if (this.canBeCulled(target, this.parasite, weapon, gameState)) {\n            computedDamage = target.healthTrait.getHitPoints();\n        }\n        weapon.warhead.inflictDamage(computedDamage, target, {\n            player: this.parasite.owner,\n            obj: this.parasite,\n            weapon: weapon,\n        }, gameState);\n        if (target.isCrashing) {\n            this.parasiteWeapon.expireCooldown();\n            this.evictOrDestroyParasite(target, gameState);\n        }\n        else if (!target.isDestroyed &&\n            target.isVehicle() &&\n            target.zone !== ZoneType.Air &&\n            weapon.warhead.rules.rocker) {\n            target.applyRocking(90 * (gameState.generateRandom() >= 0.5 ? 1 : -1), 1);\n        }\n    }\n    private canBeCulled(target: any, parasite: any, weapon: any, gameState: any): boolean {\n        if (!weapon.warhead.rules.culling)\n            return false;\n        const audioVisual = gameState.rules.audioVisual;\n        const threshold = parasite.veteranTrait?.isElite()\n            ? audioVisual.conditionYellow\n            : audioVisual.conditionRed;\n        return target.healthTrait.health <= 100 * threshold;\n    }\n    [NotifyHeal.onHeal](target: any, gameState: any, amount: number, healer: any): void {\n        if (!this.parasite ||\n            this.parasite.isDestroyed ||\n            healer === target ||\n            (target.isAircraft() && healer?.rules.unitReload)) {\n            return;\n        }\n        if (this.parasite.rules.organic) {\n            const parasite = this.parasite;\n            this.evictOrDestroyParasite(target, gameState);\n            this.stunParasite(parasite, gameState);\n        }\n        else {\n            this.parasite.deathType = DeathType.None;\n            gameState.destroyObject(this.parasite, healer ? { player: healer.owner, obj: healer } : undefined);\n            this.uninfest();\n        }\n    }\n    [NotifyDamage.onDamage](target: any, gameState: any, damage: number, attacker: any): void {\n        if (attacker?.obj !== this.parasite) {\n            this.lastExternalDamageInflicted = damage;\n            this.lastExternalDamageTick = gameState.currentTick;\n        }\n    }\n    [NotifyAttack.onAttack](target: any, attacker: any, gameState: any): void {\n        if (!this.parasite ||\n            this.parasite.isDestroyed ||\n            !attacker?.weapon?.warhead.rules.sonic) {\n            return;\n        }\n        const parasite = this.parasite;\n        this.evictOrDestroyParasite(target, gameState);\n        this.stunParasite(parasite, gameState);\n        const warhead = attacker.weapon.warhead;\n        if (warhead.canDamage(parasite, parasite.tile, parasite.zone)) {\n            const damage = warhead.computeDamage(attacker.weapon.rules.damage, parasite, gameState);\n            warhead.inflictDamage(damage, parasite, attacker, gameState);\n        }\n        const currentTask = attacker.obj?.unitOrderTrait.getCurrentTask();\n        if (currentTask instanceof AttackTask &&\n            currentTask.getWeapon().warhead.rules.sonic) {\n            currentTask.cancel();\n        }\n    }\n    [NotifyDestroy.onDestroy](target: any, gameState: any, destroyer: any, forced: boolean): void {\n        if (!this.parasite || this.parasite.isDestroyed)\n            return;\n        if (forced ||\n            (!this.parasite.invulnerableTrait.isActive() &&\n                this.shouldSupressParasite(gameState, this.parasite, destroyer))) {\n            this.parasite.deathType = DeathType.None;\n            gameState.destroyObject(this.parasite, destroyer, forced);\n            this.uninfest();\n        }\n        else {\n            this.parasiteWeapon.expireCooldown();\n            this.evictOrDestroyParasite(target, gameState);\n        }\n    }\n    private shouldSupressParasite(gameState: any, parasite: any, destroyer: any): boolean {\n        return destroyer?.obj !== parasite ||\n            (this.lastExternalDamageInflicted &&\n                this.lastExternalDamageInflicted > parasite.rules.suppressionThreshold &&\n                gameState.currentTick - this.lastExternalDamageTick! <\n                    2 * this.lastExternalDamageInflicted);\n    }\n    [NotifyTeleport.onBeforeTeleport](target: any, gameState: any, fromTile: any, toTile: any): void {\n        if (!fromTile || !toTile || !this.parasite || this.parasite.isDestroyed)\n            return;\n        this.parasiteWeapon.expireCooldown();\n        const parasite = this.parasite;\n        this.evictOrDestroyParasite(target, gameState, true);\n        if (!parasite.isDestroyed) {\n            this.stunParasite(parasite, gameState);\n        }\n    }\n    private stunParasite(parasite: any, gameState: any): void {\n        parasite.unitOrderTrait.addTaskToFront(new WaitMinutesTask(10 / 60).setCancellable(false));\n        if (parasite.isVehicle() && parasite.submergibleTrait) {\n            parasite.submergibleTrait.emerge(parasite, gameState);\n            parasite.cloakableTrait?.uncloak(gameState);\n            parasite.submergibleTrait.setCooldown(10 * GameSpeed.BASE_TICKS_PER_SECOND);\n        }\n    }\n    private evictOrDestroyParasite(host: any, gameState: any, teleporting: boolean = false): void {\n        if (!this.parasite || this.parasite.isDestroyed)\n            return;\n        const canEvict = gameState.map.terrain.getPassableSpeed(host.tile, this.parasite.rules.speedType, this.parasite.isInfantry(), host.onBridge) || gameState.map.getObjectsOnTile(host.tile).find((obj: any) => obj.isBuilding());\n        if (canEvict) {\n            let targetTile = host.tile;\n            let onBridge = host.onBridge;\n            if ((!teleporting && !host.isDestroyed) || this.parasite.rules.organic) {\n                const tileFinder = new RadialTileFinder(gameState.map.tiles, gameState.map.mapBounds, targetTile, { width: 1, height: 1 }, 1, 1, (tile: any) => gameState.map.terrain.getPassableSpeed(tile, this.parasite.rules.speedType, this.parasite.isInfantry(), onBridge) > 0 &&\n                    !gameState.map.terrain.findObstacles({ tile, onBridge }, this.parasite).length);\n                const foundTile = tileFinder.getNextTile();\n                if (!foundTile) {\n                    this.parasite.deathType = DeathType.None;\n                    gameState.destroyObject(this.parasite, {\n                        player: host.owner,\n                        obj: host,\n                    });\n                    this.uninfest();\n                    return;\n                }\n                targetTile = foundTile;\n            }\n            this.parasite.onBridge = onBridge;\n            this.parasite.position.subCell = this.parasite.isInfantry()\n                ? host.position.subCell\n                : 0;\n            this.parasite.zone = gameState.map.getTileZone(targetTile, !onBridge);\n            this.parasite.position.tileElevation = onBridge\n                ? gameState.map.tileOccupation.getBridgeOnTile(targetTile).tileElevation\n                : 0;\n            this.parasite.resetGuardModeToIdle();\n            gameState.unlimboObject(this.parasite, targetTile, true);\n        }\n        else {\n            this.parasite.deathType = DeathType.None;\n            gameState.destroyObject(this.parasite, {\n                player: host.owner,\n                obj: host,\n            });\n        }\n        this.uninfest();\n    }\n    destroyParasite(destroyer: any, gameState: any): void {\n        if (this.parasite) {\n            this.parasite.deathType = DeathType.None;\n            gameState.destroyObject(this.parasite, destroyer);\n            this.uninfest();\n        }\n    }\n    dispose(): void {\n        this.gameObject = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/PoweredTrait.ts",
    "content": "import { PowerLevel } from '@/game/player/trait/PowerTrait';\nimport { GameObject } from '@/game/gameobject/GameObject';\nexport class PoweredTrait {\n    private obj: GameObject;\n    private turnedOn: boolean;\n    constructor(obj: GameObject) {\n        this.obj = obj;\n        this.turnedOn = true;\n    }\n    setTurnedOn(turnedOn: boolean): void {\n        this.turnedOn = turnedOn;\n    }\n    isCharged(): boolean {\n        return (!!this.obj.isBuilding() &&\n            !!this.obj.overpoweredTrait?.hasChargersToPowerOn());\n    }\n    isPoweredOn(checkCharged: boolean = false): boolean {\n        return (!(!this.obj || !this.turnedOn) &&\n            (!(checkCharged || !this.isCharged()) ||\n                (!this.obj.rules.power && this.obj.rules.needsEngineer\n                    ? !this.obj.owner.isNeutral\n                    : !!this.obj.owner.powerTrait &&\n                        this.obj.owner.powerTrait?.level !== PowerLevel.Low)));\n    }\n    dispose(): void {\n        this.obj = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/PsychicDetectorTrait.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { RangeHelper } from '@/game/gameobject/unit/RangeHelper';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { NotifyWarpChange } from './interface/NotifyWarpChange';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class PsychicDetectorTrait {\n    private radiusTiles: number;\n    private detectionLines: Array<{\n        source: GameObject;\n        target: any;\n    }>;\n    private nextScan: number;\n    constructor(radiusTiles: number) {\n        this.radiusTiles = radiusTiles;\n        this.detectionLines = [];\n        this.nextScan = GameSpeed.BASE_TICKS_PER_SECOND;\n    }\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        if (gameObject.owner.powerTrait?.isLowPower()) {\n            this.disable();\n        }\n        else {\n            if (this.nextScan > 0) {\n                this.nextScan--;\n            }\n            if (this.nextScan <= 0) {\n                this.nextScan = GameSpeed.BASE_TICKS_PER_SECOND;\n                this.detectionLines = this.scan(gameObject, world);\n            }\n        }\n    }\n    [NotifyWarpChange.onChange](gameObject: GameObject, world: World, isWarping: boolean): void {\n        if (isWarping) {\n            this.disable();\n        }\n    }\n    disable(): void {\n        if (this.detectionLines.length) {\n            this.detectionLines = [];\n            this.nextScan = 0;\n        }\n    }\n    private scan(gameObject: GameObject, world: World): Array<{\n        source: GameObject;\n        target: any;\n    }> {\n        const enemies = world\n            .getCombatants()\n            .filter(combatant => combatant !== gameObject.owner && !world.alliances.areAllied(combatant, gameObject.owner));\n        const detectionLines: Array<{\n            source: GameObject;\n            target: any;\n        }> = [];\n        const rangeHelper = new RangeHelper(world.map.tileOccupation);\n        const isInRange = (unit: any) => rangeHelper.distance2(unit, gameObject) / Coords.LEPTONS_PER_TILE <= this.radiusTiles;\n        for (const enemy of enemies) {\n            for (const obj of enemy.getOwnedObjects()) {\n                if (!isInRange(obj)) {\n                    continue;\n                }\n                if (obj.attackTrait?.currentTarget) {\n                    const target = obj.attackTrait.currentTarget;\n                    detectionLines.push({ source: obj, target });\n                }\n                else if (obj.isUnit() && obj.unitOrderTrait.targetLinesConfig) {\n                    const config = obj.unitOrderTrait.targetLinesConfig;\n                    if (config.target) {\n                        const target = world.createTarget(config.target, config.target.tile);\n                        detectionLines.push({ source: obj, target });\n                    }\n                    else if (config.pathNodes[0]) {\n                        const node = config.pathNodes[0];\n                        const target = world.createTarget(node.onBridge, node.tile);\n                        detectionLines.push({ source: obj, target });\n                    }\n                }\n            }\n        }\n        return detectionLines;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/RallyTrait.ts",
    "content": "import { TerrainType } from '@/engine/type/TerrainType';\nimport { RadialTileFinder } from '@/game/map/tileFinder/RadialTileFinder';\nimport { FactoryType } from '@/game/rules/TechnoRules';\nimport { MovementZone } from '@/game/type/MovementZone';\nimport { SpeedType } from '@/game/type/SpeedType';\nimport { Tile } from '@/game/map/Tile';\nimport { GameMap } from '@/game/GameMap';\nimport { GameObject } from '@/game/gameobject/GameObject';\ntype RallyContext = {\n    map: GameMap;\n};\nexport class RallyTrait {\n    private rallyPoint?: Tile;\n    getRallyPoint(): Tile | undefined {\n        return this.rallyPoint;\n    }\n    changeRallyPoint(targetTile: Tile, gameObject: GameObject, world: RallyContext): void {\n        const validPoint = this.findValidRallyPoint(gameObject, targetTile, world.map);\n        if (validPoint) {\n            this.rallyPoint = validPoint;\n        }\n    }\n    findValidRallyPoint(gameObject: GameObject, targetTile: Tile, map: GameMap): Tile | undefined {\n        const finder = new RadialTileFinder(map.tiles, map.mapBounds, targetTile, { width: 1, height: 1 }, 0, 20, (tile) => (gameObject.rules.naval || tile.terrainType !== TerrainType.Water) &&\n            !map.tileOccupation.isTileOccupiedBy(tile, gameObject));\n        let validTile = finder.getNextTile();\n        if (!validTile && gameObject.factoryTrait?.type === FactoryType.NavalUnitType) {\n            const { width, height } = gameObject.getFoundation();\n            for (let x = 0; x < width; x++) {\n                for (let y = 0; y < height; y++) {\n                    const tile = map.tiles.getByMapCoords(gameObject.tile.rx + x, gameObject.tile.ry + y);\n                    if (!tile)\n                        break;\n                    if (map.terrain.getPassableSpeed(tile, SpeedType.Float, false, false) > 0) {\n                        validTile = tile;\n                        break;\n                    }\n                }\n            }\n        }\n        return validTile;\n    }\n    findRallyNodeForUnit(unit: GameObject, map: GameMap): {\n        tile: Tile;\n        onBridge?: any;\n    } | undefined {\n        if (this.rallyPoint) {\n            const rallyTile = this.findRallyPointforUnit(unit, this.rallyPoint, map, true);\n            return {\n                tile: rallyTile,\n                onBridge: unit.rules.naval ? undefined : map.tileOccupation.getBridgeOnTile(rallyTile)\n            };\n        }\n    }\n    findRallyPointforUnit(unit: GameObject, targetTile: Tile, map: GameMap, checkBuildings: boolean, targetElevation?: number): Tile {\n        const bridge = unit.rules.naval ? undefined : map.tileOccupation.getBridgeOnTile(targetTile);\n        const isFlying = unit.rules.movementZone === MovementZone.Fly;\n        const finder = new RadialTileFinder(map.tiles, map.mapBounds, targetTile, { width: 1, height: 1 }, 0, 5, (tile) => {\n            const tileBridge = !bridge || bridge.isHighBridge()\n                ? map.tileOccupation.getBridgeOnTile(tile)\n                : undefined;\n            return (!(isFlying ? [] : map.terrain.findObstacles({ tile, onBridge: tileBridge }, unit as any)).length &&\n                (targetElevation === undefined || Math.abs(targetElevation - (tile.z + (tileBridge?.tileElevation ?? 0))) < 4) &&\n                (!checkBuildings || !map.getObjectsOnTile(tile).find(obj => obj.isBuilding() && !obj.isDestroyed)) &&\n                (isFlying || map.terrain.getPassableSpeed(tile, unit.rules.speedType, unit.isInfantry(), !!tileBridge) > 0));\n        });\n        return finder.getNextTile() ?? targetTile;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/SelfHealingTrait.ts",
    "content": "import { NotifyTick } from './interface/NotifyTick';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class SelfHealingTrait {\n    private cooldownTicks: number = 0;\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        if (gameObject.healthTrait.health !== 100) {\n            if (this.cooldownTicks <= 0) {\n                this.cooldownTicks +=\n                    GameSpeed.BASE_TICKS_PER_SECOND *\n                        world.rules.general.repair.repairRate *\n                        60;\n                gameObject.healthTrait.healBy(1, gameObject, world);\n            }\n            else {\n                this.cooldownTicks--;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/SensorsTrait.ts",
    "content": "export class SensorsTrait {\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/SpawnDebrisTrait.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { DeathType } from '@/game/gameobject/common/DeathType';\nimport { NotifyCrash } from '@/game/gameobject/trait/interface/NotifyCrash';\nimport { NotifyDestroy } from '@/game/gameobject/trait/interface/NotifyDestroy';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class SpawnDebrisTrait {\n    [NotifyCrash.onCrash](gameObject: GameObject, world: World): void {\n        this.handleDestroy(gameObject, world);\n    }\n    [NotifyDestroy.onDestroy](gameObject: GameObject, world: World, context?: any): void {\n        if (!context?.weapon?.warhead.rules.temporal &&\n            !gameObject.isCrashing &&\n            gameObject.deathType !== DeathType.Sink &&\n            gameObject.isSpawned) {\n            this.handleDestroy(gameObject, world);\n        }\n    }\n    private handleDestroy(gameObject: GameObject, world: World): void {\n        if (gameObject.isVehicle() || gameObject.isBuilding() || gameObject.isOverlay()) {\n            const minDebris = gameObject.isOverlay() ? 0 : gameObject.rules.minDebris;\n            const maxDebris = gameObject.isOverlay()\n                ? world.rules.general.bridgeVoxelMax\n                : gameObject.rules.maxDebris;\n            const debrisCount = world.generateRandomInt(minDebris, maxDebris);\n            if (debrisCount > 0) {\n                this.spawnDebris(gameObject, world, debrisCount);\n            }\n        }\n    }\n    private spawnDebris(gameObject: GameObject, world: World, count: number): void {\n        const position = gameObject.position.getMapPosition();\n        if (world.map.isWithinHardBounds(position)) {\n            let debrisTypes = gameObject.isOverlay()\n                ? []\n                : gameObject.isVehicle()\n                    ? gameObject.rules.debrisTypes\n                    : gameObject.rules.debrisAnims;\n            if (!debrisTypes.length) {\n                debrisTypes = world.rules.audioVisual.metallicDebris;\n            }\n            debrisTypes = debrisTypes.filter(type => world.rules.hasObject(type, ObjectType.VoxelAnim) ||\n                world.art.hasObject(type, ObjectType.Animation));\n            Array(count).fill(0)\n                .map(() => debrisTypes[world.generateRandomInt(0, debrisTypes.length - 1)])\n                .map(type => world.createObject(ObjectType.Debris, type))\n                .forEach(debris => {\n                debris.position.moveToLeptons(position);\n                debris.position.tileElevation = gameObject.position.tileElevation;\n                world.spawnObject(debris, debris.position.tile);\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/SpawnLinkTrait.ts",
    "content": "import { AttackTask } from '@/game/gameobject/task/AttackTask';\nimport { MoveTask } from '@/game/gameobject/task/move/MoveTask';\nimport { RangeHelper } from '@/game/gameobject/unit/RangeHelper';\nimport { AttackTrait, AttackState } from '@/game/gameobject/trait/AttackTrait';\nimport { NotifyTick } from '@/game/gameobject/trait/interface/NotifyTick';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class SpawnLinkTrait {\n    private parent?: GameObject;\n    setParent(parent: GameObject): void {\n        this.parent = parent;\n    }\n    getParent(): GameObject | undefined {\n        return this.parent;\n    }\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        if (!this.parent || !gameObject.attackTrait || !gameObject.primaryWeapon) {\n            return;\n        }\n        const parentTarget = this.parent.attackTrait?.currentTarget;\n        const currentTask = gameObject.unitOrderTrait.getCurrentTask();\n        const rangeHelper = new RangeHelper(world.map.tileOccupation);\n        const spawnerWeapon = this.parent.armedTrait?.getWeapons().find(w => w.rules.spawner);\n        const shouldAttack = gameObject.ammo &&\n            !(parentTarget && gameObject.attackTrait.currentTarget\n                ? parentTarget.equals(gameObject.attackTrait.currentTarget)\n                : parentTarget === gameObject.attackTrait.currentTarget ||\n                    (!parentTarget &&\n                        this.parent.isUnit() &&\n                        (this.parent.unitOrderTrait.getCurrentTask() instanceof MoveTask ||\n                            this.parent.unitOrderTrait.getCurrentTask() instanceof AttackTask))) &&\n            (!parentTarget ||\n                (spawnerWeapon &&\n                    rangeHelper.isInWeaponRange(this.parent, parentTarget.obj ?? parentTarget.tile, spawnerWeapon, world.rules)));\n        if (shouldAttack) {\n            if (parentTarget &&\n                gameObject.primaryWeapon.targeting.canTarget(parentTarget.obj, parentTarget.tile, world, true, false)) {\n                if (!currentTask || currentTask instanceof MoveTask) {\n                    gameObject.unitOrderTrait.cancelAllTasks();\n                    gameObject.unitOrderTrait.addTask(gameObject.attackTrait.createAttackTask(world, parentTarget.obj, parentTarget.tile, gameObject.primaryWeapon, { force: true }));\n                }\n                else if (gameObject.attackTrait.attackState !== AttackState.Idle) {\n                    currentTask.requestTargetUpdate(parentTarget);\n                }\n            }\n            else {\n                if (currentTask) {\n                    if (currentTask instanceof MoveTask) {\n                        this.tryMoveToParent(gameObject, this.parent, world);\n                    }\n                    else {\n                        currentTask.cancel();\n                    }\n                }\n                else {\n                    this.tryMoveToParent(gameObject, this.parent, world);\n                }\n            }\n        }\n        else {\n            this.tryMoveToParent(gameObject, this.parent, world);\n        }\n    }\n    private tryMoveToParent(gameObject: GameObject, parent: GameObject, world: World): void {\n        if (gameObject.tile !== parent.tile) {\n            const currentTask = gameObject.unitOrderTrait.getCurrentTask();\n            if (currentTask instanceof MoveTask) {\n                currentTask.updateTarget(parent.tile, parent.isUnit() && parent.onBridge);\n            }\n            else {\n                gameObject.unitOrderTrait.addTask(new MoveTask(world as any, parent.tile, parent.isUnit() && parent.onBridge, {\n                    closeEnoughTiles: 0,\n                    strictCloseEnough: true\n                }));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/SubmergibleTrait.ts",
    "content": "import { ShipSubmergeChangeEvent } from '@/game/event/ShipSubmergeChangeEvent';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { AttackTrait, AttackState } from '@/game/gameobject/trait/AttackTrait';\nimport { NotifyDamage } from '@/game/gameobject/trait/interface/NotifyDamage';\nimport { NotifyTick } from '@/game/gameobject/trait/interface/NotifyTick';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class SubmergibleTrait {\n    private isActive: boolean = false;\n    private cooldownTicks?: number;\n    isSubmerged(): boolean {\n        return this.isActive;\n    }\n    setCooldown(ticks: number): void {\n        this.cooldownTicks = ticks;\n    }\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        if (this.isActive || gameObject.parasiteableTrait?.isInfested()) {\n            return;\n        }\n        if (gameObject.attackTrait &&\n            gameObject.attackTrait.attackState !== AttackState.Idle &&\n            !gameObject.moveTrait.isMoving()) {\n            this.cooldownTicks = Math.max(this.cooldownTicks ?? 0, 5 * GameSpeed.BASE_TICKS_PER_SECOND);\n        }\n        else {\n            this.cooldownTicks ??= Math.floor(60 * world.rules.general.cloakDelay * GameSpeed.BASE_TICKS_PER_SECOND);\n        }\n        if (this.cooldownTicks > 0) {\n            this.cooldownTicks--;\n        }\n        if (this.cooldownTicks <= 0) {\n            this.isActive = true;\n            world.events.dispatch(new ShipSubmergeChangeEvent(gameObject));\n        }\n    }\n    [NotifyDamage.onDamage](gameObject: GameObject, world: World): void {\n        this.emerge(gameObject, world);\n    }\n    emerge(gameObject: GameObject, world: World): void {\n        if (this.isActive) {\n            this.isActive = false;\n            this.cooldownTicks = undefined;\n            world.events.dispatch(new ShipSubmergeChangeEvent(gameObject));\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/SuperWeaponTrait.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { NotifyOwnerChange } from '@/game/gameobject/trait/interface/NotifyOwnerChange';\nimport { NotifySpawn } from '@/game/gameobject/trait/interface/NotifySpawn';\nimport { NotifyUnspawn } from '@/game/gameobject/trait/interface/NotifyUnspawn';\nexport class SuperWeaponTrait {\n    private name: string;\n    constructor(name: string) {\n        this.name = name;\n    }\n    getSuperWeapon(gameObject: any) {\n        return gameObject.owner.superWeaponsTrait?.get(this.name);\n    }\n    [NotifySpawn.onSpawn](gameObject: any, world: any): void {\n        this.addSuperWeaponToPlayerIfNeeded(gameObject.owner, world);\n    }\n    [NotifyUnspawn.onUnspawn](gameObject: any, world: any): void {\n        this.removeSuperWeaponFromPlayerIfNeeded(gameObject.owner);\n    }\n    [NotifyOwnerChange.onChange](gameObject: any, oldOwner: any, newOwner: any): void {\n        this.removeSuperWeaponFromPlayerIfNeeded(oldOwner);\n        this.addSuperWeaponToPlayerIfNeeded(gameObject.owner, newOwner);\n    }\n    private addSuperWeaponToPlayerIfNeeded(player: any, world: any): void {\n        if (player.superWeaponsTrait && !player.superWeaponsTrait.has(this.name)) {\n            const superWeapon = world.createSuperWeapon(this.name, player);\n            player.superWeaponsTrait.add(superWeapon);\n            if (superWeapon.rules.isPowered && player.powerTrait?.isLowPower()) {\n                superWeapon.pauseTimer();\n            }\n        }\n    }\n    private removeSuperWeaponFromPlayerIfNeeded(player: any): void {\n        const superWeaponsTrait = player.superWeaponsTrait;\n        if (!superWeaponsTrait)\n            return;\n        const hasBuildingWithSuperWeapon = player\n            .getOwnedObjectsByType(ObjectType.Building)\n            .some(building => building.superWeaponTrait?.name === this.name);\n        if (!hasBuildingWithSuperWeapon) {\n            const superWeapon = superWeaponsTrait.get(this.name);\n            if (superWeapon && !superWeapon.isGift) {\n                superWeaponsTrait.remove(this.name);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/SuppressionTrait.ts",
    "content": "import { NotifyTick } from './interface/NotifyTick';\nexport class SuppressionTrait {\n    private suppressionTicks: number = 0;\n    private enabled: boolean = true;\n    disable(): void {\n        this.enabled = false;\n    }\n    isSuppressed(): boolean {\n        return this.enabled && this.suppressionTicks > 0;\n    }\n    suppress(): void {\n        if (this.enabled) {\n            this.suppressionTicks = 30;\n        }\n    }\n    [NotifyTick.onTick](): void {\n        if (this.suppressionTicks > 0) {\n            this.suppressionTicks--;\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/TemporalTrait.ts",
    "content": "import { DeathType } from '@/game/gameobject/common/DeathType';\nimport { AttackTask } from '@/game/gameobject/task/AttackTask';\nimport { MoveTask } from '@/game/gameobject/task/move/MoveTask';\nimport { NotifyDestroy } from './interface/NotifyDestroy';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class TemporalTrait {\n    private gameObject: GameObject;\n    private ticksWhenWarpedOut: boolean = true;\n    private attackers: Set<GameObject> = new Set();\n    private currentTarget?: GameObject;\n    private currentWeapon?: any;\n    private eraseTicks?: number;\n    constructor(gameObject: GameObject) {\n        this.gameObject = gameObject;\n    }\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        gameObject.attackTrait &&\n            ((gameObject.attackTrait.currentTarget &&\n                !gameObject.warpedOutTrait.isActive()) ||\n                this.releaseCurrentTarget(world));\n        if (this.eraseTicks !== undefined) {\n            for (const attacker of this.attackers) {\n                const weapon = attacker.temporalTrait.currentWeapon;\n                if (!weapon) {\n                    throw new Error(`Attacker \"${attacker.name}\" is no longer targeting \"${gameObject.name}\"`);\n                }\n                const damage = weapon.rules.damage;\n                this.eraseTicks -= damage;\n                if (this.eraseTicks <= 0) {\n                    gameObject.deathType = DeathType.Temporal;\n                    world.destroyObject(gameObject, { player: attacker.owner, obj: attacker, weapon }, true);\n                    this.eraseTicks = undefined;\n                    break;\n                }\n            }\n        }\n    }\n    getTarget(): GameObject | undefined {\n        return this.currentTarget;\n    }\n    updateTarget(target: GameObject, weapon: any, world: World): void {\n        if (this.currentTarget !== target) {\n            this.releaseCurrentTarget(world);\n            this.currentTarget = target;\n            this.currentWeapon = weapon;\n            const attackerCount = target.temporalTrait.attackers.size;\n            target.temporalTrait.attackers.add(this.gameObject);\n            if (!attackerCount) {\n                target.warpedOutTrait.setActive(true, true, world);\n                const currentTask = target.unitOrderTrait.getCurrentTask();\n                if ((currentTask && currentTask instanceof AttackTask) ||\n                    currentTask instanceof MoveTask) {\n                    currentTask.cancel();\n                }\n                target.temporalTrait.eraseTicks = 10 * target.healthTrait.maxHitPoints;\n            }\n        }\n    }\n    releaseCurrentTarget(world: World): void {\n        if (this.currentTarget) {\n            if (!this.currentTarget.isDisposed) {\n                const attackers = this.currentTarget.temporalTrait.attackers;\n                attackers.delete(this.gameObject);\n                if (!attackers.size) {\n                    this.currentTarget.warpedOutTrait.expire(world);\n                    this.currentTarget.temporalTrait.eraseTicks = undefined;\n                }\n            }\n            this.currentTarget = undefined;\n            this.currentWeapon = undefined;\n        }\n    }\n    [NotifyDestroy.onDestroy](gameObject: GameObject, world: World): void {\n        this.releaseCurrentTarget(world);\n    }\n    dispose(): void {\n        this.gameObject = undefined;\n        this.attackers.clear();\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/TiberiumTrait.ts",
    "content": "import { OreOverlayTypes } from '@/game/map/OreOverlayTypes';\nimport { OverlayTibType } from '@/engine/type/OverlayTibType';\nimport { TiberiumType } from '@/engine/type/TiberiumType';\nimport { LandType } from '@/game/type/LandType';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class TiberiumTrait {\n    static maxBails: number = 11;\n    private gameObject: GameObject;\n    constructor(gameObject: GameObject) {\n        this.gameObject = gameObject;\n    }\n    static canBePlacedOn(tile: any, world: World): boolean {\n        return ([LandType.Clear, LandType.Road, LandType.Rough].includes(tile.landType) &&\n            !world\n                .getGroundObjectsOnTile(tile)\n                .find((obj) => !obj.isSmudge() && !obj.isUnit()));\n    }\n    getTiberiumType(): TiberiumType {\n        const overlayTibType = OreOverlayTypes.getOverlayTibType(this.gameObject.overlayId);\n        switch (overlayTibType) {\n            case OverlayTibType.Ore:\n                return TiberiumType.Ore;\n            case OverlayTibType.Gems:\n                return TiberiumType.Gems;\n            case OverlayTibType.Vinifera:\n                return TiberiumType.Ore;\n            default:\n                throw new Error(`Unsupported tiberium type ${overlayTibType}`);\n        }\n    }\n    collectBail(): TiberiumType | undefined {\n        const bailCount = this.getBailCount();\n        if (bailCount <= 0) {\n            throw new Error('Attempted to collect an ore bail, but there are none left');\n        }\n        this.gameObject.value--;\n        return bailCount > 1 ? this.getTiberiumType() : undefined;\n    }\n    spawnBails(count: number): void {\n        this.gameObject.value = Math.min(TiberiumTrait.maxBails, this.gameObject.value + count);\n    }\n    removeBails(count: number): void {\n        this.gameObject.value = Math.max(-1, this.gameObject.value - count);\n    }\n    getBailCount(): number {\n        return this.gameObject.value + 1;\n    }\n    dispose(): void {\n        this.gameObject = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/TiberiumTreeTrait.ts",
    "content": "import { NotifyTick } from './interface/NotifyTick';\nimport { RadialTileFinder } from '@/game/map/tileFinder/RadialTileFinder';\nimport { LandType } from '@/game/type/LandType';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { OreSpread } from '@/game/map/OreSpread';\nimport { OverlayTibType } from '@/engine/type/OverlayTibType';\nimport { TiberiumTrait } from './TiberiumTrait';\nexport enum SpawnStatus {\n    Idle = 0,\n    Spawning = 1\n}\nexport class TiberiumTreeTrait {\n    private rules: any;\n    private ticksSinceLastSpawn: number = 0;\n    private cooldownTicks: number;\n    private status: SpawnStatus = SpawnStatus.Idle;\n    constructor(rules: any) {\n        this.rules = rules;\n        this.cooldownTicks = Math.floor(1 / rules.animationProbability);\n    }\n    [NotifyTick.onTick](gameObject: any, world: any): void {\n        this.status = SpawnStatus.Idle;\n        if (this.ticksSinceLastSpawn++ > this.cooldownTicks) {\n            this.ticksSinceLastSpawn = 0;\n            this.status = SpawnStatus.Spawning;\n            this.spawnTiberium(gameObject.tile, world);\n        }\n    }\n    private spawnTiberium(tile: any, world: any): void {\n        for (let radius = 1; radius <= 2; radius++) {\n            let finder = new RadialTileFinder(world.map.tiles, world.map.mapBounds, tile, { width: 1, height: 1 }, radius, radius, (tile) => TiberiumTrait.canBePlacedOn(tile, world.map));\n            let targetTile = finder.getNextTile();\n            if (targetTile) {\n                const overlayId = OreSpread.calculateOverlayId(OverlayTibType.Ore, targetTile);\n                if (overlayId === undefined) {\n                    throw new Error('Expected an overlayId');\n                }\n                const overlay = world.createObject(ObjectType.Overlay, world.rules.getOverlayName(overlayId));\n                overlay.overlayId = overlayId;\n                overlay.value = 3;\n                world.spawnObject(overlay, targetTile);\n                return;\n            }\n            finder = new RadialTileFinder(world.map.tiles, world.map.mapBounds, tile, { width: 1, height: 1 }, radius, radius, (tile) => tile.landType === LandType.Tiberium);\n            let existingTiberium;\n            while (!existingTiberium) {\n                const nextTile = finder.getNextTile();\n                if (!nextTile)\n                    break;\n                existingTiberium = world.map\n                    .getObjectsOnTile(nextTile)\n                    .find((obj) => obj.isOverlay() &&\n                    obj.isTiberium() &&\n                    obj.traits.get(TiberiumTrait).getBailCount() + 1 <= TiberiumTrait.maxBails);\n            }\n            if (existingTiberium) {\n                existingTiberium.traits.get(TiberiumTrait).spawnBails(1);\n                return;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/TilterTrait.ts",
    "content": "import { NotifySpawn } from './interface/NotifySpawn';\nimport { NotifyTileChange } from './interface/NotifyTileChange';\nexport class TilterTrait {\n    private tilt: {\n        pitch: number;\n        yaw: number;\n    };\n    constructor() {\n        this.tilt = { pitch: 0, yaw: 0 };\n    }\n    [NotifySpawn.onSpawn](target: any): void {\n        this.tilt = this.computeTilt(target.tile.rampType);\n    }\n    [NotifyTileChange.onTileChange](target: any): void {\n        this.tilt = this.computeTilt(target.tile.rampType);\n    }\n    private computeTilt(rampType: number): {\n        pitch: number;\n        yaw: number;\n    } {\n        let pitch: number;\n        let yaw: number;\n        if (rampType === 0 || rampType >= 17) {\n            pitch = yaw = 0;\n        }\n        else if (rampType <= 4) {\n            pitch = 25;\n            yaw = -90 * rampType;\n        }\n        else {\n            pitch = 25;\n            yaw = 225 - ((rampType - 1) % 4) * 90;\n        }\n        return { pitch, yaw };\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/TntChargeTrait.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport { Warhead } from '@/game/Warhead';\nimport { DeathType } from '@/game/gameobject/common/DeathType';\nimport { CollisionType } from '@/game/gameobject/unit/CollisionType';\nimport { Timer } from '@/game/gameobject/unit/Timer';\nimport { NotifyDestroy } from './interface/NotifyDestroy';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class TntChargeTrait {\n    private timer: Timer;\n    private attackerInfo?: any;\n    constructor() {\n        this.timer = new Timer();\n    }\n    hasCharge(): boolean {\n        return this.timer.isActive();\n    }\n    setCharge(ticks: number, currentTick: number, attackerInfo: any): void {\n        if (!this.hasCharge()) {\n            this.timer.setActiveFor(ticks, currentTick);\n            this.attackerInfo = attackerInfo;\n        }\n    }\n    getChargeOwner(): any {\n        return this.attackerInfo?.player;\n    }\n    removeCharge(): void {\n        this.timer.reset();\n    }\n    getTicksLeft(): number {\n        return this.timer.getTicksLeft();\n    }\n    getInitialTicks(): number {\n        return this.timer.getInitialTicks();\n    }\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        if (this.timer.isActive() && this.timer.tick(world.currentTick) === true) {\n            if (gameObject.isBuilding() && gameObject.cabHutTrait) {\n                gameObject.cabHutTrait.demolishBridge(world, this.attackerInfo);\n            }\n            this.detonateIvanWarhead(world, gameObject);\n        }\n    }\n    [NotifyDestroy.onDestroy](gameObject: GameObject, world: World, context?: any): void {\n        if (this.timer.isActive() &&\n            !context?.weapon?.warhead.rules.ivanBomb &&\n            gameObject.deathType !== DeathType.None &&\n            gameObject.deathType !== DeathType.Temporal) {\n            this.timer.reset();\n            this.detonateIvanWarhead(world, gameObject);\n        }\n    }\n    private detonateIvanWarhead(world: World, target: GameObject): void {\n        const damage = world.rules.combatDamage.ivanDamage;\n        const warhead = new Warhead(world.rules.getWarhead(world.rules.combatDamage.ivanWarhead));\n        const tile = target.tile;\n        const elevation = target.tileElevation;\n        const zone = target.isUnit() ? target.zone : world.map.getTileZone(tile);\n        const onBridge = !!target.isUnit() && target.onBridge;\n        warhead.detonate(world as any, damage, tile, elevation, target.isBuilding()\n            ? Coords.tile3dToWorld(tile.rx + 0.5, tile.ry + 0.5, tile.z + elevation)\n            : target.position.worldPosition, zone, onBridge ? CollisionType.OnBridge : CollisionType.None, world.createTarget(target, tile), { ...this.attackerInfo, weapon: undefined }, false, false as any, undefined);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/TransportTrait.ts",
    "content": "import { fnv32a } from '@/util/math';\nimport { NotifyDestroy } from './interface/NotifyDestroy';\nimport { ScatterTask } from '../task/ScatterTask';\nimport { LeaveTransportEvent } from '@/game/event/LeaveTransportEvent';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { ZoneType } from '../unit/ZoneType';\nimport { GameObject } from '../GameObject';\nimport { World } from '@/game/World';\nexport class TransportTrait {\n    private obj: GameObject;\n    public units: GameObject[] = [];\n    private loadQueue: GameObject[] = [];\n    constructor(obj: GameObject) {\n        this.obj = obj;\n    }\n    unitFitsInside(unit: GameObject): boolean {\n        return (unit.rules.size <= this.obj.rules.sizeLimit &&\n            unit.rules.size <= this.getAvailableCapacity());\n    }\n    getOccupiedCapacity(): number {\n        return this.units.reduce((sum, unit) => sum + unit.rules.size, 0);\n    }\n    getMaxCapacity(): number {\n        return this.obj.rules.passengers;\n    }\n    getAvailableCapacity(): number {\n        return this.getMaxCapacity() - this.getOccupiedCapacity();\n    }\n    addToLoadQueue(unit: GameObject): number {\n        this.loadQueue.push(unit);\n        return this.loadQueue.length - 1;\n    }\n    unitIsFirstInLoadQueue(unit: GameObject): boolean {\n        return this.loadQueue[0] === unit;\n    }\n    removeFromLoadQueue(unit: GameObject): void {\n        const index = this.loadQueue.indexOf(unit);\n        if (index !== -1) {\n            this.loadQueue.splice(index, 1);\n        }\n    }\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        this.loadQueue = this.loadQueue.filter((unit) => !unit.isDestroyed && !unit.isCrashing);\n    }\n    [NotifyDestroy.onDestroy](gameObject: GameObject, world: World, context?: any, forceDestroy?: boolean): void {\n        const hasDeathWeapon = !!gameObject.armedTrait?.deathWeapon;\n        const isParasite = context?.weapon?.warhead.rules.parasite;\n        if (forceDestroy || hasDeathWeapon || gameObject.zone === ZoneType.Air || isParasite) {\n            for (const unit of this.units) {\n                if (hasDeathWeapon && unit.armedTrait) {\n                    unit.armedTrait.deathWeapon = undefined;\n                }\n                unit.position.tileElevation = gameObject.position.tileElevation;\n                unit.zone = gameObject.zone;\n                unit.onBridge = gameObject.onBridge;\n                unit.position.tile = gameObject.tile;\n                world.destroyObject(unit, context, true);\n            }\n        }\n        else {\n            this.spawnSurvivors(world);\n        }\n        this.units = [];\n    }\n    private spawnSurvivors(world: World): void {\n        const transport = this.obj;\n        if (this.units.length) {\n            for (const unit of this.units) {\n                if (world.map.terrain.getPassableSpeed(transport.tile, unit.rules.speedType, unit.isInfantry(), transport.onBridge) > 0) {\n                    unit.owner.addOwnedObject(unit);\n                    unit.position.tileElevation = transport.onBridge\n                        ? world.map.tileOccupation.getBridgeOnTile(transport.tile).tileElevation\n                        : 0;\n                    unit.onBridge = transport.onBridge;\n                    unit.zone = world.map.getTileZone(transport.tile, !transport.onBridge);\n                    world.unlimboObject(unit, transport.tile);\n                    unit.unitOrderTrait.addTask(new ScatterTask(world));\n                }\n                else {\n                    unit.position.tileElevation = transport.position.tileElevation;\n                    unit.zone = transport.zone;\n                    unit.onBridge = transport.onBridge;\n                    unit.position.tile = transport.tile;\n                    world.destroyObject(unit, { player: unit.owner });\n                }\n            }\n            world.events.dispatch(new LeaveTransportEvent(transport));\n        }\n    }\n    getHash(): number {\n        return fnv32a(this.units.map((unit) => unit.getHash()));\n    }\n    debugGetState(): any[] {\n        return this.units.map((unit) => unit.debugGetState());\n    }\n    dispose(): void {\n        this.obj = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/TurretTrait.ts",
    "content": "import { FacingUtil } from '@/game/gameobject/unit/FacingUtil';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { NotifySpawn } from './interface/NotifySpawn';\nexport class TurretTrait {\n    private facing: number = 0;\n    private desiredFacing: number = 0;\n    isRotating(): boolean {\n        return this.facing !== this.desiredFacing;\n    }\n    [NotifySpawn.onSpawn](target: any): void {\n        if (target.isUnit()) {\n            this.facing = this.desiredFacing = target.direction;\n        }\n    }\n    [NotifyTick.onTick](gameObject: any): void {\n        if (this.desiredFacing !== this.facing) {\n            const rotationSpeed = gameObject.rules.rot;\n            this.facing = FacingUtil.tick(this.facing, this.desiredFacing, rotationSpeed || Number.POSITIVE_INFINITY).facing;\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/UnitOrderTrait.ts",
    "content": "import { TaskRunner } from \"@/game/gameobject/task/system/TaskRunner\";\nimport { TaskStatus } from \"@/game/gameobject/task/system/TaskStatus\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nimport { WaitTicksTask } from \"@/game/gameobject/task/system/WaitTicksTask\";\nimport { NotifyOwnerChange } from \"@/game/gameobject/trait/interface/NotifyOwnerChange\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { NotifyTeleport } from \"@/game/gameobject/trait/interface/NotifyTeleport\";\nimport { NotifyOrder } from \"@/game/gameobject/trait/interface/NotifyOrder\";\ninterface GameObject {\n    isSpawned: boolean;\n    resetGuardModeToIdle(): void;\n    traits: {\n        filter(type: any): any[];\n    };\n    unitOrderTrait: UnitOrderTrait;\n}\ninterface Task {\n    isCancelling(): boolean;\n    cancel(): void;\n    status: TaskStatus;\n    useChildTargetLines?: boolean;\n    children?: Task[];\n    getTargetLinesConfig?(gameObject: GameObject): any;\n}\ninterface Order {\n    isValid(): boolean;\n    isAllowed(): boolean;\n    process(): Task[] | null;\n    onAdd(tasks: Task[], queued: boolean): boolean | void;\n    orderType: string;\n}\ninterface Waypoint {\n    next?: Waypoint;\n}\ninterface WaypointPath {\n    waypoints: Waypoint[];\n    units: GameObject[];\n}\nexport class UnitOrderTrait implements NotifyTick, NotifyOwnerChange, NotifyTeleport {\n    private gameObject: GameObject;\n    private orders: Order[] = [];\n    private queuedOrders = new Set<Order>();\n    private tasks: Task[] = [];\n    private taskRunner = new TaskRunner();\n    private waypointPath?: WaypointPath;\n    private currentWaypoint?: Waypoint;\n    private targetLinesTask?: Task;\n    private targetLinesConfig?: any;\n    constructor(gameObject: GameObject) {\n        this.gameObject = gameObject;\n    }\n    [NotifyTick.onTick](gameObject: GameObject, deltaTime: number): void {\n        if (!gameObject.isSpawned)\n            return;\n        const hasTasks = this.hasTasks();\n        const currentTask = this.tasks.find(task => !task.isCancelling());\n        if (hasTasks) {\n            this.taskRunner.tick(this.tasks as any, gameObject);\n        }\n        if (!gameObject.isSpawned)\n            return;\n        const orderCount = this.orders.length;\n        if (orderCount && (!hasTasks || !currentTask)) {\n            let processedOrder = false;\n            while (this.orders.length > 0) {\n                const order = this.orders[0];\n                if (order.isValid() && order.isAllowed()) {\n                    const newTasks = order.process();\n                    if (newTasks) {\n                        if (this.queuedOrders.has(order)) {\n                            this.tasks.push(new WaitTicksTask(5) as any);\n                            this.tasks.push(new CallbackTask(() => {\n                                gameObject.resetGuardModeToIdle();\n                            }) as any);\n                        }\n                        this.tasks.push(...newTasks);\n                        if (!hasTasks) {\n                            this.taskRunner.tick(this.tasks as any, gameObject);\n                        }\n                    }\n                    processedOrder = true;\n                }\n                this.orders.shift();\n                this.queuedOrders.delete(order);\n                if (!gameObject.isSpawned)\n                    return;\n                if (this.waypointPath) {\n                    if (this.currentWaypoint) {\n                        this.cleanupWaypoint(this.currentWaypoint, this.waypointPath);\n                        this.currentWaypoint = this.currentWaypoint.next;\n                    }\n                    else {\n                        this.currentWaypoint = this.waypointPath.waypoints[0];\n                    }\n                    if (!this.currentWaypoint) {\n                        this.cleanupWaypointPath();\n                    }\n                }\n                if (processedOrder)\n                    break;\n            }\n        }\n        if (!orderCount && !hasTasks && this.waypointPath && this.currentWaypoint) {\n            this.cleanupWaypoint(this.currentWaypoint, this.waypointPath);\n            this.cleanupWaypointPath();\n        }\n        let targetTask = currentTask;\n        while (targetTask?.useChildTargetLines) {\n            const childTask = targetTask.children?.find(child => !child.isCancelling());\n            if (!childTask)\n                break;\n            targetTask = childTask;\n        }\n        if (this.targetLinesTask !== targetTask) {\n            this.targetLinesTask = targetTask;\n            this.targetLinesConfig = targetTask?.getTargetLinesConfig?.(this.gameObject);\n        }\n    }\n    [NotifyOwnerChange.onChange](): void {\n        this.clearOrders();\n        this.cancelAllTasks();\n    }\n    [NotifyTeleport.onBeforeTeleport](gameObject: GameObject, fromPos: any, toPos: any, crossRealm: boolean): void {\n        if (toPos && !crossRealm) {\n            this.clearOrders();\n            this.tasks.length = 0;\n        }\n    }\n    addOrder(order: Order, queued = false): void {\n        const addResult = order.onAdd(this.tasks, queued);\n        if (addResult === false) {\n            this.targetLinesTask = undefined;\n            return;\n        }\n        if (!queued) {\n            this.clearOrders();\n            this.tasks = this.tasks.filter(task => task.status !== TaskStatus.NotStarted);\n            this.tasks.forEach(task => task.cancel());\n        }\n        this.orders.push(order);\n        if (queued) {\n            this.queuedOrders.add(order);\n        }\n        this.gameObject.traits\n            .filter(NotifyOrder)\n            .forEach(trait => {\n            trait[NotifyOrder.onPush](this.gameObject, order.orderType);\n        });\n    }\n    clearOrders(): void {\n        this.orders.length = 0;\n        this.queuedOrders.clear();\n        if (this.currentWaypoint && this.waypointPath) {\n            this.cleanupWaypoint(this.currentWaypoint, this.waypointPath);\n        }\n        this.cleanupWaypointPath();\n        this.gameObject.resetGuardModeToIdle();\n    }\n    unmarkNextQueuedOrder(): void {\n        if (this.orders.length > 0) {\n            this.queuedOrders.delete(this.orders[0]);\n        }\n    }\n    hasTasks(): boolean {\n        return this.tasks.length > 0;\n    }\n    isIdle(): boolean {\n        return this.orders.length === 0 && this.tasks.length === 0;\n    }\n    getCurrentTask(): Task | undefined {\n        return this.tasks[0];\n    }\n    cancelAllTasks(): void {\n        this.tasks.forEach(task => task.cancel());\n    }\n    addTask(task: Task): void {\n        this.tasks.push(task);\n    }\n    addTasks(...tasks: Task[]): void {\n        tasks.forEach(task => this.addTask(task));\n    }\n    addTaskToFront(task: Task): void {\n        this.tasks.unshift(task);\n    }\n    addTaskNext(task: Task): void {\n        this.tasks.splice(1, 0, task);\n    }\n    getTasks(): Task[] {\n        return [...this.tasks];\n    }\n    dispose(): void {\n        this.clearOrders();\n        this.tasks.length = 0;\n        this.gameObject = undefined as any;\n    }\n    private cleanupWaypointPath(): void {\n        if (!this.waypointPath)\n            return;\n        const unitIndex = this.waypointPath.units.indexOf(this.gameObject);\n        if (unitIndex !== -1) {\n            this.waypointPath.units.splice(unitIndex, 1);\n        }\n        if (this.waypointPath.units.length === 0) {\n            this.waypointPath.waypoints.forEach(waypoint => {\n                waypoint.next = undefined;\n            });\n            this.waypointPath.waypoints.length = 0;\n        }\n        this.waypointPath = undefined;\n        this.currentWaypoint = undefined;\n    }\n    private cleanupWaypoint(waypoint: Waypoint, waypointPath: WaypointPath): void {\n        const isWaypointInUse = waypointPath.units.find(unit => {\n            if (unit === this.gameObject)\n                return false;\n            const otherCurrentWaypoint = unit.unitOrderTrait.currentWaypoint ??\n                unit.unitOrderTrait.waypointPath?.waypoints[0];\n            return otherCurrentWaypoint === waypoint;\n        });\n        const isReferencedAsNext = waypointPath.waypoints.find(wp => wp.next === waypoint);\n        if (!isWaypointInUse && !isReferencedAsNext) {\n            const waypointIndex = waypointPath.waypoints.indexOf(waypoint);\n            if (waypointIndex === -1) {\n                throw new Error(\"Given waypoint not found in waypoint path\");\n            }\n            waypointPath.waypoints.splice(waypointIndex, 1);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/UnitReloadTrait.ts",
    "content": "import { GameSpeed } from '@/game/GameSpeed';\nimport { ZoneType } from '@/game/gameobject/unit/ZoneType';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class UnitReloadTrait {\n    private cooldownTicks?: number;\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        if (gameObject.dockTrait &&\n            gameObject.dockTrait.hasDockedUnits() &&\n            !gameObject.dockTrait.getDockedUnits().every((unit) => !this.canReloadUnit(unit))) {\n            if (this.cooldownTicks === undefined) {\n                this.cooldownTicks =\n                    GameSpeed.BASE_TICKS_PER_SECOND *\n                        world.rules.general.repair.reloadRate *\n                        60;\n            }\n            if (this.cooldownTicks <= 0) {\n                this.cooldownTicks =\n                    GameSpeed.BASE_TICKS_PER_SECOND *\n                        world.rules.general.repair.reloadRate *\n                        60;\n                const dockedUnits = gameObject.dockTrait.getDockedUnits();\n                const unitsToReload = dockedUnits[0].ammo === 0 ? dockedUnits.slice(0, 1) : dockedUnits;\n                for (const unit of unitsToReload) {\n                    if (this.canReloadUnit(unit)) {\n                        unit.ammoTrait.ammo++;\n                    }\n                }\n            }\n            else {\n                this.cooldownTicks--;\n            }\n        }\n    }\n    canReloadUnit(unit: GameObject): boolean {\n        return !(!unit.ammoTrait ||\n            !unit.rules.manualReload ||\n            unit.ammoTrait.isFull() ||\n            unit.zone === ZoneType.Air);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/UnitRepairTrait.ts",
    "content": "import { UnitRepairFinishEvent } from '@/game/event/UnitRepairFinishEvent';\nimport { UnitRepairStartEvent } from '@/game/event/UnitRepairStartEvent';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { Vector2 } from '@/game/math/Vector2';\nimport { MoveTask } from '@/game/gameobject/task/move/MoveTask';\nimport { ZoneType } from '@/game/gameobject/unit/ZoneType';\nimport { NotifySpawn } from './interface/NotifySpawn';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport enum RepairStatus {\n    Idle = 0,\n    Repairing = 1\n}\nexport class UnitRepairTrait {\n    private status: RepairStatus = RepairStatus.Idle;\n    private cooldownTicks: number = 0;\n    private lastRepairTickSuccessful: boolean = false;\n    [NotifySpawn.onSpawn](gameObject: GameObject, world: World): void {\n        this.resetRallyPoint(gameObject, world);\n    }\n    private resetRallyPoint(gameObject: GameObject, world: World): void {\n        if (!gameObject.factoryTrait) {\n            const rallyPoint = this.computeDefaultRallyPoint(gameObject, world.map);\n            gameObject.rallyTrait.changeRallyPoint(rallyPoint, gameObject, world);\n        }\n    }\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        if (!gameObject.dockTrait || (gameObject.rules.needsEngineer && gameObject.owner.isNeutral)) {\n            return;\n        }\n        if (!gameObject.dockTrait.hasDockedUnits() ||\n            gameObject.dockTrait.getDockedUnits().some(unit => unit.zone === ZoneType.Air) ||\n            (gameObject.poweredTrait && !gameObject.poweredTrait.isPoweredOn())) {\n            this.status = RepairStatus.Idle;\n            return;\n        }\n        if (this.cooldownTicks <= 0) {\n            const repairRules = world.rules.general.repair;\n            const repairRate = gameObject.rules.unitReload ? repairRules.reloadRate : repairRules.uRepairRate;\n            this.cooldownTicks += GameSpeed.BASE_TICKS_PER_SECOND * repairRate * 60;\n            let repairSuccessful = false;\n            for (const unit of gameObject.dockTrait.getDockedUnits()) {\n                if (unit.zone === ZoneType.Air)\n                    continue;\n                if (unit.healthTrait.health < 100 && world.areFriendly(unit, gameObject)) {\n                    if (this.tickRepair(unit, world, gameObject)) {\n                        repairSuccessful = true;\n                    }\n                    if (repairSuccessful &&\n                        this.status === RepairStatus.Idle &&\n                        !this.lastRepairTickSuccessful &&\n                        !gameObject.helipadTrait) {\n                        world.events.dispatch(new UnitRepairStartEvent(unit));\n                    }\n                }\n                else {\n                    const rallyNode = gameObject.rallyTrait.findRallyNodeForUnit(unit, world.map);\n                    if (rallyNode) {\n                        gameObject.dockTrait.undockUnit(unit);\n                        unit.unitOrderTrait.addTask(new MoveTask(world as any, rallyNode.tile, !!rallyNode.onBridge, {\n                            closeEnoughTiles: world.rules.general.closeEnough\n                        }));\n                    }\n                    if (!gameObject.helipadTrait) {\n                        world.events.dispatch(new UnitRepairFinishEvent(unit, gameObject));\n                    }\n                }\n            }\n            this.lastRepairTickSuccessful = repairSuccessful;\n            this.status = repairSuccessful ? RepairStatus.Repairing : RepairStatus.Idle;\n        }\n        else {\n            this.cooldownTicks--;\n        }\n    }\n    private tickRepair(unit: GameObject, world: World, repairBuilding: GameObject): boolean {\n        const repairRules = world.rules.general.repair;\n        const repairStep = Math.floor(repairRules.repairStep);\n        const repairPercent = repairRules.repairPercent;\n        let repairAmount: number;\n        if (repairPercent) {\n            const costPerHP = (repairPercent * unit.purchaseValue) / unit.healthTrait.maxHitPoints;\n            const maxCost = Math.min(unit.owner.credits, Math.max(1, Math.floor(costPerHP * repairStep)));\n            repairAmount = costPerHP && maxCost ? Math.floor(maxCost / costPerHP) : repairStep;\n            if (!maxCost)\n                return false;\n            unit.owner.credits -= maxCost;\n        }\n        else {\n            repairAmount = repairStep;\n        }\n        repairAmount = Math.min(repairAmount, unit.healthTrait.maxHitPoints - unit.healthTrait.getHitPoints());\n        if (!repairAmount)\n            return false;\n        unit.healthTrait.healBy(repairAmount, repairBuilding, world);\n        return true;\n    }\n    private computeDefaultRallyPoint(gameObject: GameObject, map: any): any {\n        const foundation = gameObject.getFoundation();\n        const rallyPos = new Vector2(gameObject.tile.rx, gameObject.tile.ry + foundation.height);\n        return map.tiles.getByMapCoords(rallyPos.x, rallyPos.y) ?? gameObject.tile;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/UnlandableTrait.ts",
    "content": "import { Vector2 } from '@/game/math/Vector2';\nimport { bresenham } from '@/util/bresenham';\nimport { isNotNullOrUndefined } from '@/util/typeGuard';\nimport { MoveTask } from '@/game/gameobject/task/move/MoveTask';\nimport { CallbackTask } from '@/game/gameobject/task/system/CallbackTask';\nimport { TaskGroup } from '@/game/gameobject/task/system/TaskGroup';\nimport { NotifyTick } from './interface/NotifyTick';\nimport { GameObject } from '@/game/gameobject/GameObject';\nimport { World } from '@/game/World';\nexport class UnlandableTrait {\n    private enabled: boolean = true;\n    setEnabled(enabled: boolean): void {\n        this.enabled = enabled;\n    }\n    [NotifyTick.onTick](gameObject: GameObject, world: World): void {\n        if (!this.enabled)\n            return;\n        if (!gameObject.owner.isNeutral &&\n            gameObject.name !== world.rules.general.paradrop.paradropPlane)\n            return;\n        if (!gameObject.unitOrderTrait.isIdle())\n            return;\n        const exitTile = this.chooseExitTile(gameObject.tile, world);\n        gameObject.unitOrderTrait.addTask(new TaskGroup(new MoveTask(world as any, exitTile, false, { allowOutOfBoundsTarget: true }), new CallbackTask((obj) => world.unspawnObject(obj))).setCancellable(false));\n    }\n    private chooseExitTile(tile: any, world: World): any {\n        const mapSize = world.map.tiles.getMapSize();\n        const targetPoint = world.generateRandom() > 0.5\n            ? new Vector2(Math.floor(mapSize.width / 2), 0)\n            : new Vector2(0, Math.floor(mapSize.height / 2));\n        const startPoint = new Vector2(tile.rx, tile.ry);\n        const path = bresenham(startPoint.x, startPoint.y, targetPoint.x, targetPoint.y)\n            .map(point => world.map.tiles.getByMapCoords(point.x, point.y))\n            .filter(isNotNullOrUndefined);\n        if (!path.length) {\n            throw new Error('No valid exit tile found');\n        }\n        return path[path.length - 1];\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/VeteranTrait.ts",
    "content": "import { VeteranLevel } from '@/game/gameobject/unit/VeteranLevel';\nimport { NotifyTargetDestroy } from '@/game/trait/interface/NotifyTargetDestroy';\nimport { UnitPromoteEvent } from '@/game/event/UnitPromoteEvent';\nimport { VeteranAbility } from '@/game/gameobject/unit/VeteranAbility';\nimport { SelfHealingTrait } from './SelfHealingTrait';\nimport { CloakableTrait } from './CloakableTrait';\nimport { ArmedTrait } from './ArmedTrait';\nimport { SensorsTrait } from './SensorsTrait';\ninterface GameObject {\n    rules: {\n        cost: number;\n        veteranAbilities: Set<VeteranAbility>;\n        eliteAbilities: Set<VeteranAbility>;\n        dontScore?: boolean;\n        insignificant?: boolean;\n        organic?: boolean;\n    };\n    traits: { find(type: any): any; add(trait: any): void; getAll(): any[] };\n    armedTrait?: ArmedTrait;\n    cloakableTrait?: CloakableTrait;\n    sensorsTrait?: SensorsTrait;\n    suppressionTrait?: any;\n    unitOrderTrait: any;\n    explodes?: boolean;\n    radarInvisible?: boolean;\n    c4?: boolean;\n    defaultToGuardArea?: boolean;\n    crusher?: boolean;\n    isDestroyed?: boolean;\n    isCrashing?: boolean;\n    veteranLevel: VeteranLevel;\n    isTechno(): boolean;\n    isInfantry(): boolean;\n    resetGuardModeToIdle(): void;\n}\ninterface VeteranRules {\n    veteranRatio: number;\n    veteranCap: VeteranLevel;\n    veteranSpeed: number;\n    veteranArmor: number;\n    veteranCombat: number;\n    veteranROF: number;\n    veteranSight: number;\n}\ninterface GameManager {\n    rules: {\n        general: {\n            cloakDelay: number;\n        };\n    };\n    events: {\n        dispatch(event: UnitPromoteEvent): void;\n    };\n    areFriendly(obj1: GameObject, obj2: GameObject): boolean;\n    addObjectTrait(obj: GameObject, trait: any): void;\n}\ninterface Weapon {\n    warhead: {\n        rules: {\n            temporal?: boolean;\n            parasite?: boolean;\n        };\n    };\n}\nexport class VeteranTrait implements NotifyTargetDestroy {\n    private gameObject: GameObject;\n    private veteranRules: VeteranRules;\n    private veteranLevel: VeteranLevel;\n    private xp: number;\n    private promotionThresh: number;\n    constructor(gameObject: GameObject, veteranRules: VeteranRules) {\n        this.gameObject = gameObject;\n        this.veteranRules = veteranRules;\n        this.veteranLevel = VeteranLevel.None;\n        this.xp = 0;\n        this.promotionThresh = gameObject.rules.cost * veteranRules.veteranRatio + 1;\n    }\n    [NotifyTargetDestroy.onDestroy](source: GameObject, target: GameObject, weapon?: Weapon, gameManager?: GameManager): void {\n        if (source.isDestroyed && !source.isCrashing)\n            return;\n        if (!target.isTechno())\n            return;\n        if (target.rules.dontScore || target.rules.insignificant)\n            return;\n        const isTemporalOrParasiteKill = weapon && (weapon.warhead.rules.temporal ||\n            (weapon.warhead.rules.parasite && source.rules.organic));\n        if (!isTemporalOrParasiteKill && !gameManager?.areFriendly(source, target)) {\n            if (this.veteranLevel >= this.veteranRules.veteranCap)\n                return;\n            const xpGain = target.rules.cost * (target.veteranLevel + 1);\n            if (this.gainXP(xpGain) && gameManager) {\n                this.handlePromotion(source, gameManager);\n            }\n        }\n    }\n    setRelativeXP(percentage: number): void {\n        this.gainXP(Math.floor(percentage * this.promotionThresh));\n    }\n    gainXP(amount: number): boolean {\n        this.xp += amount;\n        if (this.xp >= this.promotionThresh) {\n            const newLevel = Math.min(this.veteranLevel + Math.floor(this.xp / this.promotionThresh), this.veteranRules.veteranCap);\n            const levelIncrease = newLevel - this.veteranLevel;\n            if (levelIncrease > 0) {\n                this.xp -= levelIncrease * this.promotionThresh;\n                this.setVeteranLevel(newLevel);\n                return true;\n            }\n        }\n        return false;\n    }\n    promote(levels: number, gameManager: GameManager): void {\n        const newLevel = Math.min(this.veteranLevel + levels, this.veteranRules.veteranCap);\n        if (newLevel > this.veteranLevel) {\n            this.setVeteranLevel(newLevel);\n            this.handlePromotion(this.gameObject, gameManager);\n        }\n    }\n    isMaxLevel(): boolean {\n        return this.veteranLevel === this.veteranRules.veteranCap;\n    }\n    isElite(): boolean {\n        return this.veteranLevel === VeteranLevel.Elite;\n    }\n    private setVeteranLevel(level: VeteranLevel): void {\n        this.veteranLevel = level;\n        if (this.veteranLevel === VeteranLevel.Elite) {\n            this.gameObject.armedTrait?.toggleEliteWeapons?.(true);\n        }\n    }\n    private handlePromotion(gameObject: GameObject, gameManager: GameManager): void {\n        if (this.hasVeteranAbility(VeteranAbility.SELF_HEAL)) {\n            if (!gameObject.traits.find(SelfHealingTrait)) {\n                gameManager.addObjectTrait(gameObject, new SelfHealingTrait());\n            }\n        }\n        if (this.hasVeteranAbility(VeteranAbility.CLOAK)) {\n            if (!gameObject.cloakableTrait) {\n                gameObject.cloakableTrait = new CloakableTrait(gameObject, gameManager.rules.general.cloakDelay);\n                gameManager.addObjectTrait(gameObject, gameObject.cloakableTrait);\n            }\n        }\n        if (this.hasVeteranAbility(VeteranAbility.EXPLODES)) {\n            if (!gameObject.explodes) {\n                gameObject.explodes = true;\n                if (!gameObject.armedTrait) {\n                    gameObject.armedTrait = new ArmedTrait(gameObject as any, gameManager.rules as any);\n                    gameManager.addObjectTrait(gameObject, gameObject.armedTrait);\n                }\n            }\n        }\n        if (this.hasVeteranAbility(VeteranAbility.RADAR_INVISIBLE)) {\n            if (!gameObject.radarInvisible) {\n                gameObject.radarInvisible = true;\n            }\n        }\n        if (this.hasVeteranAbility(VeteranAbility.SENSORS)) {\n            if (!gameObject.sensorsTrait) {\n                gameObject.sensorsTrait = new SensorsTrait();\n                gameManager.addObjectTrait(gameObject, gameObject.sensorsTrait);\n            }\n        }\n        if (gameObject.isInfantry() && this.hasVeteranAbility(VeteranAbility.FEARLESS)) {\n            gameObject.suppressionTrait?.disable?.();\n        }\n        if (this.hasVeteranAbility(VeteranAbility.C4)) {\n            if (!gameObject.c4) {\n                gameObject.c4 = true;\n            }\n        }\n        if (this.hasVeteranAbility(VeteranAbility.GUARD_AREA)) {\n            if (!gameObject.defaultToGuardArea) {\n                gameObject.defaultToGuardArea = true;\n                if (gameObject.unitOrderTrait.isIdle?.()) {\n                    gameObject.resetGuardModeToIdle();\n                }\n            }\n        }\n        if (this.hasVeteranAbility(VeteranAbility.CRUSHER)) {\n            if (!gameObject.crusher) {\n                gameObject.crusher = true;\n            }\n        }\n        gameManager.events.dispatch(new UnitPromoteEvent(gameObject));\n    }\n    getVeteranSightMultiplier(): number {\n        return this.getVeteranAbilityMultiplier(VeteranAbility.SIGHT);\n    }\n    getVeteranSpeedMultiplier(): number {\n        return this.getVeteranAbilityMultiplier(VeteranAbility.FASTER);\n    }\n    getVeteranArmorMultiplier(): number {\n        return this.getVeteranAbilityMultiplier(VeteranAbility.STRONGER);\n    }\n    getVeteranDamageMultiplier(): number {\n        return this.getVeteranAbilityMultiplier(VeteranAbility.FIREPOWER);\n    }\n    getVeteranRofMultiplier(): number {\n        return this.getVeteranAbilityMultiplier(VeteranAbility.ROF);\n    }\n    hasVeteranAbility(ability: VeteranAbility): boolean {\n        return ((this.veteranLevel === VeteranLevel.Veteran &&\n            this.gameObject.rules.veteranAbilities.has(ability)) ||\n            (this.veteranLevel >= VeteranLevel.Elite &&\n                this.gameObject.rules.eliteAbilities.has(ability)));\n    }\n    private getVeteranAbilityMultiplier(ability: VeteranAbility): number {\n        let multiplier = 1;\n        if ((this.veteranLevel === VeteranLevel.Veteran &&\n            this.gameObject.rules.veteranAbilities.has(ability)) ||\n            (this.veteranLevel >= VeteranLevel.Elite &&\n                this.gameObject.rules.eliteAbilities.has(ability))) {\n            multiplier = this.getVeteranRulesMultiplier(ability);\n        }\n        return multiplier;\n    }\n    private getVeteranRulesMultiplier(ability: VeteranAbility): number {\n        switch (ability) {\n            case VeteranAbility.FASTER:\n                return this.veteranRules.veteranSpeed;\n            case VeteranAbility.STRONGER:\n                return this.veteranRules.veteranArmor;\n            case VeteranAbility.FIREPOWER:\n                return this.veteranRules.veteranCombat;\n            case VeteranAbility.ROF:\n                return this.veteranRules.veteranROF;\n            case VeteranAbility.SIGHT:\n                return this.veteranRules.veteranSight;\n            default:\n                throw new Error(`Unhandled VeteranAbility: ${ability}`);\n        }\n    }\n    dispose(): void {\n        this.gameObject = undefined as any;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/WallTrait.ts",
    "content": "import { NotifyDamage } from \"@/game/gameobject/trait/interface/NotifyDamage\";\nimport { LandType } from \"@/game/type/LandType\";\nimport { NotifySpawn } from \"@/game/gameobject/trait/interface/NotifySpawn\";\nimport { NotifyUnspawn } from \"@/game/gameobject/trait/interface/NotifyUnspawn\";\nimport { CardinalTileFinder } from \"@/game/map/tileFinder/CardinalTileFinder\";\nimport { wallTypes } from \"@/game/map/wallTypes\";\nexport class WallTrait implements NotifyDamage, NotifySpawn, NotifyUnspawn {\n    private linkedDamageHandled: boolean = false;\n    private wallType: number = 0;\n    [NotifySpawn.onSpawn](gameObject: any, context: any): void {\n        if (gameObject.isBuilding()) {\n            this.connectWall(gameObject, context.map);\n        }\n        else {\n            this.wallType = gameObject.value;\n        }\n    }\n    [NotifyUnspawn.onUnspawn](gameObject: any, context: any): void {\n        this.updateAdjacentWalls(gameObject, context.map);\n    }\n    [NotifyDamage.onDamage](gameObject: any, context: any, damage: number, source: any): void {\n        if (!this.linkedDamageHandled) {\n            const linkedDamage = Math.floor(damage / 2);\n            if (linkedDamage) {\n                for (const tile of context.map.tiles.getAllNeighbourTiles(gameObject.tile)) {\n                    if (tile.landType === LandType.Wall) {\n                        const wall = context.map.getObjectsOnTile(tile).find((obj: any) => (obj.isBuilding() || obj.isOverlay()) && obj.wallTrait);\n                        if (wall) {\n                            wall.wallTrait.linkedDamageHandled = true;\n                            wall.healthTrait.inflictDamage(linkedDamage, source, context);\n                            wall.wallTrait.linkedDamageHandled = false;\n                            if (!wall.healthTrait.health) {\n                                context.destroyObject(wall, source);\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n    private updateAdjacentWalls(gameObject: any, map: any): void {\n        const finder = new CardinalTileFinder(map.tiles, map.mapBounds, gameObject.tile, 1, 1);\n        finder.diagonal = false;\n        let tile;\n        while ((tile = finder.getNextTile())) {\n            const wall = map.getObjectsOnTile(tile).find((obj: any) => (obj.isBuilding() || obj.isOverlay()) && obj.name === gameObject.rules.name);\n            if (wall) {\n                this.connectWall(wall, map);\n            }\n        }\n    }\n    private connectWall(wall: any, map: any): void {\n        const adjacentData = this.getAdjacentWallData(wall.tile, wall.name, map);\n        this.updateWallType(wall, adjacentData.map(data => data.direction));\n        adjacentData.forEach(data => {\n            const adjacentWallData = this.getAdjacentWallData(data.tile, data.wall.name, map);\n            this.updateWallType(data.wall, adjacentWallData.map(data => data.direction));\n        });\n    }\n    private updateWallType(wall: any, directions: number[][]): void {\n        const connections = [0, 0, 0, 0];\n        for (const dir of directions) {\n            if (dir[0] === 0 && dir[1] === -1)\n                connections[0] = 1;\n            if (dir[0] === 1 && dir[1] === 0)\n                connections[1] = 1;\n            if (dir[0] === 0 && dir[1] === 1)\n                connections[2] = 1;\n            if (dir[0] === -1 && dir[1] === 0)\n                connections[3] = 1;\n        }\n        const wallType = this.findWallType(connections);\n        wall.wallTrait.wallType = wallType;\n        if (wall.isOverlay()) {\n            wall.value = wallType;\n        }\n    }\n    private findWallType(connections: number[]): number {\n        for (let i = 0; i < wallTypes.length; ++i) {\n            const type = wallTypes[i];\n            if (type[0] === connections[0] &&\n                type[1] === connections[1] &&\n                type[2] === connections[2] &&\n                type[3] === connections[3]) {\n                return i;\n            }\n        }\n        console.warn(\"Invalid wall directions\", connections);\n        return 0;\n    }\n    private getAdjacentWallData(tile: any, wallName: string, map: any): Array<{\n        direction: number[];\n        tile: any;\n        wall: any;\n    }> {\n        const adjacentWalls = [];\n        const directions = [\n            [0, 1],\n            [0, -1],\n            [1, 0],\n            [-1, 0]\n        ];\n        for (const dir of directions) {\n            const coords = { x: tile.rx + dir[0], y: tile.ry + dir[1] };\n            const adjacentTile = map.tiles.getByMapCoords(coords.x, coords.y);\n            if (adjacentTile) {\n                const wall = map.getObjectsOnTile(adjacentTile).find((obj: any) => (obj.isBuilding() || obj.isOverlay()) && obj.name === wallName);\n                if (wall) {\n                    adjacentWalls.push({\n                        direction: dir,\n                        tile: adjacentTile,\n                        wall: wall\n                    });\n                }\n            }\n        }\n        return adjacentWalls;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/WarpedOutTrait.ts",
    "content": "import { NotifyWarpChange } from \"@/game/gameobject/trait/interface/NotifyWarpChange\";\nimport { NotifyTick } from \"@/game/gameobject/trait/interface/NotifyTick\";\nexport class WarpedOutTrait implements NotifyTick {\n    private gameObject: any;\n    private ticksWhenWarpedOut: boolean = true;\n    private remainingTicks: number = 0;\n    private invulnerable: boolean = false;\n    constructor(gameObject: any) {\n        this.gameObject = gameObject;\n    }\n    isActive(): boolean {\n        return this.remainingTicks > 0;\n    }\n    setActive(active: boolean, invulnerable: boolean, context: any): void {\n        this.remainingTicks = active ? Number.POSITIVE_INFINITY : 0;\n        this.invulnerable = invulnerable;\n        this.notifyChange(active, context);\n    }\n    setTimed(ticks: number, invulnerable: boolean, context: any): void {\n        this.remainingTicks = ticks;\n        this.invulnerable = invulnerable;\n        this.notifyChange(true, context);\n    }\n    debugSetActive(active: boolean): void {\n        this.remainingTicks = active ? Number.POSITIVE_INFINITY : 0;\n    }\n    private notifyChange(isWarpedOut: boolean, context: any): void {\n        context.traits\n            .filter(NotifyWarpChange)\n            .forEach(trait => {\n            trait[NotifyWarpChange.onChange](this.gameObject, context, isWarpedOut);\n        });\n        this.gameObject.traits\n            .filter(NotifyWarpChange)\n            .forEach(trait => {\n            trait[NotifyWarpChange.onChange](this.gameObject, context, isWarpedOut);\n        });\n    }\n    expire(context: any): void {\n        this.remainingTicks = 0;\n        this.notifyChange(false, context);\n    }\n    isInvulnerable(): boolean {\n        return this.isActive() && this.invulnerable;\n    }\n    [NotifyTick.onTick](gameObject: any, context: any): void {\n        if (this.remainingTicks > 0) {\n            this.remainingTicks--;\n            if (this.remainingTicks <= 0) {\n                this.notifyChange(false, context);\n            }\n        }\n    }\n    dispose(): void {\n        this.gameObject = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyAttack.ts",
    "content": "export const NotifyAttack = {\n    onAttack: Symbol()\n};\nexport interface NotifyAttack {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyBuildStatus.ts",
    "content": "export interface NotifyBuildStatus {\n    [key: symbol]: (...args: any[]) => void;\n}\nexport const NotifyBuildStatus = {\n    onStatusChange: Symbol()\n};\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyCrash.ts",
    "content": "export const NotifyCrash = {\n    onCrash: Symbol()\n};\nexport interface NotifyCrash {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyDamage.ts",
    "content": "export const NotifyDamage = {\n    onDamage: Symbol()\n};\nexport interface NotifyDamage {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyDestroy.ts",
    "content": "export const NotifyDestroy = {\n    onDestroy: Symbol('onDestroy')\n};\nexport interface NotifyDestroy {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyHeal.ts",
    "content": "export const NotifyHeal = {\n    onHeal: Symbol()\n};\nexport interface NotifyHeal {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyHealthChange.ts",
    "content": "export const NotifyHealthChange = {\n    onChange: Symbol()\n};\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyOrder.ts",
    "content": "export const NotifyOrder = {\n    onPush: Symbol()\n};\nexport interface NotifyOrder {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyOwnerChange.ts",
    "content": "export const NotifyOwnerChange = {\n    onChange: Symbol('onChange')\n};\nexport interface NotifyOwnerChange {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifySell.ts",
    "content": "export const NotifySell = {\n    onSell: Symbol()\n};\nexport interface NotifySell {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifySpawn.ts",
    "content": "export const NotifySpawn = {\n    onSpawn: Symbol()\n};\nexport interface NotifySpawn {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyTeleport.ts",
    "content": "export const NotifyTeleport = {\n    onBeforeTeleport: Symbol()\n};\nexport interface NotifyTeleport {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyTick.ts",
    "content": "export const NotifyTick = {\n    onTick: Symbol()\n};\nexport interface NotifyTick {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyTileChange.ts",
    "content": "export const NotifyTileChange = {\n    onTileChange: Symbol()\n};\nexport interface NotifyTileChange {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyUnspawn.ts",
    "content": "export const NotifyUnspawn = {\n    onUnspawn: Symbol()\n};\nexport interface NotifyUnspawn {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/gameobject/trait/interface/NotifyWarpChange.ts",
    "content": "export interface NotifyWarpChange {\n    [key: symbol]: (...args: any[]) => void;\n}\nexport const NotifyWarpChange = {\n    onChange: Symbol()\n};\n"
  },
  {
    "path": "src/game/gameobject/unit/CollisionHelper.ts",
    "content": "import { TerrainType } from '@/engine/type/TerrainType';\nimport { LandType } from '@/game/type/LandType';\nimport { CollisionType } from '@/game/gameobject/unit/CollisionType';\nimport { ZoneType } from '@/game/gameobject/unit/ZoneType';\ninterface TileOccupation {\n    getObjectsOnTile(tile: any): any[];\n    getBridgeOnTile(tile: any): any;\n}\ninterface CollisionOptions {\n    walls?: boolean;\n    units?: (owner: any) => boolean;\n    shore?: boolean;\n    ground?: boolean;\n    cliffs?: boolean;\n}\ninterface CollisionResult {\n    type: CollisionType;\n    target?: any;\n}\nexport class CollisionHelper {\n    private tileOccupation: TileOccupation;\n    constructor(tileOccupation: TileOccupation) {\n        this.tileOccupation = tileOccupation;\n    }\n    checkCollisions(source: any, target: any, options: CollisionOptions): CollisionResult {\n        const sourceTile = source.tile;\n        let bridge: any, unit: any, wall: any;\n        for (const obj of this.tileOccupation.getObjectsOnTile(sourceTile)) {\n            if (obj.isOverlay() && obj.isBridge())\n                bridge = obj;\n            if (obj.isOverlay() && obj.wallTrait)\n                wall = obj;\n            if (obj.isTechno() && !obj.isDestroyed)\n                unit = obj;\n        }\n        if (options.walls) {\n            if (source.tileElevation <= 2 && sourceTile.landType === LandType.Wall) {\n                return { type: CollisionType.Wall, target: wall };\n            }\n            if (options.units &&\n                unit?.tile === sourceTile &&\n                (!unit.isUnit() || unit.zone === ZoneType.Ground) &&\n                source.tileElevation <= 1.1 &&\n                options.units(unit.owner)) {\n                return { type: CollisionType.Wall, target: unit };\n            }\n        }\n        if (options.shore && sourceTile.landType !== LandType.Water) {\n            return { type: CollisionType.Shore };\n        }\n        if (options.ground && source.tileElevation < 0) {\n            return { type: CollisionType.Ground };\n        }\n        const sourceHeight = source.tileElevation + sourceTile.z;\n        const targetHeight = target.tileElevation + target.tile.z;\n        if (bridge?.isHighBridge()) {\n            const bridgeHeight = bridge.tile.z + bridge.tileElevation;\n            if ((bridgeHeight < targetHeight && sourceHeight <= bridgeHeight) ||\n                (targetHeight < bridgeHeight && bridgeHeight - 1 <= sourceHeight)) {\n                return targetHeight < bridgeHeight\n                    ? { type: CollisionType.UnderBridge, target: bridge }\n                    : { type: CollisionType.OnBridge, target: bridge };\n            }\n        }\n        else if (bridge?.isLowBridge() && options.shore) {\n            return { type: CollisionType.UnderBridge, target: bridge };\n        }\n        if (options.cliffs) {\n            const heightDiff = sourceTile.z - target.tile.z;\n            if (source.tileElevation < 0 && heightDiff >= 4) {\n                return { type: CollisionType.Cliff };\n            }\n        }\n        return { type: CollisionType.None };\n    }\n    computeDetonationZone(tile: any, height: number, collisionType: CollisionType): ZoneType {\n        const bridge = this.tileOccupation.getBridgeOnTile(tile);\n        if (collisionType === CollisionType.None && height > 1.5 + (bridge?.tileElevation ?? 0)) {\n            return ZoneType.Air;\n        }\n        if ((bridge && height > 1.5) ||\n            tile.terrainType !== TerrainType.Water ||\n            bridge?.isLowBridge()) {\n            return ZoneType.Ground;\n        }\n        return ZoneType.Water;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/CollisionType.ts",
    "content": "export enum CollisionType {\n    None = 0,\n    Ground = 1,\n    Wall = 2,\n    Cliff = 3,\n    OnBridge = 4,\n    UnderBridge = 5,\n    Shore = 6\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/CrateBonuses.ts",
    "content": "export class CrateBonuses {\n    firepower: number;\n    armor: number;\n    speed: number;\n    constructor() {\n        this.firepower = 1;\n        this.armor = 1;\n        this.speed = 1;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/FacingUtil.ts",
    "content": "import { Vector2 } from '@/game/math/Vector2';\nimport * as geometry from '@/game/math/geometry';\nexport class FacingUtil {\n    static tick(currentFacing: number, targetFacing: number, turnRate: number): {\n        facing: number;\n        delta: number;\n    } {\n        if (currentFacing === targetFacing) {\n            return { facing: currentFacing, delta: 0 };\n        }\n        const clockwiseDelta = (currentFacing - targetFacing + 360) % 360;\n        const counterClockwiseDelta = (targetFacing - currentFacing + 360) % 360;\n        if (Math.min(clockwiseDelta, counterClockwiseDelta) < turnRate) {\n            return { facing: targetFacing, delta: 0 };\n        }\n        const delta = (counterClockwiseDelta <= clockwiseDelta ? 1 : -1) * turnRate;\n        return { facing: (currentFacing + delta + 360) % 360, delta };\n    }\n    static fromMapCoords(vector: Vector2): number {\n        return (-geometry.angleDegFromVec2(vector) - 90 + 720) % 360;\n    }\n    static toMapCoords(angle: number): Vector2 {\n        return geometry\n            .rotateVec2(new Vector2(1000, 0), FacingUtil.toWorldDeg(angle))\n            .round()\n            .normalize();\n    }\n    static toWorldDeg(angle: number): number {\n        return -(angle + 90);\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/HealthLevel.ts",
    "content": "export enum HealthLevel {\n    Green = 0,\n    Yellow = 1,\n    Red = 2\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/LosHelper.ts",
    "content": "import { bresenham } from '@/util/bresenham';\nimport { LandType } from '@/game/type/LandType';\ninterface TileOccupation {\n    getBridgeOnTile(tile: any): any;\n}\ninterface Tiles {\n    getByMapCoords(x: number, y: number): any;\n}\ninterface GameObject {\n    position?: any;\n    tile: any;\n    z: number;\n    rx: number;\n    ry: number;\n    isUnit(): boolean;\n    isBuilding(): boolean;\n    onBridge?: boolean;\n    centerTile?: any;\n}\ninterface WeaponRules {\n    warhead: {\n        rules: {\n            wall: boolean;\n        };\n    };\n    projectileRules: {\n        subjectToWalls: boolean;\n        subjectToCliffs: boolean;\n    };\n    rules: {\n        spawner: boolean;\n    };\n}\nexport class LosHelper {\n    private tiles: Tiles;\n    private tileOccupation: TileOccupation;\n    constructor(tiles: Tiles, tileOccupation: TileOccupation) {\n        this.tiles = tiles;\n        this.tileOccupation = tileOccupation;\n    }\n    hasLineOfSight(source: GameObject | any, target: GameObject | any, weapon: WeaponRules): boolean {\n        const ignoreWalls = weapon.warhead.rules.wall || !weapon.projectileRules.subjectToWalls;\n        const checkCliffs = weapon.projectileRules.subjectToCliffs;\n        const isSpawner = weapon.rules.spawner;\n        let cliffCount = 0;\n        let wasCliff = false;\n        if (!ignoreWalls || checkCliffs || isSpawner) {\n            const sourceTile = this.hasPosition(source) ? source.tile : source;\n            const targetTile = this.hasPosition(target)\n                ? (target.isBuilding() ? target.centerTile : target.tile)\n                : target;\n            let sourceZ = sourceTile.z;\n            if (checkCliffs && this.hasPosition(source) && source.isUnit() && source.onBridge) {\n                sourceZ += this.tileOccupation.getBridgeOnTile(sourceTile)?.tileElevation ?? 0;\n            }\n            for (const { x, y } of bresenham(sourceTile.rx, sourceTile.ry, targetTile.rx, targetTile.ry)) {\n                const tile = this.tiles.getByMapCoords(x, y);\n                if (!tile)\n                    return false;\n                if (!ignoreWalls && tile.landType === LandType.Wall)\n                    return false;\n                if (checkCliffs) {\n                    if (tile.landType === LandType.Cliff) {\n                        if (tile.z > sourceZ)\n                            return false;\n                        wasCliff = true;\n                    }\n                    else {\n                        if (tile.z > sourceZ && wasCliff)\n                            return false;\n                        wasCliff = false;\n                    }\n                }\n                if (isSpawner && cliffCount < 2 && this.tileOccupation.getBridgeOnTile(tile)?.isHighBridge()) {\n                    return false;\n                }\n                cliffCount++;\n            }\n        }\n        return true;\n    }\n    private hasPosition(obj: any): obj is GameObject {\n        return obj.position !== undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/MovePositionHelper.ts",
    "content": "import { RadialTileFinder } from '@/game/map/tileFinder/RadialTileFinder';\nimport { MovementZone } from '@/game/type/MovementZone';\nimport { SpeedType } from '@/game/type/SpeedType';\ninterface GameObject {\n    tile: Tile;\n    rules: {\n        movementZone: MovementZone;\n        airportBound?: boolean;\n        balloonHover?: boolean;\n        hoverAttack?: boolean;\n    };\n    isInfantry(): boolean;\n}\ninterface Tile {\n    rx: number;\n    ry: number;\n    z: number;\n    onBridgeLandType?: boolean;\n}\ninterface Bridge {\n    isHighBridge(): boolean;\n    tileElevation?: number;\n}\ninterface GameMap {\n    tiles: {\n        getByMapCoords(rx: number, ry: number): Tile | undefined;\n    };\n    mapBounds: {\n        isWithinBounds(tile: Tile): boolean;\n    };\n    tileOccupation: {\n        getBridgeOnTile(tile: Tile): Bridge | undefined;\n    };\n    terrain: {\n        getPassableSpeed(tile: Tile, speedType: SpeedType, param3: boolean, param4: boolean): boolean;\n    };\n}\ninterface Cluster {\n    objects: Set<GameObject>;\n}\nexport class MovePositionHelper {\n    private map: GameMap;\n    constructor(map: GameMap) {\n        this.map = map;\n    }\n    findPositions(objects: GameObject[], targetTile: Tile, sourceBridge: Bridge | undefined, isSpecialCondition: boolean): Map<GameObject, Tile> {\n        const tileAssignments = new Map<Tile, GameObject[]>();\n        const clusters = this.clusterObjects(objects);\n        if (!clusters.length) {\n            throw new Error(\"We should have found at least one cluster\");\n        }\n        const largestCluster = clusters.reduce((largest, current) => current.objects.size > largest.objects.size ? current : largest, clusters[0]);\n        clusters.splice(clusters.indexOf(largestCluster), 1);\n        const unplacedObjects: GameObject[] = [];\n        const centerTile = this.findCenterTile([...largestCluster.objects]);\n        largestCluster.objects.forEach(obj => {\n            const candidateTile = this.map.tiles.getByMapCoords(targetTile.rx + obj.tile.rx - centerTile.rx, targetTile.ry + obj.tile.ry - centerTile.ry);\n            const bridge = candidateTile?.onBridgeLandType\n                ? this.map.tileOccupation.getBridgeOnTile(candidateTile)\n                : undefined;\n            if (!candidateTile ||\n                !this.map.mapBounds.isWithinBounds(candidateTile) ||\n                (tileAssignments.has(candidateTile) && !this.tileHasRoom(obj, tileAssignments.get(candidateTile)!)) ||\n                (obj.rules.movementZone === MovementZone.Fly &&\n                    !(obj.rules.airportBound || (isSpecialCondition && obj.rules.balloonHover && !obj.rules.hoverAttack)) &&\n                    !this.map.terrain.getPassableSpeed(candidateTile, SpeedType.Amphibious, false, !!bridge)) ||\n                (obj.rules.movementZone !== MovementZone.Fly &&\n                    !this.isEligibleTile(candidateTile, bridge, sourceBridge, targetTile))) {\n                unplacedObjects.push(obj);\n            }\n            else {\n                let assignedObjects = tileAssignments.get(candidateTile);\n                if (!assignedObjects) {\n                    assignedObjects = [];\n                    tileAssignments.set(candidateTile, assignedObjects);\n                }\n                assignedObjects.push(obj);\n            }\n        });\n        clusters.forEach(cluster => {\n            unplacedObjects.push(...cluster.objects);\n        });\n        const tileFinder = new RadialTileFinder(this.map.tiles as any, this.map.mapBounds as any, targetTile as any, { width: 1, height: 1 }, 1, 5, () => true);\n        let nextTile: Tile | undefined;\n        while (unplacedObjects.length && (nextTile = tileFinder.getNextTile() as any)) {\n            const obj = unplacedObjects[0];\n            const bridge = this.map.tileOccupation.getBridgeOnTile(nextTile);\n            if ((!tileAssignments.has(nextTile) || this.tileHasRoom(obj, tileAssignments.get(nextTile)!)) &&\n                (obj.rules.movementZone !== MovementZone.Fly ||\n                    obj.rules.airportBound ||\n                    this.map.terrain.getPassableSpeed(nextTile, SpeedType.Amphibious, false, !!bridge)) &&\n                (obj.rules.movementZone === MovementZone.Fly ||\n                    this.isEligibleTile(nextTile, bridge, sourceBridge, targetTile))) {\n                let assignedObjects = tileAssignments.get(nextTile);\n                if (!assignedObjects) {\n                    assignedObjects = [];\n                    tileAssignments.set(nextTile, assignedObjects);\n                }\n                assignedObjects.push(unplacedObjects.shift()!);\n            }\n        }\n        const result = new Map<GameObject, Tile>();\n        tileAssignments.forEach((objects, tile) => {\n            objects.forEach(obj => result.set(obj, tile));\n        });\n        unplacedObjects.forEach(obj => result.set(obj, targetTile));\n        if (result.size !== objects.length) {\n            throw new Error(\"We should have computed a number of positions equal to the number of input objects\");\n        }\n        return result;\n    }\n    private tileHasRoom(obj: GameObject, existingObjects: GameObject[]): boolean {\n        if (obj.isInfantry()) {\n            if (existingObjects.find(existing => !existing.isInfantry())) {\n                return false;\n            }\n            const maxInfantry = obj.rules.movementZone === MovementZone.Fly ? 1 : 3;\n            return existingObjects.filter(existing => existing.isInfantry()).length < maxInfantry;\n        }\n        return !existingObjects.length;\n    }\n    public isEligibleTile(tile: Tile, tileBridge: Bridge | undefined, sourceBridge: Bridge | undefined, targetTile: Tile): boolean {\n        if (sourceBridge?.isHighBridge() || tileBridge?.isHighBridge()) {\n            return (tile.z + (tileBridge?.tileElevation ?? 0) ===\n                targetTile.z + (sourceBridge?.tileElevation ?? 0));\n        }\n        return (!sourceBridge && !tileBridge) || Math.abs(tile.z - targetTile.z) < 2;\n    }\n    private clusterObjects(objects: GameObject[]): Cluster[] {\n        const tileGroups = new Map<string, GameObject[]>();\n        objects.forEach(obj => {\n            const key = `${obj.tile.rx}_${obj.tile.ry}`;\n            tileGroups.set(key, [...(tileGroups.get(key) || []), obj]);\n        });\n        const clusters: Cluster[] = [];\n        const remaining = new Set(objects);\n        while (remaining.size) {\n            const cluster = new Set<GameObject>();\n            const queue: GameObject[] = [];\n            const startTile = [...remaining][0].tile;\n            tileGroups.get(`${startTile.rx}_${startTile.ry}`)!.forEach(obj => {\n                queue.push(obj);\n            });\n            while (queue.length) {\n                const obj = queue.shift()!;\n                cluster.add(obj);\n                remaining.delete(obj);\n                for (let dx = -1; dx <= 1; dx++) {\n                    for (let dy = -1; dy <= 1; dy++) {\n                        if (dx || dy) {\n                            const adjacentObjects = tileGroups.get(`${obj.tile.rx + dx}_${obj.tile.ry + dy}`);\n                            if (adjacentObjects?.length) {\n                                adjacentObjects.forEach(adjacent => {\n                                    if (remaining.has(adjacent)) {\n                                        remaining.delete(adjacent);\n                                        queue.push(adjacent);\n                                    }\n                                });\n                            }\n                        }\n                    }\n                }\n            }\n            clusters.push({ objects: cluster });\n        }\n        return clusters;\n    }\n    private findCenterTile(objects: GameObject[]): Tile {\n        let totalRx = 0;\n        let totalRy = 0;\n        objects.forEach(obj => {\n            totalRx += obj.tile.rx;\n            totalRy += obj.tile.ry;\n        });\n        const centerRx = Math.round(totalRx / objects.length);\n        const centerRy = Math.round(totalRy / objects.length);\n        let centerTile = this.map.tiles.getByMapCoords(centerRx, centerRy);\n        if (!centerTile) {\n            centerTile = objects.find(obj => Math.abs(obj.tile.rx - centerRx) <= 1 &&\n                Math.abs(obj.tile.ry - centerRy) <= 1)?.tile;\n            if (!centerTile) {\n                throw new Error(\"At least one adjacent object should have been found\");\n            }\n        }\n        return centerTile;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/RangeHelper.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport * as MathUtil from '@/util/math';\nimport { ZoneType } from './ZoneType';\nimport { MovementZone } from '@/game/type/MovementZone';\nimport { Vector2 } from '../../math/Vector2';\ninterface Position {\n    worldPosition: Vector3;\n    tileElevation: number;\n}\ninterface GameObject {\n    position: Position;\n    tile: Tile;\n    isUnit(): boolean;\n    isBuilding(): boolean;\n    isTechno(): boolean;\n    rules: GameObjectRules;\n    zone?: ZoneType;\n    getFoundation(): Foundation;\n}\ninterface GameObjectRules {\n    movementZone: MovementZone;\n    airRangeBonus: number;\n}\ninterface Tile {\n    z: number;\n    rx: number;\n    ry: number;\n}\ninterface Vector3 {\n    x: number;\n    y: number;\n    z: number;\n    distanceTo(other: Vector3): number;\n}\ninterface Vector3Like {\n    x: number;\n    z: number;\n    addScalar?: (scalar: number) => void;\n}\ninterface TileCoord {\n    rx: number;\n    ry: number;\n    z: number;\n}\ninterface Weapon {\n    minRange: number;\n    range: number;\n    rules: WeaponRules;\n    projectileRules: ProjectileRules;\n    warhead: Warhead;\n}\ninterface WeaponRules {\n    limboLaunch: boolean;\n    cellRangefinding: boolean;\n}\ninterface ProjectileRules {\n    arcing: boolean;\n    vertical: boolean;\n    subjectToElevation: boolean;\n    isAntiAir: boolean;\n}\ninterface Warhead {\n    rules: WarheadRules;\n}\ninterface WarheadRules {\n    ivanBomb: boolean;\n}\ninterface Foundation {\n    width: number;\n    height: number;\n}\ninterface GameRules {\n    elevationModel: ElevationModel;\n}\ninterface ElevationModel {\n    getBonus(fromElevation: number, toElevation: number): number;\n}\ninterface TileOccupation {\n    calculateTilesForGameObject(tile: Tile, gameObject: GameObject): Tile[];\n}\ntype RangeTarget = GameObject | Vector3Like | TileCoord | Tile[];\nconst hasPosition = (obj: any): obj is GameObject => obj.position !== undefined;\nconst hasAddScalar = (obj: any): obj is Vector3Like => obj.addScalar !== undefined;\nexport class RangeHelper {\n    private tileOccupation: TileOccupation;\n    constructor(tileOccupation: TileOccupation) {\n        this.tileOccupation = tileOccupation;\n    }\n    isInWeaponRange(shooter: GameObject, target: RangeTarget, weapon: Weapon, gameRules: GameRules, rangeSource?: GameObject): boolean {\n        const effectiveShooter = rangeSource ?? shooter;\n        if (weapon.rules.limboLaunch) {\n            const shooterElevation = hasPosition(effectiveShooter)\n                ? effectiveShooter.position.tileElevation + effectiveShooter.tile.z\n                : (effectiveShooter as TileCoord).z;\n            const targetElevation = hasPosition(target)\n                ? target.position.tileElevation + target.tile.z\n                : (target as TileCoord).z;\n            if (Math.abs(shooterElevation - targetElevation) > 2) {\n                return false;\n            }\n        }\n        const { minRange, range } = this.computeWeaponRangeVsTarget(effectiveShooter, target, weapon, gameRules);\n        if (weapon.rules.cellRangefinding) {\n            return this.isInTileRange(effectiveShooter, target, minRange, range);\n        }\n        else if (shooter.isUnit() && shooter.rules.movementZone === MovementZone.Fly) {\n            return this.isInRange2(effectiveShooter, target, minRange, range);\n        }\n        else {\n            return this.isInRange3(effectiveShooter, target, minRange, range);\n        }\n    }\n    computeWeaponRangeVsTarget(shooter: RangeTarget, target: RangeTarget, weapon: Weapon, gameRules: GameRules): {\n        minRange: number;\n        range: number;\n    } {\n        let rangeBonus = 0;\n        if (hasPosition(target) && target.isBuilding() &&\n            !weapon.projectileRules.arcing &&\n            !weapon.projectileRules.vertical &&\n            !weapon.warhead.rules.ivanBomb) {\n            const foundation = target.getFoundation();\n            if (foundation.width > 1 && foundation.height > 1) {\n                rangeBonus += Math.ceil(Math.min(foundation.width, foundation.height) / 2);\n            }\n        }\n        if (weapon.projectileRules.subjectToElevation &&\n            !(weapon.projectileRules.arcing && !hasPosition(target))) {\n            const shooterElevation = hasPosition(shooter)\n                ? shooter.tile.z + shooter.position.tileElevation\n                : (shooter as TileCoord).z;\n            const targetElevation = hasPosition(target)\n                ? target.tile.z + target.position.tileElevation\n                : (target as TileCoord).z;\n            if (targetElevation < shooterElevation) {\n                rangeBonus += gameRules.elevationModel.getBonus(shooterElevation, targetElevation);\n            }\n        }\n        if (weapon.projectileRules.isAntiAir &&\n            hasPosition(shooter) && shooter.isTechno() &&\n            hasPosition(target) && target.isUnit() &&\n            target.zone === ZoneType.Air) {\n            rangeBonus += shooter.rules.airRangeBonus;\n        }\n        return {\n            minRange: weapon.minRange,\n            range: weapon.range + rangeBonus\n        };\n    }\n    isInRange(source: RangeTarget, target: RangeTarget, minRange: number, maxRange: number, useTileRange = false): boolean {\n        if (useTileRange) {\n            return this.isInTileRange(source, target, minRange, maxRange);\n        }\n        else if (hasPosition(source) && source.isUnit() &&\n            source.rules.movementZone === MovementZone.Fly) {\n            return this.isInRange2(source, target, minRange, maxRange);\n        }\n        else {\n            return this.isInRange3(source, target, minRange, maxRange);\n        }\n    }\n    public isInRange3(source: RangeTarget, target: RangeTarget, minRange: number, maxRange: number): boolean {\n        const distance = this.distance3(source, target) / Coords.LEPTONS_PER_TILE;\n        return MathUtil.isBetween(distance, minRange, maxRange);\n    }\n    public isInRange2(source: RangeTarget, target: RangeTarget, minRange: number, maxRange: number): boolean {\n        const distance = this.distance2(source, target) / Coords.LEPTONS_PER_TILE;\n        return MathUtil.isBetween(distance, minRange, maxRange);\n    }\n    public distance3(source: RangeTarget, target: RangeTarget): number {\n        const sourcePos = this.getWorldPosition3D(source);\n        const targetPos = this.getWorldPosition3D(target);\n        return sourcePos.distanceTo(targetPos);\n    }\n    public distance2(source: RangeTarget, target: RangeTarget): number {\n        const sourcePos = this.getWorldPosition2D(source);\n        const targetPos = this.getWorldPosition2D(target);\n        return sourcePos.distanceTo(targetPos);\n    }\n    private getWorldPosition3D(obj: RangeTarget): Vector3 {\n        if (hasPosition(obj)) {\n            return obj.position.worldPosition;\n        }\n        else if (hasAddScalar(obj)) {\n            return obj as Vector3;\n        }\n        else {\n            const tile = obj as TileCoord;\n            return Coords.tile3dToWorld(tile.rx + 0.5, tile.ry + 0.5, tile.z);\n        }\n    }\n    private getWorldPosition2D(obj: RangeTarget): Vector2 {\n        if (hasPosition(obj)) {\n            const worldPos = obj.position.worldPosition;\n            return new Vector2(worldPos.x, worldPos.z);\n        }\n        else if (hasAddScalar(obj)) {\n            const vec = obj as Vector3Like;\n            return new Vector2(vec.x, vec.z);\n        }\n        else {\n            const tile = obj as TileCoord;\n            return new Vector2(tile.rx + 0.5, tile.ry + 0.5)\n                .multiplyScalar(Coords.LEPTONS_PER_TILE);\n        }\n    }\n    public isInTileRange(source: RangeTarget, target: RangeTarget, minRange: number, maxRange: number): boolean {\n        const distance = this.tileDistance(source, target);\n        return MathUtil.isBetween(distance, minRange, maxRange);\n    }\n    public tileDistance(source: RangeTarget, target: RangeTarget): number {\n        const sourceTiles = this.getTiles(source);\n        const targetTiles = this.getTiles(target);\n        const sourceVec = new Vector2();\n        const targetVec = new Vector2();\n        let minDistance = Number.POSITIVE_INFINITY;\n        for (const sourceTile of sourceTiles) {\n            for (const targetTile of targetTiles) {\n                sourceVec.set(sourceTile.rx, sourceTile.ry);\n                targetVec.set(targetTile.rx, targetTile.ry);\n                const distance = sourceVec.distanceTo(targetVec);\n                if (distance <= minDistance) {\n                    minDistance = distance;\n                }\n            }\n        }\n        return minDistance;\n    }\n    private getTiles(obj: RangeTarget): Tile[] {\n        if (hasPosition(obj)) {\n            return this.tileOccupation.calculateTilesForGameObject(obj.tile, obj);\n        }\n        else if (Array.isArray(obj)) {\n            return obj;\n        }\n        else {\n            return [obj as Tile];\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/ScatterPositionHelper.ts",
    "content": "import { MovePositionHelper } from '@/game/gameobject/unit/MovePositionHelper';\nimport { RandomTileFinder } from '@/game/map/tileFinder/RandomTileFinder';\ninterface Game {\n    map: {\n        tiles: any;\n        mapBounds: any;\n        tileOccupation: {\n            getBridgeOnTile(tile: any): any;\n        };\n        terrain: {\n            findObstacles(params: {\n                tile: any;\n                onBridge: any;\n            }, unit: any): any[];\n        };\n    };\n}\ninterface Unit {\n    tile: any;\n    onBridge?: boolean;\n}\ninterface MovePosition {\n    tile: any;\n    onBridge?: any;\n}\ninterface FindFreeMovePositionOptions {\n    ignoredBlockers?: any[];\n    excludedTiles?: any[];\n    noSlopes?: boolean;\n}\nexport class ScatterPositionHelper {\n    private game: Game;\n    private movePositionHelper: MovePositionHelper;\n    constructor(game: Game) {\n        this.game = game;\n        this.movePositionHelper = new MovePositionHelper(game.map as any);\n    }\n    findPositions(units: Unit[], options: FindFreeMovePositionOptions = {}): Map<Unit, MovePosition> {\n        const occupiedTiles = new Set();\n        const positions = new Map();\n        for (const unit of units) {\n            const position = this.findFreeMovePosition(unit, occupiedTiles, options);\n            if (position) {\n                positions.set(unit, position);\n                occupiedTiles.add(position.tile);\n            }\n        }\n        return positions;\n    }\n    findFreeMovePosition(unit: Unit, occupiedTiles: Set<any>, { ignoredBlockers, excludedTiles, noSlopes }: FindFreeMovePositionOptions = {}): MovePosition | undefined {\n        const map = this.game.map;\n        const unitBridge = unit.onBridge ? map.tileOccupation.getBridgeOnTile(unit.tile) : undefined;\n        const tileFinder = new RandomTileFinder(map.tiles, map.mapBounds, unit.tile, 1, this.game as any, (tile) => {\n            if (excludedTiles?.includes(tile))\n                return false;\n            const bridge = map.tileOccupation.getBridgeOnTile(tile);\n            return (((bridge &&\n                this.movePositionHelper.isEligibleTile(tile as any, bridge, unitBridge, unit.tile)) ||\n                this.movePositionHelper.isEligibleTile(tile as any, undefined, unitBridge, unit.tile)) &&\n                (!noSlopes || tile.rampType === 0));\n        });\n        let foundTile;\n        let foundBridge;\n        while (true) {\n            const tile = tileFinder.getNextTile();\n            if (!tile)\n                break;\n            foundTile = tile;\n            foundBridge = map.tileOccupation.getBridgeOnTile(tile);\n            if (foundBridge &&\n                !this.movePositionHelper.isEligibleTile(tile as any, foundBridge, unitBridge, unit.tile)) {\n                foundBridge = undefined;\n            }\n            if (!occupiedTiles.has(tile)) {\n                let obstacles = map.terrain.findObstacles({ tile, onBridge: foundBridge }, unit);\n                if (ignoredBlockers?.length) {\n                    obstacles = obstacles.filter(obs => !ignoredBlockers.includes(obs.obj));\n                }\n                if (!obstacles.length)\n                    break;\n            }\n        }\n        if (foundTile) {\n            return { tile: foundTile, onBridge: foundBridge };\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/TargetUtil.ts",
    "content": "import { Vector3 } from '@/game/math/Vector3';\nimport { degToRad, rotateVec2 } from '@/game/math/geometry';\nimport { GameMath } from '@/game/math/GameMath';\nimport { Vector2 } from '@/game/math/Vector2';\nexport class TargetUtil {\n    static computeInterceptPoint(source: Vector3, speed: number, target: Vector3, targetVelocity: Vector3): Vector3 {\n        const relativePos = source.clone().sub(target);\n        const targetSpeed = targetVelocity.length();\n        const a = speed * speed - targetSpeed * targetSpeed;\n        const b = 2 * relativePos.dot(targetVelocity);\n        const c = -relativePos.dot(relativePos);\n        if (b * b - 4 * a * c < 0) {\n            return new Vector3();\n        }\n        const time = (-b + GameMath.sqrt(b * b - 4 * a * c)) / (2 * a);\n        return targetVelocity.clone().multiplyScalar(time).add(target);\n    }\n    static computeTurnCircle(position: Vector2, direction: Vector2, turnRate: number, speed: number): {\n        center: Vector2;\n        radius: number;\n    } {\n        const radius = speed / degToRad(Math.abs(turnRate));\n        const perpendicular = rotateVec2(direction.clone(), 90 * -Math.sign(turnRate));\n        return {\n            center: isFinite(radius)\n                ? perpendicular.setLength(radius).add(position)\n                : position.clone(),\n            radius,\n        };\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/Timer.ts",
    "content": "export class Timer {\n    private activeTicks: number = 0;\n    private activeFor?: number;\n    private activeSince?: number;\n    constructor() {\n        this.activeTicks = 0;\n    }\n    isActive(): boolean {\n        return this.activeTicks > 0;\n    }\n    setActiveFor(ticks: number, timestamp?: number): void {\n        this.activeTicks = ticks;\n        this.activeFor = ticks;\n        this.activeSince = timestamp;\n    }\n    reset(): void {\n        this.activeTicks = 0;\n        this.activeSince = undefined;\n        this.activeFor = undefined;\n    }\n    getTicksLeft(): number {\n        return this.activeTicks;\n    }\n    getInitialTicks(): number {\n        return this.activeFor ?? 0;\n    }\n    tick(timestamp: number): boolean {\n        if (this.activeTicks <= 0) {\n            return false;\n        }\n        this.activeTicks--;\n        if (this.activeTicks <= 0 ||\n            (this.activeSince !== undefined &&\n                timestamp - this.activeSince > this.activeFor!)) {\n            this.reset();\n            return true;\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/VeteranAbility.ts",
    "content": "export enum VeteranAbility {\n    FASTER = 0,\n    STRONGER = 1,\n    FIREPOWER = 2,\n    SCATTER = 3,\n    ROF = 4,\n    SIGHT = 5,\n    SELF_HEAL = 6,\n    CLOAK = 7,\n    EXPLODES = 8,\n    RADAR_INVISIBLE = 9,\n    SENSORS = 10,\n    FEARLESS = 11,\n    C4 = 12,\n    GUARD_AREA = 13,\n    CRUSHER = 14\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/VeteranLevel.ts",
    "content": "export enum VeteranLevel {\n    None = 0,\n    Veteran = 1,\n    Elite = 2\n}\n"
  },
  {
    "path": "src/game/gameobject/unit/ZoneType.ts",
    "content": "import { LandType } from '@/game/type/LandType';\nexport enum ZoneType {\n    Ground = 0,\n    Air = 1,\n    Water = 2\n}\nexport const getZoneType = (landType: LandType): ZoneType => {\n    return [LandType.Water, LandType.Beach].includes(landType)\n        ? ZoneType.Water\n        : ZoneType.Ground;\n};\n"
  },
  {
    "path": "src/game/gameopts/GameOptRandomGen.ts",
    "content": "import { Vector2 } from \"@/game/math/Vector2\";\nimport { Prng } from \"@/game/Prng\";\nimport { mpAllowedColors } from \"@/game/rules/mpAllowedColors\";\nimport { isNotNullOrUndefined } from \"@/util/typeGuard\";\nimport { RANDOM_COLOR_ID, RANDOM_COUNTRY_ID, RANDOM_START_POS, OBS_COUNTRY_ID } from \"@/game/gameopts/constants\";\nexport class GameOptRandomGen {\n    private prng: Prng;\n    static factory(seed: string | number, sequence: number): GameOptRandomGen {\n        return new GameOptRandomGen(Prng.factory(seed, sequence));\n    }\n    constructor(prng: Prng) {\n        this.prng = prng;\n    }\n    generateColors(players: {\n        humanPlayers: any[];\n        aiPlayers: any[];\n    }): Map<any, number> {\n        const allPlayers = [...players.humanPlayers, ...players.aiPlayers].filter(isNotNullOrUndefined);\n        const usedColors = allPlayers\n            .map(player => player.colorId)\n            .filter(colorId => colorId !== RANDOM_COLOR_ID);\n        const totalColors = mpAllowedColors.length;\n        const availableColors = new Array(totalColors)\n            .fill(0)\n            .map((_, index) => index)\n            .filter(colorId => !usedColors.includes(colorId));\n        const colorMap = new Map();\n        allPlayers.forEach(player => {\n            if (player.countryId !== OBS_COUNTRY_ID && player.colorId === RANDOM_COLOR_ID) {\n                if (availableColors.length < 1) {\n                    throw new Error(\"Out of available colors to choose from\");\n                }\n                const randomIndex = this.prng.generateRandomInt(0, availableColors.length - 1);\n                colorMap.set(player, availableColors[randomIndex]);\n                availableColors.splice(randomIndex, 1);\n            }\n        });\n        return colorMap;\n    }\n    generateCountries(players: {\n        humanPlayers: any[];\n        aiPlayers: any[];\n    }, rules: any): Map<any, number> {\n        const countryCount = rules.getMultiplayerCountries().length;\n        const allPlayers = [...players.humanPlayers, ...players.aiPlayers].filter(isNotNullOrUndefined);\n        const countryMap = new Map();\n        allPlayers.forEach(player => {\n            if (player.countryId === RANDOM_COUNTRY_ID) {\n                countryMap.set(player, this.prng.generateRandomInt(0, countryCount - 1));\n            }\n        });\n        return countryMap;\n    }\n    generateStartLocations(players: {\n        humanPlayers: any[];\n        aiPlayers: any[];\n    }, locations: Map<number, {\n        x: number;\n        y: number;\n    }>): Map<any, number> {\n        const allPlayers = [...players.humanPlayers, ...players.aiPlayers].filter(isNotNullOrUndefined);\n        const fixedPositions = allPlayers\n            .filter(player => player.startPos !== RANDOM_START_POS)\n            .map(player => player.startPos);\n        const availablePositions = [...locations.keys()].filter(pos => !fixedPositions.includes(pos));\n        const shuffledPositions: number[] = [];\n        while (availablePositions.length) {\n            const randomIndex = availablePositions.length ?\n                this.prng.generateRandomInt(0, availablePositions.length - 1) : 0;\n            shuffledPositions.push(...availablePositions.splice(randomIndex, 1));\n        }\n        shuffledPositions.unshift(...fixedPositions);\n        if (shuffledPositions.length >= 3) {\n            for (const offset of [1, 2]) {\n                if (!(fixedPositions.length - 1 >= offset)) {\n                    const positions = shuffledPositions.map(pos => locations[pos]);\n                    const farthestPoint = this.findFarthestPointFrom(positions.slice(0, offset), positions.slice(offset));\n                    const index = positions.findIndex(pos => pos.x === farthestPoint.x && pos.y === farthestPoint.y);\n                    shuffledPositions.splice(offset, 0, ...shuffledPositions.splice(index, 1));\n                }\n            }\n        }\n        if (shuffledPositions.length >= 4 && fixedPositions.length - 1 < 3) {\n            const positions = shuffledPositions.map(pos => locations[pos]);\n            const farthestPoint = this.findFarthestPointFrom(positions.slice(2, 3), positions.slice(3));\n            const index = positions.findIndex(pos => pos.x === farthestPoint.x && pos.y === farthestPoint.y);\n            shuffledPositions.splice(3, 0, ...shuffledPositions.splice(index, 1));\n        }\n        shuffledPositions.splice(0, fixedPositions.length);\n        const locationMap = new Map();\n        let currentIndex = -1;\n        allPlayers.forEach(player => {\n            if (player.countryId !== OBS_COUNTRY_ID && player.startPos === RANDOM_START_POS) {\n                if (currentIndex >= shuffledPositions.length - 1) {\n                    throw new RangeError(\"Map has fewer starting locations than players\");\n                }\n                locationMap.set(player, shuffledPositions[++currentIndex]);\n            }\n        });\n        return locationMap;\n    }\n    private findFarthestPointFrom(points: {\n        x: number;\n        y: number;\n    }[], searchPoints: {\n        x: number;\n        y: number;\n    }[]): {\n        x: number;\n        y: number;\n    } {\n        const vectors = points.map(point => new Vector2(point.x, point.y));\n        let farthestPoint: {\n            x: number;\n            y: number;\n        } | undefined;\n        let maxDistance = 0;\n        if (!searchPoints.length) {\n            throw new Error(\"Search array must have at least one element\");\n        }\n        for (const point of searchPoints) {\n            const vector = new Vector2(point.x, point.y);\n            const totalDistance = vectors.reduce((sum, vec) => sum + vector.distanceTo(vec), 0);\n            if (totalDistance >= maxDistance) {\n                farthestPoint = point;\n                maxDistance = totalDistance;\n            }\n        }\n        return farthestPoint!;\n    }\n}\n"
  },
  {
    "path": "src/game/gameopts/GameOptSanitizer.ts",
    "content": "import { clamp } from \"@/util/math\";\nexport class GameOptSanitizer {\n    static sanitize(gameOpts: any, rules: any): void {\n        const mpDialogSettings = rules.mpDialogSettings;\n        gameOpts.credits = Math.floor(clamp(gameOpts.credits, mpDialogSettings.minMoney, mpDialogSettings.maxMoney));\n        gameOpts.gameSpeed = Math.floor(clamp(gameOpts.gameSpeed, 0, 6));\n        gameOpts.unitCount = Math.floor(clamp(gameOpts.unitCount, mpDialogSettings.minUnitCount, mpDialogSettings.maxUnitCount));\n    }\n}\n"
  },
  {
    "path": "src/game/gameopts/GameOpts.ts",
    "content": "export function isHumanPlayerInfo(info: any): boolean {\n    return \"name\" in info;\n}\nexport enum AiDifficulty {\n    Brutal = 0,\n    Medium = 1,\n    Easy = 2,\n    MediumSea = 3,\n    Normal = 4,\n    Custom = 5,\n}\nexport interface HumanPlayerInfo {\n    name: string;\n    countryId: number;\n    colorId: number;\n    startPos: number;\n    teamId: number;\n}\nexport interface AiPlayerInfo {\n    difficulty: AiDifficulty;\n    customBotId?: string;\n    countryId: number;\n    colorId: number;\n    startPos: number;\n    teamId: number;\n}\nexport interface GameOpts {\n    gameMode: number;\n    gameSpeed: number;\n    credits: number;\n    unitCount: number;\n    shortGame: boolean;\n    superWeapons: boolean;\n    buildOffAlly: boolean;\n    mcvRepacks: boolean;\n    cratesAppear: boolean;\n    hostTeams?: boolean;\n    destroyableBridges: boolean;\n    multiEngineer: boolean;\n    noDogEngiKills: boolean;\n    mapName: string;\n    mapTitle: string;\n    mapDigest: string;\n    mapSizeBytes: number;\n    maxSlots: number;\n    mapOfficial: boolean;\n    humanPlayers: HumanPlayerInfo[];\n    aiPlayers: (AiPlayerInfo | undefined)[];\n    unknown?: string;\n}\n"
  },
  {
    "path": "src/game/gameopts/constants.ts",
    "content": "import { AiDifficulty } from \"./GameOpts\";\nexport const RANDOM_COUNTRY_ID = -2;\nexport const RANDOM_COLOR_ID = -2;\nexport const RANDOM_START_POS = -2;\nexport const NO_TEAM_ID = -2;\nexport const OBS_TEAM_ID = -3;\nexport const OBS_COUNTRY_ID = -3;\nexport const OBS_COLOR_ID = -2;\nexport const RANDOM_COUNTRY_NAME = \"Random\";\nexport const OBS_COUNTRY_NAME = \"Observer\";\nexport const aiUiNames = new Map<AiDifficulty, string>()\n    .set(AiDifficulty.Easy, \"GUI:AIDummy\")\n    .set(AiDifficulty.Normal, \"GUI:AINormal\")\n    .set(AiDifficulty.Custom, \"GUI:AICustom\");\nexport const aiUiTooltips = new Map<AiDifficulty, string>()\n    .set(AiDifficulty.Normal, \"GUI:AINormal:Tooltip\")\n    .set(AiDifficulty.Custom, \"GUI:AICustom:Tooltip\");\nexport const RANDOM_COUNTRY_UI_NAME = \"GUI:RandomEx\";\nexport const RANDOM_COUNTRY_UI_TOOLTIP = \"STT:PlayerSideRandom\";\nexport const OBS_COUNTRY_UI_NAME = \"GUI:Observer\";\nexport const OBS_COUNTRY_UI_TOOLTIP = \"STT:PlayerSideObserver\";\nexport const RANDOM_COLOR_NAME = \"\";\n"
  },
  {
    "path": "src/game/ini/GameModeType.ts",
    "content": "export enum GameModeType {\n    Battle = 0,\n    ManBattle = 1,\n    FreeForAll = 2,\n    Unholy = 3,\n    Cooperative = 4\n}\n"
  },
  {
    "path": "src/game/ini/GameModes.ts",
    "content": "import { MpDialogSettings } from '../rules/MpDialogSettings';\nimport { GameModeType } from './GameModeType';\nimport type { IniFile } from '../../data/IniFile';\nimport type { IniSection } from '../../data/IniSection';\nexport interface GameModeEntry {\n    id: number;\n    type: GameModeType;\n    label: string;\n    description: string;\n    rulesOverride: string;\n    mapFilter: string;\n    randomMapsAllowed: string;\n    aiAllowed: boolean;\n    mpDialogSettings: MpDialogSettings;\n}\nexport class GameModes {\n    private modeIniLoader: (fileName: string) => IniFile;\n    private entries: Map<number, GameModeEntry> = new Map();\n    constructor(mainMpModesIni: IniFile, modeIniLoader: (fileName: string) => IniFile) {\n        this.modeIniLoader = modeIniLoader;\n        this.loadIni(mainMpModesIni);\n    }\n    private loadIni(iniFile: IniFile): void {\n        iniFile.getOrderedSections().forEach((section: IniSection) => {\n            const gameModeTypeKey = section.name as keyof typeof GameModeType;\n            const type: GameModeType = GameModeType[gameModeTypeKey] ?? GameModeType.Battle;\n            Array.from(section.entries.keys()).forEach((key: string) => {\n                const values = section.getArray(key);\n                if (values.length < 5) {\n                    throw new Error(`Invalid format for mp mode entry \"${key}\". Expected at least 5 values.`);\n                }\n                const id = Number(key);\n                const rulesOverrideFileName = values[2].toLowerCase();\n                const entry: GameModeEntry = {\n                    id: id,\n                    type: type,\n                    label: values[0],\n                    description: values[1],\n                    rulesOverride: rulesOverrideFileName,\n                    mapFilter: values[3],\n                    randomMapsAllowed: values[4],\n                    aiAllowed: id < 3,\n                    mpDialogSettings: new MpDialogSettings().readIni(this.modeIniLoader(rulesOverrideFileName).getOrCreateSection(\"MultiplayerDialogSettings\")),\n                };\n                this.entries.set(id, entry);\n            });\n        });\n    }\n    getById(id: number): GameModeEntry {\n        const entry = this.entries.get(id);\n        if (!entry) {\n            throw new Error(`No game mode found with id ${id}`);\n        }\n        return entry;\n    }\n    hasId(id: number): boolean {\n        return this.entries.has(id);\n    }\n    getAll(): GameModeEntry[] {\n        return Array.from(this.entries.values());\n    }\n}\n"
  },
  {
    "path": "src/game/ini/MixinRules.ts",
    "content": "import { MixinRulesType } from './MixinRulesType';\nexport class MixinRules {\n    static getTypes(config: {\n        noDogEngiKills?: boolean;\n    }): MixinRulesType[] {\n        const types: MixinRulesType[] = [];\n        if (config.noDogEngiKills) {\n            types.push(MixinRulesType.NoDogEngiKills);\n        }\n        return types;\n    }\n}\n"
  },
  {
    "path": "src/game/ini/MixinRulesType.ts",
    "content": "export enum MixinRulesType {\n    NoDogEngiKills = 0\n}\n"
  },
  {
    "path": "src/game/map/BridgeOverlayTypes.ts",
    "content": "import { isBetween } from '@/util/math';\nexport enum OverlayBridgeType {\n    NotBridge = 0,\n    Concrete = 1,\n    Wood = 2\n}\nexport class BridgeOverlayTypes {\n    static minLowBridgeWoodId = 74;\n    static maxLowBridgeWoodId = 99;\n    static minLowBridgeConcreteId = 205;\n    static maxLowBridgeConcreteId = 230;\n    static minHighBridgeConcreteId = 24;\n    static maxHighBridgeConcreteId = 25;\n    static minHighBridgeWoodId = 237;\n    static maxHighBridgeWoodId = 238;\n    static bridgePlaceholderIds = [100, 101, 231, 232];\n    static getOverlayBridgeType(id: number): OverlayBridgeType {\n        return isBetween(id, this.minHighBridgeConcreteId, this.maxHighBridgeConcreteId) ||\n            isBetween(id, this.minLowBridgeConcreteId, this.maxLowBridgeConcreteId)\n            ? OverlayBridgeType.Concrete\n            : isBetween(id, this.minHighBridgeWoodId, this.maxHighBridgeWoodId) ||\n                isBetween(id, this.minLowBridgeWoodId, this.maxLowBridgeWoodId)\n                ? OverlayBridgeType.Wood\n                : OverlayBridgeType.NotBridge;\n    }\n    static isBridge(id: number): boolean {\n        return this.isHighBridge(id) || this.isLowBridge(id);\n    }\n    static isBridgePlaceholder(id: number): boolean {\n        return this.bridgePlaceholderIds.includes(id);\n    }\n    static isHighBridge(id: number): boolean {\n        return (isBetween(id, this.minHighBridgeWoodId, this.maxHighBridgeWoodId) ||\n            isBetween(id, this.minHighBridgeConcreteId, this.maxHighBridgeConcreteId));\n    }\n    static isLowBridge(id: number): boolean {\n        return (isBetween(id, this.minLowBridgeWoodId, this.maxLowBridgeWoodId) ||\n            isBetween(id, this.minLowBridgeConcreteId, this.maxLowBridgeConcreteId));\n    }\n    static isXBridge(id: number): boolean {\n        return (id === this.minHighBridgeWoodId ||\n            id === this.minHighBridgeConcreteId ||\n            isBetween(id, this.minLowBridgeWoodId, this.minLowBridgeWoodId + 8) ||\n            isBetween(id, this.minLowBridgeWoodId + 18, this.minLowBridgeWoodId + 21) ||\n            isBetween(id, this.minLowBridgeConcreteId, this.minLowBridgeConcreteId + 8) ||\n            isBetween(id, this.minLowBridgeConcreteId + 18, this.minLowBridgeConcreteId + 21));\n    }\n    static isLowBridgeHead(id: number): boolean {\n        return (isBetween(id, this.minLowBridgeWoodId + 18, this.minLowBridgeWoodId + 25) ||\n            isBetween(id, this.minLowBridgeConcreteId + 18, this.minLowBridgeConcreteId + 25));\n    }\n    static isLowBridgeHeadStart(id: number): boolean {\n        return (isBetween(id, this.minLowBridgeWoodId + 20, this.minLowBridgeWoodId + 23) ||\n            isBetween(id, this.minLowBridgeConcreteId + 20, this.minLowBridgeConcreteId + 23));\n    }\n    static calculateLowBridgeOverlayId(type: OverlayBridgeType, isStart: boolean): number {\n        let baseId: number;\n        if (type === OverlayBridgeType.Concrete) {\n            baseId = this.minLowBridgeConcreteId;\n        }\n        else if (type === OverlayBridgeType.Wood) {\n            baseId = this.minLowBridgeWoodId;\n        }\n        else {\n            throw new Error(\"Not implemented\");\n        }\n        return baseId + (isStart ? 0 : 9);\n    }\n    static calculateHighBridgeOverlayId(type: OverlayBridgeType, isStart: boolean): number {\n        let baseId: number;\n        if (type === OverlayBridgeType.Concrete) {\n            baseId = this.minHighBridgeConcreteId;\n        }\n        else if (type === OverlayBridgeType.Wood) {\n            baseId = this.minHighBridgeWoodId;\n        }\n        else {\n            throw new Error(\"Not implemented\");\n        }\n        return baseId + (isStart ? 0 : 1);\n    }\n}\n"
  },
  {
    "path": "src/game/map/Bridges.ts",
    "content": "import { TileCollection, TileDirection, Tile } from \"@/game/map/TileCollection\";\nimport { BridgeOverlayTypes, OverlayBridgeType } from \"@/game/map/BridgeOverlayTypes\";\nimport { DirectionalTileFinder } from \"@/game/map/tileFinder/DirectionalTileFinder\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { TileSets, HighBridgeHeadType } from \"@/game/theater/TileSets\";\nimport { TerrainType } from \"@/engine/type/TerrainType\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { FloodTileFinder } from \"@/game/map/tileFinder/FloodTileFinder\";\nimport { MapBounds } from \"@/game/map/MapBounds\";\nexport enum BridgeHeadType {\n    None = 0,\n    Start = 1,\n    End = 2\n}\ninterface BridgeObject {\n    tile: Tile;\n    overlayId: number;\n    value: number;\n    name: string;\n    tileElevation: number;\n    healthTrait?: {\n        health: number;\n    };\n    isOverlay(): boolean;\n    isBridge(): boolean;\n    isXBridge(): boolean;\n    isHighBridge(): boolean;\n    isLowBridge(): boolean;\n    isBridgePlaceholder(): boolean;\n}\ninterface GameObject {\n    isBuilding(): boolean;\n    isUnit(): boolean;\n    isSmudge(): boolean;\n    isOverlay(): boolean;\n    isBridgePlaceholder(): boolean;\n    rules: {\n        invisibleInGame: boolean;\n    };\n}\ninterface TileOccupationUpdateEvent {\n    object: BridgeObject;\n    type: \"added\" | \"removed\";\n}\ninterface TileOccupation {\n    onChange: {\n        subscribe(handler: (event: TileOccupationUpdateEvent) => void): void;\n        unsubscribe(handler: (event: TileOccupationUpdateEvent) => void): void;\n    };\n    getBridgeOnTile(tile: Tile): BridgeObject | null;\n    getGroundObjectsOnTile(tile: Tile): GameObject[];\n}\ninterface Rules {\n    getOverlayName(overlayId: number): string;\n}\ninterface BridgePiece {\n    obj: BridgeObject;\n    prev?: BridgePiece;\n    next?: BridgePiece;\n    headType: BridgeHeadType;\n}\ninterface BridgeSpec {\n    start: Tile;\n    end: Tile;\n    type: OverlayBridgeType;\n    isHigh: boolean;\n}\ninterface HighBridgeBoundary {\n    tile: Tile;\n    headType: HighBridgeHeadType;\n}\ninterface AdjacentTiles {\n    prev: Tile | null;\n    next: Tile | null;\n}\nexport class Bridges {\n    private pieces = new Set<BridgePiece>();\n    private piecesByTile = new Map<Tile, BridgePiece>();\n    constructor(private tileSets: TileSets, private tiles: TileCollection, private tileOccupation: TileOccupation, private mapBounds: MapBounds, private rules: Rules) {\n        tileOccupation.onChange.subscribe(this.handleTileOccupationUpdate);\n    }\n    private handleTileOccupationUpdate = ({ object: obj, type }: TileOccupationUpdateEvent): void => {\n        if (obj.isOverlay() && obj.isBridge()) {\n            const tile = obj.tile;\n            let piece = this.piecesByTile.get(tile);\n            if (type === \"added\") {\n                if (piece) {\n                    throw new Error(`A bridge piece already exists at tile (${tile.rx},${tile.ry})`);\n                }\n                const adjacentTiles = this.findBridgeAdjacentTiles(obj);\n                piece = {\n                    obj,\n                    prev: undefined,\n                    next: undefined,\n                    headType: this.computeHead(obj, adjacentTiles.prev, adjacentTiles.next),\n                };\n                this.piecesByTile.set(tile, piece);\n                this.pieces.add(piece);\n                this.connectPiece(piece, adjacentTiles.prev, adjacentTiles.next);\n                this.updateOverlayData(piece);\n                if (piece.prev)\n                    this.updateOverlayData(piece.prev);\n                if (piece.next)\n                    this.updateOverlayData(piece.next);\n            }\n            else {\n                if (!piece) {\n                    throw new Error(`Bridge piece was alredy removed at tile (${tile.rx},${tile.ry})`);\n                }\n                const prevPiece = piece.prev;\n                const nextPiece = piece.next;\n                this.disconnectPiece(piece);\n                this.piecesByTile.delete(tile);\n                this.pieces.delete(piece);\n                if (prevPiece)\n                    this.updateOverlayData(prevPiece);\n                if (nextPiece)\n                    this.updateOverlayData(nextPiece);\n            }\n        }\n    };\n    getPieceAtTile(tile: Tile): BridgePiece | undefined {\n        return this.piecesByTile.get(tile);\n    }\n    handlePieceHealthChange(piece: BridgePiece): void {\n        this.updateOverlayData(piece);\n        if (piece.prev)\n            this.updateOverlayData(piece.prev);\n        if (piece.next)\n            this.updateOverlayData(piece.next);\n    }\n    findDominoPieces(piece: BridgePiece): BridgePiece[] {\n        const dominoPieces: BridgePiece[] = [];\n        let foundEnd = false;\n        let currentPiece = piece.next;\n        if (piece.headType === BridgeHeadType.None || currentPiece) {\n            while (currentPiece) {\n                dominoPieces.push(currentPiece);\n                if (currentPiece.headType !== BridgeHeadType.None) {\n                    foundEnd = true;\n                    break;\n                }\n                currentPiece = currentPiece.next;\n            }\n        }\n        else {\n            foundEnd = true;\n        }\n        if (foundEnd) {\n            foundEnd = false;\n            dominoPieces.length = 0;\n            let currentPiece = piece.prev;\n            if (piece.headType === BridgeHeadType.None || currentPiece) {\n                while (currentPiece) {\n                    dominoPieces.push(currentPiece);\n                    if (currentPiece.headType !== BridgeHeadType.None) {\n                        foundEnd = true;\n                        break;\n                    }\n                    currentPiece = currentPiece.prev;\n                }\n            }\n            else {\n                foundEnd = true;\n            }\n            if (foundEnd) {\n                return [];\n            }\n        }\n        return dominoPieces;\n    }\n    private findBridgeAdjacentTiles(bridgeObj: BridgeObject): AdjacentTiles {\n        const isXBridge = bridgeObj.isXBridge();\n        const direction = new Vector2(Number(isXBridge), Number(!isXBridge));\n        const currentPos = new Vector2(bridgeObj.tile.rx, bridgeObj.tile.ry);\n        const prevPos = currentPos.clone().sub(direction);\n        const prevTile = this.tiles.getByMapCoords(prevPos.x, prevPos.y);\n        const nextPos = currentPos.clone().add(direction);\n        const nextTile = this.tiles.getByMapCoords(nextPos.x, nextPos.y);\n        return { prev: prevTile, next: nextTile };\n    }\n    private connectPiece(piece: BridgePiece, prevTile: Tile | null, nextTile: Tile | null): void {\n        if (prevTile) {\n            piece.prev = this.getPieceAtTile(prevTile);\n            if (piece.prev) {\n                piece.prev.next = piece;\n            }\n        }\n        if (nextTile) {\n            piece.next = this.getPieceAtTile(nextTile);\n            if (piece.next) {\n                piece.next.prev = piece;\n            }\n        }\n    }\n    private disconnectPiece(piece: BridgePiece): void {\n        if (piece.next) {\n            piece.next.prev = undefined;\n            piece.next = undefined;\n        }\n        if (piece.prev) {\n            piece.prev.next = undefined;\n            piece.prev = undefined;\n        }\n    }\n    private computeHead(bridgeObj: BridgeObject, prevTile: Tile | null, nextTile: Tile | null): BridgeHeadType {\n        const tile = bridgeObj.tile;\n        if (bridgeObj.isHighBridge()) {\n            const bridgeZ = tile.z + bridgeObj.tileElevation;\n            return prevTile?.z === bridgeZ ? BridgeHeadType.Start :\n                nextTile?.z === bridgeZ ? BridgeHeadType.End :\n                    BridgeHeadType.None;\n        }\n        return BridgeOverlayTypes.isLowBridgeHead(bridgeObj.overlayId)\n            ? BridgeOverlayTypes.isLowBridgeHeadStart(bridgeObj.overlayId)\n                ? BridgeHeadType.Start\n                : BridgeHeadType.End\n            : BridgeHeadType.None;\n    }\n    private updateOverlayData(piece: BridgePiece): void {\n        const obj = piece.obj;\n        const prevPiece = piece.prev;\n        const nextPiece = piece.next;\n        let overlayChanged = false;\n        const isXBridge = obj.isXBridge();\n        const bridgeType = BridgeOverlayTypes.getOverlayBridgeType(obj.overlayId);\n        if (BridgeOverlayTypes.isLowBridgeHead(obj.overlayId)) {\n            let overlayValue = 0;\n            if (BridgeOverlayTypes.isLowBridgeHeadStart(obj.overlayId)) {\n                overlayValue = isXBridge ? 20 : 22;\n                if (!nextPiece)\n                    overlayValue++;\n            }\n            else {\n                overlayValue = isXBridge ? 18 : 24;\n                if (!prevPiece)\n                    overlayValue++;\n            }\n            obj.overlayId = (bridgeType === OverlayBridgeType.Wood\n                ? BridgeOverlayTypes.minLowBridgeWoodId\n                : BridgeOverlayTypes.minLowBridgeConcreteId) + overlayValue;\n            obj.value = overlayValue;\n            overlayChanged = true;\n        }\n        else {\n            let overlayValue: number;\n            const isDamaged = (obj.healthTrait?.health ?? 100) <= 50;\n            if (piece.headType !== BridgeHeadType.None) {\n                if (piece.headType === BridgeHeadType.Start) {\n                    if (nextPiece) {\n                        if (isDamaged) {\n                            overlayValue = 6;\n                        }\n                        else {\n                            overlayValue = (nextPiece.obj.healthTrait?.health ?? 100) <= 50 ? 5 : 0;\n                        }\n                    }\n                    else {\n                        overlayValue = isXBridge ? 8 : 7;\n                    }\n                }\n                else {\n                    if (prevPiece) {\n                        if (isDamaged) {\n                            overlayValue = 6;\n                        }\n                        else {\n                            overlayValue = (prevPiece.obj.healthTrait?.health ?? 100) <= 50 ? 4 : 0;\n                        }\n                    }\n                    else {\n                        overlayValue = isXBridge ? 7 : 8;\n                    }\n                }\n            }\n            else {\n                let actualPrev = prevPiece;\n                let actualNext = nextPiece;\n                if (!isXBridge) {\n                    [actualPrev, actualNext] = [actualNext, actualPrev];\n                }\n                if (actualPrev || actualNext) {\n                    if (actualPrev) {\n                        if (actualNext) {\n                            const prevDamaged = (actualPrev.obj.healthTrait?.health ?? 100) <= 50;\n                            const nextDamaged = (actualNext.obj.healthTrait?.health ?? 100) <= 50;\n                            overlayValue = isDamaged || (prevDamaged && nextDamaged) ? 6 :\n                                prevDamaged ? 4 :\n                                    nextDamaged ? 5 : 0;\n                        }\n                        else {\n                            overlayValue = 8;\n                        }\n                    }\n                    else {\n                        overlayValue = 7;\n                    }\n                }\n                else {\n                    overlayValue = 0;\n                }\n            }\n            if (!isXBridge) {\n                overlayValue += 9;\n            }\n            if (obj.isHighBridge()) {\n                obj.value = overlayValue;\n            }\n            else {\n                obj.overlayId = (bridgeType === OverlayBridgeType.Wood\n                    ? BridgeOverlayTypes.minLowBridgeWoodId\n                    : BridgeOverlayTypes.minLowBridgeConcreteId) + overlayValue;\n                obj.value = overlayValue;\n                overlayChanged = true;\n            }\n        }\n        if (overlayChanged) {\n            obj.name = this.rules.getOverlayName(obj.overlayId);\n        }\n    }\n    findClosestBridgeSpec(centerTile: Tile): BridgeSpec | undefined {\n        const finder = new RadialTileFinder(this.tiles, this.mapBounds, centerTile, { width: 1, height: 1 }, 1, 3, (tile: Tile) => {\n            if (tile.z !== centerTile.z)\n                return false;\n            const bridge = this.tileOccupation.getBridgeOnTile(tile);\n            return (!!bridge?.isLowBridge() && this.getPieceAtTile(bridge.tile)?.headType !== BridgeHeadType.None) ||\n                !!this.tileSets.isHighBridgeBoundaryTile(tile.tileNum);\n        }, false);\n        const foundTile = finder.getNextTile() as Tile | undefined;\n        if (!foundTile)\n            return;\n        let startTile: Tile;\n        let bridgeType: OverlayBridgeType;\n        let isXBridge: boolean;\n        let isStartHead: boolean;\n        let endTile: Tile;\n        let headType: HighBridgeHeadType | undefined;\n        const isHighBridge = !this.tileOccupation.getBridgeOnTile(foundTile);\n        if (isHighBridge) {\n            const boundary = this.findHighBridgeBoundary(foundTile);\n            if (!boundary)\n                return;\n            startTile = boundary.tile;\n            bridgeType = this.tileSets.getSetNum(foundTile.tileNum) === this.tileSets.getGeneralValue(\"WoodBridgeSet\")\n                ? OverlayBridgeType.Wood\n                : OverlayBridgeType.Concrete;\n            isXBridge = boundary.headType === HighBridgeHeadType.TopLeft ||\n                boundary.headType === HighBridgeHeadType.BottomRight;\n            isStartHead = boundary.headType === HighBridgeHeadType.TopLeft ||\n                boundary.headType === HighBridgeHeadType.TopRight;\n            headType = boundary.headType;\n        }\n        else {\n            const bridge = this.tileOccupation.getBridgeOnTile(foundTile)!;\n            startTile = bridge.tile;\n            const piece = this.getPieceAtTile(startTile);\n            if (!piece)\n                throw new Error(\"Bridge head is not defined\");\n            const overlayBridgeType = BridgeOverlayTypes.getOverlayBridgeType(piece.obj.overlayId);\n            if (overlayBridgeType === OverlayBridgeType.NotBridge) {\n                throw new Error(\"Expected a bridge type\");\n            }\n            bridgeType = overlayBridgeType;\n            isXBridge = piece.obj.isXBridge();\n            isStartHead = piece.headType === BridgeHeadType.Start;\n        }\n        const deltaX = Number(isXBridge) * (isStartHead ? 1 : -1);\n        const deltaY = Number(!isXBridge) * (isStartHead ? 1 : -1);\n        if (isHighBridge) {\n            const endFinder = new DirectionalTileFinder(this.tiles, this.mapBounds, startTile, 1, 100, deltaX, deltaY, (tile: Tile) => tile.z === startTile.z && this.tileSets.isHighBridgeBoundaryTile(tile.tileNum), false);\n            const foundEndTile = endFinder.getNextTile() as Tile | undefined;\n            if (!foundEndTile) {\n                return;\n            }\n            const startSetNum = this.tileSets.getSetNum(startTile.tileNum);\n            if (this.tileSets.getSetNum(foundEndTile.tileNum) !== startSetNum) {\n                return;\n            }\n            const endBoundary = this.findHighBridgeBoundary(foundEndTile);\n            if (!endBoundary)\n                return;\n            if (headType !== this.tileSets.getOppositeHighBridgeHeadType(endBoundary.headType)) {\n                return;\n            }\n            endTile = endBoundary.tile;\n        }\n        else {\n            let targetPiece: BridgePiece | undefined;\n            let distance = 1;\n            const startX = startTile.rx;\n            const startY = startTile.ry;\n            while (!targetPiece) {\n                const checkTile = this.tiles.getByMapCoords(startX + deltaX * distance, startY + deltaY * distance);\n                if (!checkTile)\n                    return;\n                const piece = this.getPieceAtTile(checkTile);\n                if (piece && piece.obj.isXBridge() !== isXBridge)\n                    return;\n                if (piece?.headType === (isStartHead ? BridgeHeadType.End : BridgeHeadType.Start)) {\n                    targetPiece = piece;\n                }\n                distance++;\n            }\n            endTile = targetPiece.obj.tile;\n        }\n        return {\n            start: isStartHead ? startTile : endTile,\n            end: isStartHead ? endTile : startTile,\n            type: bridgeType,\n            isHigh: isHighBridge,\n        };\n    }\n    private findHighBridgeBoundary(tile: Tile): HighBridgeBoundary | undefined {\n        const tileData = this.tileSets.getTile(tile.tileNum);\n        const headType = this.tileSets.getHighBridgeHeadType(tileData.index);\n        if (headType === undefined) {\n            console.warn(`Couldn't find a valid bridge type for index \"${tileData.index}\" @ ${tile.rx},${tile.ry}`);\n            return;\n        }\n        let deltaX = 0;\n        let deltaY = 0;\n        switch (headType) {\n            case HighBridgeHeadType.TopLeft:\n            case HighBridgeHeadType.MiddleTlBr:\n                deltaX = 1;\n                deltaY = 0;\n                break;\n            case HighBridgeHeadType.BottomRight:\n                deltaX = -1;\n                deltaY = 0;\n                break;\n            case HighBridgeHeadType.TopRight:\n            case HighBridgeHeadType.MiddleTrBl:\n                deltaX = 0;\n                deltaY = 1;\n                break;\n            case HighBridgeHeadType.BottomLeft:\n                deltaX = 0;\n                deltaY = -1;\n                break;\n            default:\n                throw new Error(`Unhandled head type \"${headType}\"`);\n        }\n        const floodFinder = new FloodTileFinder(this.tiles, this.mapBounds, tile, (t: Tile) => t.tileNum === tile.tileNum, (t: Tile) => t.terrainType === TerrainType.Pavement && t.z >= tile.z, false);\n        const tiles: Tile[] = [];\n        let foundTile: Tile | null;\n        while (foundTile = floodFinder.getNextTile()) {\n            tiles.push(foundTile);\n        }\n        if (tiles.length) {\n            tiles.sort((a, b) => 100 * (deltaX ? deltaX * (b.rx - a.rx) : deltaY * (b.ry - a.ry)) +\n                (deltaX ? a.ry - b.ry : a.rx - b.rx));\n            return { tile: tiles[0], headType };\n        }\n    }\n    canBeRepaired(spec: BridgeSpec): boolean {\n        const finder = this.createBridgePieceTileFinder(spec, (tile: Tile) => !(this.getPieceAtTile(tile) ||\n            (this.tileSets.isHighBridgeMiddleTile(tile.tileNum) && tile.z === spec.start.z)));\n        let hasDestroyedPieces = false;\n        const direction = spec.start.rx !== spec.end.rx ? TileDirection.BottomLeft : TileDirection.BottomRight;\n        let tile: Tile | null;\n        while (tile = finder.getNextTile()) {\n            hasDestroyedPieces = true;\n            const secondTile = this.tiles.getNeighbourTile(tile, direction);\n            const thirdTile = this.tiles.getNeighbourTile(secondTile, direction);\n            if (spec.isHigh) {\n                if ([tile, secondTile, thirdTile].find(t => this.tileOccupation.getGroundObjectsOnTile(t).some(obj => obj.isBuilding() && !obj.rules.invisibleInGame))) {\n                    return false;\n                }\n            }\n            else {\n                if ([tile, secondTile, thirdTile].find(t => this.tileOccupation.getGroundObjectsOnTile(t).some(obj => !(obj.isUnit() || obj.isSmudge() || (obj.isOverlay() && obj.isBridgePlaceholder()))))) {\n                    return false;\n                }\n            }\n        }\n        return hasDestroyedPieces;\n    }\n    getPieceTiles(piece: BridgePiece): Tile[] {\n        const tile = piece.obj.tile;\n        const direction = piece.obj.isXBridge() ? TileDirection.BottomLeft : TileDirection.BottomRight;\n        const secondTile = this.tiles.getNeighbourTile(tile, direction);\n        return [tile, secondTile, this.tiles.getNeighbourTile(secondTile, direction)];\n    }\n    findMapHighBridgeHeadTiles(): Set<Tile> {\n        const bridgeSetTiles = this.tiles.getAllBridgeSetTiles();\n        const headTiles = new Set<Tile>();\n        for (const tile of bridgeSetTiles) {\n            const boundary = this.findHighBridgeBoundary(tile);\n            if (boundary) {\n                headTiles.add(boundary.tile);\n            }\n        }\n        return headTiles;\n    }\n    findBridgeSpecsForHeadTiles(headTiles: Set<Tile>): BridgeSpec[] {\n        const specMap = new Map<string, BridgeSpec>();\n        for (const tile of headTiles) {\n            const spec = this.findClosestBridgeSpec(tile);\n            if (spec) {\n                specMap.set(spec.start.id + \":\" + spec.end.id, spec);\n            }\n        }\n        return [...specMap.values()];\n    }\n    findAllBridgeTiles(spec: BridgeSpec): Tile[] {\n        const tiles: Tile[] = [];\n        const direction = spec.start.rx !== spec.end.rx ? TileDirection.BottomLeft : TileDirection.BottomRight;\n        for (const pieceTile of this.findNonBuildablePieceTiles(spec)) {\n            const secondTile = this.tiles.getNeighbourTile(pieceTile, direction);\n            const thirdTile = this.tiles.getNeighbourTile(secondTile, direction);\n            tiles.push(pieceTile, secondTile, thirdTile);\n        }\n        return tiles;\n    }\n    findBridgePieces(spec: BridgeSpec): BridgePiece[] {\n        const finder = this.createBridgePieceTileFinder(spec, (tile: Tile) => !!this.getPieceAtTile(tile));\n        const pieces: BridgePiece[] = [];\n        let tile: Tile | null;\n        while (tile = finder.getNextTile()) {\n            pieces.push(this.getPieceAtTile(tile)!);\n        }\n        return pieces;\n    }\n    findDestroyedPieceTiles(spec: BridgeSpec): Tile[] {\n        const finder = this.createBridgePieceTileFinder(spec, (tile: Tile) => !(this.getPieceAtTile(tile) ||\n            (this.tileSets.isHighBridgeMiddleTile(tile.tileNum) && tile.z === spec.start.z)));\n        const tiles: Tile[] = [];\n        let tile: Tile | null;\n        while (tile = finder.getNextTile()) {\n            tiles.push(tile);\n        }\n        return tiles;\n    }\n    findNonBuildablePieceTiles(spec: BridgeSpec): Tile[] {\n        const finder = this.createBridgePieceTileFinder(spec, (tile: Tile) => !(this.tileSets.isHighBridgeMiddleTile(tile.tileNum) && tile.z === spec.start.z));\n        const tiles: Tile[] = [];\n        let tile: Tile | null;\n        while (tile = finder.getNextTile()) {\n            tiles.push(tile);\n        }\n        return tiles;\n    }\n    private createBridgePieceTileFinder(spec: BridgeSpec, predicate: (tile: Tile) => boolean): DirectionalTileFinder {\n        const isXDirection = spec.start.rx !== spec.end.rx;\n        return new DirectionalTileFinder(this.tiles, this.mapBounds, spec.start, 1, (isXDirection ? spec.end.rx - spec.start.rx : spec.end.ry - spec.start.ry) - 1, Number(isXDirection), Number(!isXDirection), predicate, false);\n    }\n    dispose(): void {\n        this.pieces.forEach(piece => {\n            piece.prev = undefined;\n            piece.next = undefined;\n        });\n        this.tileOccupation.onChange.unsubscribe(this.handleTileOccupationUpdate);\n    }\n}\n"
  },
  {
    "path": "src/game/map/MapBounds.ts",
    "content": "import { rectContainsPoint, rectClampPoint, rectContainsRect, rectEquals } from '@/util/geometry';\nimport { Coords } from '@/game/Coords';\nimport { EventDispatcher } from '@/util/event';\ninterface Size {\n    width: number;\n    height: number;\n}\ninterface Rect {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface Point {\n    x: number;\n    y: number;\n    z?: number;\n}\ninterface Tile {\n    dx: number;\n    dy: number;\n    z: number;\n}\ninterface MapFile {\n    fullSize: Size;\n    localSize: Rect;\n}\ninterface MapRules {\n    getCutoffTileHeight(): number;\n}\nexport class MapBounds {\n    private mapCutoffHeight: number;\n    private mapBuildableSize: Rect;\n    private localSize: Rect;\n    private fullSize: Size;\n    private clampedFullSize: Rect;\n    private rawLocalSize: Rect;\n    private _onLocalResize: EventDispatcher<MapBounds>;\n    constructor() {\n        this.mapCutoffHeight = 0;\n        this.mapBuildableSize = { x: 0, y: 0, width: 0, height: 0 };\n        this.localSize = { x: 0, y: 0, width: 0, height: 0 };\n        this.fullSize = { width: 0, height: 0 };\n        this.clampedFullSize = { x: 0, y: 0, width: 0, height: 0 };\n        this.rawLocalSize = { x: 0, y: 0, width: 0, height: 0 };\n        this._onLocalResize = new EventDispatcher<MapBounds>();\n    }\n    get onLocalResize() {\n        return this._onLocalResize.asEvent();\n    }\n    fromMapFile(mapFile: MapFile, rules: MapRules): MapBounds {\n        this.fullSize = {\n            width: 2 * mapFile.fullSize.width,\n            height: 2 * mapFile.fullSize.height,\n        };\n        this.clampedFullSize = {\n            x: 1,\n            y: 2,\n            width: 2 * (mapFile.fullSize.width - 1) - 1 / Coords.ISO_TILE_SIZE,\n            height: 2 * (mapFile.fullSize.height - 1) + 1 - 1 / Coords.ISO_TILE_SIZE,\n        };\n        this.mapCutoffHeight = Math.max(9, rules.getCutoffTileHeight());\n        const x = Math.max(2, mapFile.localSize.x);\n        const localSize = {\n            x,\n            y: mapFile.localSize.y,\n            width: Math.min(mapFile.fullSize.width - 2 - x, mapFile.localSize.width),\n            height: mapFile.localSize.height,\n        };\n        this.updateRawLocalSize(localSize);\n        return this;\n    }\n    updateRawLocalSize(size: Rect): void {\n        if (this.rawLocalSize.width &&\n            this.rawLocalSize.height &&\n            !rectContainsRect(size, this.rawLocalSize)) {\n            console.warn(\"New map limits must be outside old limits. Skipping.\");\n        }\n        else if (!rectEquals(size, this.rawLocalSize)) {\n            this.localSize = this.computeLocalSize(size, this.fullSize.height / 2, this.mapCutoffHeight);\n            this.rawLocalSize = { ...size };\n            this.mapBuildableSize = {\n                x: this.localSize.x,\n                y: this.localSize.y + 4,\n                width: this.localSize.width - 2,\n                height: this.localSize.height - 8,\n            };\n            this._onLocalResize.dispatch(this);\n        }\n    }\n    private computeLocalSize(size: Rect, height: number, cutoffHeight: number): Rect {\n        return {\n            x: 2 * size.x,\n            y: 2 * size.y - 4,\n            height: Math.min(2 * (size.height + 5) - 1, 2 * height - 2 * (size.y - 3) - cutoffHeight),\n            width: 2 * size.width,\n        };\n    }\n    getLocalSize(): Rect {\n        return this.localSize;\n    }\n    getRawLocalSize(): Rect {\n        return this.rawLocalSize;\n    }\n    getFullSize(): Size {\n        return this.fullSize;\n    }\n    getClampedFullSize(): Rect {\n        return this.clampedFullSize;\n    }\n    isWithinBounds(tile: Tile): boolean {\n        return rectContainsPoint(this.mapBuildableSize, {\n            x: tile.dx,\n            y: tile.dy - tile.z,\n        });\n    }\n    clampWithinBounds(tile: Tile): {\n        dx: number;\n        dy: number;\n    } {\n        let { x, y } = rectClampPoint(this.mapBuildableSize, {\n            x: tile.dx,\n            y: tile.dy - tile.z,\n        });\n        y += (x % 2) - (y % 2);\n        if (y > this.mapBuildableSize.y + this.mapBuildableSize.height) {\n            y -= 2;\n        }\n        return { dx: x, dy: y };\n    }\n    isWithinHardBounds(point: Point): boolean {\n        const x = point.x / Coords.LEPTONS_PER_TILE;\n        const y = (point.z ?? point.y) / Coords.LEPTONS_PER_TILE;\n        const r = x - y + this.fullSize.width / 2 - 1;\n        const i = x + y - this.fullSize.width / 2 - 1;\n        return rectContainsPoint(this.clampedFullSize, {\n            x: r + 1,\n            y: i + 1,\n        });\n    }\n}\n"
  },
  {
    "path": "src/game/map/MapShroud.ts",
    "content": "import { EventDispatcher } from '../../util/event';\nimport { GameSpeed } from '../GameSpeed';\nimport { TerrainType } from '../../engine/type/TerrainType';\nimport { clamp } from '../../util/math';\nexport enum ShroudType {\n    Unexplored = 0,\n    TemporaryReveal = 1,\n    Explored = 2\n}\nexport enum ShroudFlag {\n    Darken = 8\n}\ninterface Size {\n    width: number;\n    height: number;\n}\ninterface ShroudCoords {\n    sx: number;\n    sy: number;\n}\ninterface WorldCoords {\n    rx: number;\n    ry: number;\n}\ninterface Tile {\n    rx: number;\n    ry: number;\n    z: number;\n    terrainType: TerrainType;\n}\ninterface TileMap {\n    getMapSize(): Size;\n    getMaxTileHeight(): number;\n    getAll(): Tile[];\n    getByMapCoords(rx: number, ry: number): Tile | undefined;\n}\ninterface Invalidation {\n    center: ShroudCoords;\n    elevation: number;\n    radius: number;\n}\nexport class MapShroud {\n    private invalidations: Map<number, Invalidation>;\n    private temporaryReveals: Map<ShroudCoords, number>;\n    private fullInvalidation: boolean;\n    private _onChange: EventDispatcher;\n    private padding: number;\n    private size: Size;\n    private tiles: Uint8Array;\n    private tileElevation: Uint8Array;\n    private static readonly TEMPORARY_REVEAL_DURATION = 5;\n    private static readonly OBJECT_REVEAL_RADIUS = 4.25;\n    private static readonly SHROUD_TYPE_BITS = 3;\n    private static readonly SHROUD_TYPE_MASK = (1 << MapShroud.SHROUD_TYPE_BITS) - 1;\n    constructor() {\n        this.invalidations = new Map();\n        this.temporaryReveals = new Map();\n        this.fullInvalidation = false;\n        this._onChange = new EventDispatcher();\n    }\n    get onChange() {\n        return this._onChange.asEvent();\n    }\n    fromTiles(map: TileMap): this {\n        const mapSize = map.getMapSize();\n        const maxHeight = map.getMaxTileHeight();\n        this.padding = (maxHeight + (maxHeight % 2)) / 2;\n        this.size = {\n            width: mapSize.width + this.padding,\n            height: mapSize.height + this.padding\n        };\n        this.tiles = new Uint8Array(this.size.width * this.size.height);\n        this.tiles.fill(ShroudType.Unexplored);\n        this.tileElevation = new Uint8Array(this.size.width * this.size.height);\n        for (const tile of map.getAll()) {\n            const index = this.getTileIndex(tile);\n            this.tileElevation[index] = Math.max(this.tileElevation[index], tile.terrainType === TerrainType.Cliff && tile.z > 0 ? tile.z - 1 : tile.z);\n        }\n        return this;\n    }\n    getSize(): Size {\n        return this.size;\n    }\n    getTileIndex(tile: Tile): number {\n        const { sx, sy } = this.rxyzToSxy(tile.rx, tile.ry, tile.z);\n        return sx + sy * this.size.width;\n    }\n    rxyzToSxy(rx: number, ry: number, z: number): ShroudCoords {\n        const adjustedZ = (z |= 0) + (z % 2);\n        return {\n            sx: rx - adjustedZ / 2 + this.padding,\n            sy: ry - adjustedZ / 2 + this.padding\n        };\n    }\n    sxyzToRxy(sx: number, sy: number, z: number): WorldCoords {\n        return {\n            rx: sx + Math.ceil(z / 2) - this.padding,\n            ry: sy + Math.ceil(z / 2) - this.padding\n        };\n    }\n    shroudCoordsToWorld(coords: ShroudCoords): WorldCoords {\n        return this.sxyzToRxy(coords.sx, coords.sy, 0);\n    }\n    findTilesAtShroudCoords(coords: ShroudCoords, map: TileMap): Tile[] {\n        const maxHeight = map.getMaxTileHeight();\n        const adjustedMaxHeight = maxHeight + (maxHeight % 2);\n        const tiles: Tile[] = [];\n        for (let z = 0; z <= adjustedMaxHeight; z += 2) {\n            const adjustedZ = z + (z % 2);\n            const { rx, ry } = this.sxyzToRxy(coords.sx, coords.sy, adjustedZ);\n            const tile = map.getByMapCoords(rx, ry);\n            if (tile?.z === z) {\n                tiles.push(tile);\n            }\n        }\n        return tiles;\n    }\n    clone(): MapShroud {\n        const clone = new MapShroud();\n        clone.tiles = this.tiles.slice();\n        clone.size = this.size;\n        clone.padding = this.padding;\n        clone.tileElevation = this.tileElevation;\n        return clone;\n    }\n    copy(other: MapShroud): void {\n        this.tiles = other.tiles.slice();\n        this.size = other.size;\n        this.padding = other.padding;\n        this.tileElevation = other.tileElevation;\n    }\n    merge(other: MapShroud): void {\n        if (this.size.width !== other.size.width || this.size.height !== other.size.height) {\n            throw new Error(\"Size mismatch\");\n        }\n        const otherTiles = other.tiles;\n        for (let i = 0, len = this.tiles.length; i < len; i++) {\n            this.tiles[i] =\n                Math.max(otherTiles[i] & MapShroud.SHROUD_TYPE_MASK, this.tiles[i] & MapShroud.SHROUD_TYPE_MASK) |\n                    (((otherTiles[i] | this.tiles[i]) >> MapShroud.SHROUD_TYPE_BITS) << MapShroud.SHROUD_TYPE_BITS);\n        }\n    }\n    isShrouded(tile: Tile, offset: number = 0): boolean {\n        const coords = this.rxyzToSxy(tile.rx, tile.ry, tile.z + offset);\n        return this.getShroudTypeByShroudCoords(coords) === ShroudType.Unexplored;\n    }\n    getShroudType(tile: Tile): ShroudType {\n        return this.tiles[this.getTileIndex(tile)] & MapShroud.SHROUD_TYPE_MASK;\n    }\n    isFlagged(tile: Tile, flag: number): boolean {\n        return (this.tiles[this.getTileIndex(tile)] & flag) !== 0;\n    }\n    getShroudTypeByTileCoords(rx: number, ry: number, z: number): ShroudType {\n        return this.getShroudTypeByShroudCoords(this.rxyzToSxy(rx, ry, z));\n    }\n    getShroudTypeByShroudCoords({ sx, sy }: ShroudCoords): ShroudType {\n        return sx < 0 || sy < 0 || sx >= this.size.width || sy >= this.size.height\n            ? ShroudType.Unexplored\n            : this.tiles[sx + sy * this.size.width] & MapShroud.SHROUD_TYPE_MASK;\n    }\n    invalidateFull(): void {\n        this.fullInvalidation = true;\n    }\n    invalidate(coords: ShroudCoords, elevation: number, radius: number): void {\n        const index = coords.sx + coords.sy * this.size.width;\n        let invalidation = this.invalidations.get(index);\n        if (!invalidation) {\n            invalidation = { center: coords, elevation: 0, radius: 0 };\n            this.invalidations.set(index, invalidation);\n        }\n        invalidation.elevation = Math.max(invalidation.elevation, elevation);\n        invalidation.radius = Math.max(invalidation.radius, radius);\n    }\n    revealFrom(object: {\n        isBuilding(): boolean;\n        wallTrait?: boolean;\n        sight?: number;\n        tile: Tile;\n        tileElevation: number;\n    }): void {\n        if (!object.isBuilding() || !object.wallTrait) {\n            if (object.sight) {\n                const elevation = object.tile.z + object.tileElevation;\n                const coords = this.rxyzToSxy(object.tile.rx, object.tile.ry, elevation);\n                this.invalidate(coords, elevation, object.sight);\n            }\n        }\n    }\n    revealAround(tile: Tile, radius: number): void {\n        const coords = this.rxyzToSxy(tile.rx, tile.ry, tile.z);\n        this.invalidate(coords, Number.POSITIVE_INFINITY, radius);\n    }\n    unrevealAround(tile: Tile, radius: number): void {\n        const coords: ShroudCoords[] = [];\n        const center = this.rxyzToSxy(tile.rx, tile.ry, tile.z);\n        this.setValueAround(center, radius, Number.POSITIVE_INFINITY, coords, ShroudType.Unexplored, ShroudType.Explored);\n        this._onChange.dispatch(this, {\n            type: \"incremental\",\n            coords\n        });\n    }\n    revealTemporarily(object: {\n        tile: Tile;\n        tileElevation: number;\n    }): void {\n        const coords = this.rxyzToSxy(object.tile.rx, object.tile.ry, object.tile.z + object.tileElevation);\n        this.temporaryReveals.set(coords, MapShroud.TEMPORARY_REVEAL_DURATION * GameSpeed.BASE_TICKS_PER_SECOND);\n    }\n    revealObject(object: {\n        tile: Tile;\n        tileElevation: number;\n    }): void {\n        const coords = this.rxyzToSxy(object.tile.rx, object.tile.ry, object.tile.z + object.tileElevation);\n        this.invalidate(coords, Number.POSITIVE_INFINITY, MapShroud.OBJECT_REVEAL_RADIUS);\n    }\n    toggleFlagsAround(tile: Tile, radius: number, flags: number, set: boolean): void {\n        const coords: ShroudCoords[] = [];\n        const center = this.rxyzToSxy(tile.rx, tile.ry, tile.z);\n        this.setValueAround(center, radius, Number.POSITIVE_INFINITY, coords, undefined, undefined, set ? { setFlags: flags } : { clearFlags: flags });\n        this._onChange.dispatch(this, {\n            type: \"incremental\",\n            coords\n        });\n    }\n    update(): void {\n        const changedCoords: ShroudCoords[] = [];\n        if (this.invalidations.size) {\n            for (const invalidation of this.invalidations.values()) {\n                this.setValueAround(invalidation.center, invalidation.radius, invalidation.elevation, changedCoords, ShroudType.Explored, [ShroudType.Unexplored, ShroudType.TemporaryReveal]);\n            }\n            this.invalidations.clear();\n        }\n        if (this.temporaryReveals.size) {\n            this.temporaryReveals.forEach((duration, coords) => {\n                if (duration <= 0) {\n                    this.setValueAround(coords, MapShroud.SHROUD_TYPE_BITS, Number.POSITIVE_INFINITY, changedCoords, ShroudType.Unexplored, ShroudType.TemporaryReveal);\n                    this.temporaryReveals.delete(coords);\n                }\n                else {\n                    if (duration === MapShroud.TEMPORARY_REVEAL_DURATION * GameSpeed.BASE_TICKS_PER_SECOND) {\n                        this.setValueAround(coords, MapShroud.SHROUD_TYPE_BITS, Number.POSITIVE_INFINITY, changedCoords, ShroudType.TemporaryReveal, ShroudType.Unexplored);\n                    }\n                    this.temporaryReveals.set(coords, duration - 1);\n                }\n            });\n        }\n        if (this.fullInvalidation) {\n            this.fullInvalidation = false;\n            this._onChange.dispatch(this, { type: \"full\" });\n        }\n        else if (changedCoords.length) {\n            this._onChange.dispatch(this, {\n                type: \"incremental\",\n                coords: changedCoords\n            });\n        }\n    }\n    private setValueAround(center: ShroudCoords, radius: number, maxElevation: number, changedCoords: ShroudCoords[], newValue?: ShroudType, oldValue?: ShroudType | ShroudType[], flags?: {\n        setFlags?: number;\n        clearFlags?: number;\n    }): void {\n        const radiusCeil = Math.ceil(radius);\n        const minX = clamp(center.sx - radiusCeil, 0, this.size.width - 1);\n        const maxX = clamp(center.sx + radiusCeil, 0, this.size.width - 1);\n        const minY = clamp(center.sy - radiusCeil, 0, this.size.height - 1);\n        const maxY = clamp(center.sy + radiusCeil, 0, this.size.height - 1);\n        const width = this.size.width;\n        for (let x = minX; x <= maxX; x++) {\n            for (let y = minY; y <= maxY; y++) {\n                const index = x + y * width;\n                const currentType = this.tiles[index] & MapShroud.SHROUD_TYPE_MASK;\n                const currentFlags = (this.tiles[index] >> MapShroud.SHROUD_TYPE_BITS) << MapShroud.SHROUD_TYPE_BITS;\n                let newFlags = currentFlags;\n                if (flags?.setFlags !== undefined) {\n                    newFlags |= flags.setFlags;\n                }\n                if (flags?.clearFlags !== undefined) {\n                    newFlags &= ~flags.clearFlags;\n                }\n                const matchesOldValue = oldValue === undefined\n                    ? true\n                    : Array.isArray(oldValue)\n                        ? oldValue.includes(currentType)\n                        : oldValue === currentType;\n                const inRadius = (x - center.sx) * (x - center.sx) + (y - center.sy) * (y - center.sy) <=\n                    radius * radius + 1;\n                const belowElevationLimit = this.tileElevation[index] < maxElevation + 4;\n                if (!matchesOldValue || !inRadius || !belowElevationLimit) {\n                    continue;\n                }\n                const nextType = newValue ?? currentType;\n                this.tiles[index] = nextType | newFlags;\n                if (currentType !== nextType || currentFlags !== newFlags) {\n                    changedCoords.push({ sx: x, sy: y });\n                }\n            }\n        }\n    }\n    revealAll(): void {\n        this.tiles.fill(ShroudType.Explored);\n        this._onChange.dispatch(this, { type: \"clear\" });\n    }\n    reset(): void {\n        this.tiles.fill(ShroudType.Unexplored);\n        this._onChange.dispatch(this, { type: \"cover\" });\n    }\n}\n"
  },
  {
    "path": "src/game/map/OreOverlayTypes.ts",
    "content": "import { OverlayTibType } from '@/engine/type/OverlayTibType';\nexport class OreOverlayTypes {\n    static minIdRiparius = 102;\n    static maxIdRiparius = 127;\n    static minIdCruentus = 27;\n    static maxIdCruentus = 38;\n    static minIdVinifera = 127;\n    static maxIdVinifera = 146;\n    static minIdAboreus = 147;\n    static maxIdAboreus = 166;\n    static getOverlayTibType(id: number): OverlayTibType {\n        return this.isRiparius(id)\n            ? OverlayTibType.Riparius\n            : this.isCruentus(id)\n                ? OverlayTibType.Cruentus\n                : this.isVinifera(id)\n                    ? OverlayTibType.Vinifera\n                    : this.isAboreus(id)\n                        ? OverlayTibType.Aboreus\n                        : OverlayTibType.NotSpecial;\n    }\n    static isRiparius(id: number): boolean {\n        return id >= this.minIdRiparius && id <= this.maxIdRiparius;\n    }\n    static isCruentus(id: number): boolean {\n        return id >= this.minIdCruentus && id <= this.maxIdCruentus;\n    }\n    static isVinifera(id: number): boolean {\n        return id >= this.minIdVinifera && id <= this.maxIdVinifera;\n    }\n    static isAboreus(id: number): boolean {\n        return id >= this.minIdAboreus && id <= this.maxIdAboreus;\n    }\n}\n"
  },
  {
    "path": "src/game/map/OreSpread.ts",
    "content": "import { OreOverlayTypes } from './OreOverlayTypes';\nimport { OverlayTibType } from '@/engine/type/OverlayTibType';\ninterface Tile {\n    dx: number;\n    dy: number;\n}\nexport class OreSpread {\n    static calculateOverlayId(type: OverlayTibType, tile: Tile): number | undefined {\n        if (type !== OverlayTibType.NotSpecial) {\n            let x = tile.dx;\n            const y = tile.dy;\n            x = Math.floor((((((y - 9) / 2) % 12) * (((y - 8) / 2) % 12)) % 12) -\n                (((((x - 13) / 2) % 12) * (((x - 12) / 2) % 12)) % 12) +\n                120000);\n            x %= 12;\n            switch (type) {\n                case OverlayTibType.Ore:\n                    return OreOverlayTypes.minIdRiparius + x;\n                case OverlayTibType.Gems:\n                    return OreOverlayTypes.minIdCruentus + x;\n                case OverlayTibType.Vinifera:\n                    return OreOverlayTypes.minIdVinifera + x;\n                case OverlayTibType.Aboreus:\n                    return OreOverlayTypes.minIdAboreus + x;\n                default:\n                    return undefined;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/map/Terrain.ts",
    "content": "import { TileCollection, TileDirection, Tile } from \"@/game/map/TileCollection\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { Graph, GraphNode } from \"@/util/Graph\";\nimport { PathFinder } from \"@/game/map/pathFinder/PathFinder\";\nimport { isNotNullOrUndefined } from \"@/util/typeGuard\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { rectContainsPoint } from \"@/util/geometry\";\nimport { LandType, getLandType } from \"@/game/type/LandType\";\nimport { OccupationBits } from \"@/game/rules/TerrainRules\";\nimport { MapBounds } from \"@/game/map/MapBounds\";\nimport { Rules } from \"@/game/rules/Rules\";\ninterface GameObject {\n    tile: Tile;\n    onBridge?: boolean;\n    isTerrain(): boolean;\n    isOverlay(): boolean;\n    isBridge(): boolean;\n    isTiberium(): boolean;\n    isBuilding(): boolean;\n    isAircraft(): boolean;\n    isInfantry(): boolean;\n    isVehicle(): boolean;\n    isSmudge(): boolean;\n    isHighBridge(): boolean;\n    isBridgePlaceholder(): boolean;\n    isUnit(): boolean;\n    isDestroyed: boolean;\n    rules: any;\n    art: any;\n    position: any;\n    moveTrait: any;\n}\ninterface Bridge {\n    tileElevation?: number;\n    isHighBridge(): boolean;\n}\ninterface PathNode {\n    tile: Tile;\n    onBridge?: Bridge;\n}\ninterface TileOccupation {\n    onChange: {\n        subscribe(handler: (event: {\n            tiles: Tile[];\n            object: GameObject;\n        }) => void): void;\n        unsubscribe(handler: (event: {\n            tiles: Tile[];\n            object: GameObject;\n        }) => void): void;\n    };\n    calculateTilesForGameObject(tile: Tile, object: GameObject): Tile[];\n    getBridgeOnTile(tile: Tile): Bridge | undefined;\n    getObjectsOnTile(tile: Tile): GameObject[];\n    getGroundObjectsOnTile(tile: Tile): GameObject[];\n}\ninterface NodeData {\n    tile: Tile;\n    onBridge?: Bridge;\n    islandId?: number;\n}\ninterface PathOptions {\n    maxExpandedNodes?: number;\n    bestEffort?: boolean;\n    excludeTiles?: (node: PathNode) => boolean;\n    ignoredBlockers?: GameObject[];\n}\ninterface Obstacle {\n    obj: GameObject;\n    static: boolean;\n}\nfunction calculateDistance(nodeA: GraphNode<NodeData>, nodeB: GraphNode<NodeData>): number {\n    const dx = Math.abs(nodeA.data.tile.rx - nodeB.data.tile.rx);\n    const dy = Math.abs(nodeA.data.tile.ry - nodeB.data.tile.ry);\n    return dx + dy + (Math.SQRT2 - 2) * Math.min(dx, dy);\n}\nfunction calculateHeuristic(nodeA: GraphNode<NodeData>, nodeB: GraphNode<NodeData>, pathInfo?: {\n    parent?: {\n        node: GraphNode<NodeData>;\n        dirX: number;\n        dirY: number;\n    };\n    dirX?: number;\n    dirY?: number;\n}): number {\n    const dx = Math.abs(nodeA.data.tile.rx - nodeB.data.tile.rx);\n    const dy = Math.abs(nodeA.data.tile.ry - nodeB.data.tile.ry);\n    let distance = dx + dy + (Math.SQRT2 - 2) * Math.min(dx, dy);\n    if (pathInfo?.parent) {\n        const parentNode = pathInfo.parent.node;\n        const newDirX = parentNode.data.tile.rx - nodeA.data.tile.rx;\n        const newDirY = parentNode.data.tile.ry - nodeA.data.tile.ry;\n        pathInfo.dirX = newDirX;\n        pathInfo.dirY = newDirY;\n        if (newDirX !== pathInfo.parent.dirX || newDirY !== pathInfo.parent.dirY) {\n            distance += 0.2;\n        }\n    }\n    return distance;\n}\nexport class Terrain {\n    private passabilityGraphs = new Map<string, Graph<NodeData>>();\n    private invalidatedTiles = new Map<string, Set<Tile>>();\n    constructor(private tiles: TileCollection, private theaterType: any, private mapBounds: MapBounds, private tileOccupation: TileOccupation, private rules: Rules) {\n        this.tileOccupation.onChange.subscribe(this.handleTileOccupationUpdate);\n        this.mapBounds.onLocalResize.subscribe(this.handleMapBoundsResize);\n    }\n    private handleTileOccupationUpdate = ({ tiles, object }: {\n        tiles: Tile[];\n        object: GameObject;\n    }) => {\n        const relevantTiles = tiles.filter(tile => {\n            let speedType = SpeedType.Foot;\n            let isInfantry = true;\n            if (object.isTerrain() &&\n                object.rules.getOccupationBits(this.theaterType) !== OccupationBits.All) {\n                speedType = SpeedType.Wheel;\n                isInfantry = false;\n            }\n            return (object.isOverlay() && (object.isBridge() || object.isTiberium())) ||\n                this.isBlockerObject(object, tile, false, speedType, isInfantry) ||\n                this.isBlockerObject(object, tile, true, speedType, isInfantry) ||\n                (object.isBuilding() && object.rules.leaveRubble);\n        });\n        if (relevantTiles.length) {\n            this.invalidateTiles(relevantTiles);\n        }\n    };\n    private handleMapBoundsResize = () => {\n        this.passabilityGraphs.clear();\n    };\n    private getGraphKey(speedType: SpeedType, onBridge: boolean): string {\n        return speedType + \"_\" + Number(onBridge);\n    }\n    private invalidateTiles(tiles: Tile[]): void {\n        if (!tiles.length)\n            return;\n        [...this.passabilityGraphs.keys()].forEach(graphKey => {\n            let invalidatedSet = this.invalidatedTiles.get(graphKey);\n            if (invalidatedSet) {\n                tiles.forEach(tile => invalidatedSet!.add(tile));\n            }\n            else {\n                this.invalidatedTiles.set(graphKey, new Set(tiles));\n            }\n        });\n    }\n    computePath(speedType: SpeedType, onBridge: boolean, startTile: Tile, startOnBridge: boolean, endTile: Tile, endOnBridge: boolean, options: PathOptions = {}): PathNode[] {\n        const { maxExpandedNodes = Number.POSITIVE_INFINITY, bestEffort = true, excludeTiles, ignoredBlockers = [] } = options;\n        const graph = this.computePassabilityGraph(speedType, onBridge);\n        const ignoredTiles = ignoredBlockers\n            .map(blocker => this.tileOccupation.calculateTilesForGameObject(blocker.tile, blocker))\n            .reduce((acc, tiles) => acc.concat(tiles), []);\n        if (ignoredTiles.length) {\n            this.updatePassability(ignoredTiles, speedType, onBridge, graph, ignoredBlockers);\n        }\n        const startNodeId = this.getNodeId(startTile, startOnBridge);\n        const hasStartNode = graph.hasNode(startNodeId);\n        if (!hasStartNode) {\n            graph.addNode(startNodeId, {\n                tile: startTile,\n                onBridge: this.tileOccupation.getBridgeOnTile(startTile)\n            });\n            this.updatePassability([startTile], speedType, onBridge, graph, ignoredBlockers, 1);\n        }\n        const endNodeId = this.getNodeId(endTile, endOnBridge);\n        const hasEndNode = graph.hasNode(endNodeId);\n        let finalEndTile = endTile;\n        let finalEndOnBridge = endOnBridge;\n        const useIslandCheck = hasStartNode && !ignoredTiles.length;\n        let islandChecker: ((tile: Tile, onBridge: boolean) => boolean) | undefined;\n        if (useIslandCheck) {\n            const islandIdMap = this.getIslandIdMap(speedType, onBridge);\n            const startIslandId = islandIdMap.get(startTile, startOnBridge);\n            islandChecker = (tile, onBridge) => islandIdMap.get(tile, onBridge) === startIslandId;\n        }\n        else {\n            islandChecker = (tile, onBridge) => this.getPassableSpeed(tile, speedType, onBridge, onBridge, ignoredBlockers) > 0;\n        }\n        if (!hasEndNode || !islandChecker(endTile, endOnBridge)) {\n            const fallbackTile = bestEffort ?\n                new RadialTileFinder(this.tiles, this.mapBounds, endTile, { width: 1, height: 1 }, 1, useIslandCheck ? 15 : 5, (tile) => islandChecker!(tile, false) &&\n                    Math.abs(tile.z - endTile.z) < 2 &&\n                    !excludeTiles?.({ tile, onBridge: undefined })).getNextTile() : undefined;\n            if (fallbackTile) {\n                finalEndTile = fallbackTile;\n                finalEndOnBridge = false;\n            }\n            else {\n                if (useIslandCheck) {\n                    if (ignoredTiles.length) {\n                        this.updatePassability(ignoredTiles, speedType, onBridge, graph);\n                    }\n                    return [];\n                }\n                graph.addNode(endNodeId, { tile: endTile, onBridge: undefined });\n                Math.min(maxExpandedNodes, 500);\n            }\n        }\n        const pathFinder = new PathFinder(graph, {\n            bestEffort,\n            maxExpandedNodes,\n            excludedNodes: excludeTiles,\n            distance: calculateDistance,\n            heuristic: calculateHeuristic\n        });\n        let path = pathFinder\n            .find(this.getNodeId(startTile, startOnBridge), this.getNodeId(finalEndTile, finalEndOnBridge))\n            .map((node: GraphNode<NodeData>) => ({\n            tile: node.data.tile,\n            onBridge: node.data.onBridge\n        }));\n        if ((path.length < 2) ||\n            (excludeTiles && path.length &&\n                ((!bestEffort && path[0].tile !== finalEndTile) ||\n                    path[path.length - 1].tile !== startTile))) {\n            path = [];\n        }\n        if (!hasStartNode) {\n            graph.removeNode(startNodeId);\n            this.updatePassability([startTile], speedType, onBridge, graph);\n        }\n        if (!hasEndNode) {\n            graph.removeNode(endNodeId);\n        }\n        if (ignoredTiles.length) {\n            this.updatePassability(ignoredTiles, speedType, onBridge, graph);\n        }\n        return path;\n    }\n    computeAllPassabilityGraphs(): void {\n        Object.keys(SpeedType).forEach(key => {\n            const speedType = Number(key);\n            if (!isNaN(speedType) && speedType !== SpeedType.Winged) {\n                this.computePassabilityGraph(speedType, false);\n                this.computePassabilityGraph(speedType, true);\n            }\n        });\n    }\n    private computePassabilityGraph(speedType: SpeedType, onBridge: boolean): Graph<NodeData> {\n        const graphKey = this.getGraphKey(speedType, onBridge);\n        let graph = this.passabilityGraphs.get(graphKey);\n        if (graph) {\n            const invalidatedSet = this.invalidatedTiles.get(graphKey);\n            if (invalidatedSet?.size) {\n                this.updatePassability([...invalidatedSet], speedType, onBridge, graph);\n                invalidatedSet.clear();\n                this.computeIslandIds(graph);\n            }\n        }\n        else {\n            graph = new Graph<NodeData>();\n            this.passabilityGraphs.set(graphKey, graph);\n            this.tiles.forEach(tile => {\n                this.computePassability(tile, speedType, onBridge, graph);\n            });\n            this.computeIslandIds(graph);\n        }\n        return graph;\n    }\n    private updatePassability(tiles: Tile[], speedType: SpeedType, onBridge: boolean, graph: Graph<NodeData>, ignoredBlockers: GameObject[] = [], forcePassable?: number): void {\n        const affectedTiles = new Set<Tile>();\n        tiles.forEach(tile => {\n            [\n                tile,\n                this.tiles.getNeighbourTile(tile, TileDirection.Right),\n                this.tiles.getNeighbourTile(tile, TileDirection.BottomRight),\n                this.tiles.getNeighbourTile(tile, TileDirection.Bottom),\n                this.tiles.getNeighbourTile(tile, TileDirection.BottomLeft)\n            ]\n                .filter(isNotNullOrUndefined)\n                .forEach(t => affectedTiles.add(t));\n        });\n        const savedIslandIds = new Map<string, number | undefined>();\n        tiles.forEach(tile => {\n            const nodes = [\n                graph.getNode(this.getNodeId(tile, false)),\n                graph.getNode(this.getNodeId(tile, true))\n            ];\n            for (const node of nodes) {\n                if (node) {\n                    savedIslandIds.set(node.id, node.data.islandId);\n                    graph.removeNode(node.id);\n                }\n            }\n        });\n        affectedTiles.forEach(tile => {\n            this.computePassability(tile, speedType, onBridge, graph, ignoredBlockers, forcePassable && tiles.includes(tile) ? forcePassable : undefined);\n        });\n        savedIslandIds.forEach((islandId, nodeId) => {\n            const node = graph.getNode(nodeId);\n            if (node) {\n                node.data.islandId = islandId;\n            }\n        });\n    }\n    private computePassability(tile: Tile, speedType: SpeedType, onBridge: boolean, graph: Graph<NodeData>, ignoredBlockers: GameObject[] = [], forcePassable?: number): void {\n        const directions = [\n            TileDirection.Left,\n            TileDirection.TopLeft,\n            TileDirection.Top,\n            TileDirection.TopRight\n        ];\n        if (forcePassable || this.getPassableSpeed(tile, speedType, onBridge, false, ignoredBlockers)) {\n            const nodeId = this.getNodeId(tile, false);\n            if (!graph.hasNode(nodeId)) {\n                graph.addNode(nodeId, { tile, onBridge: undefined });\n            }\n            for (const direction of directions) {\n                this.connectTiles(tile, undefined, direction, speedType, onBridge, graph, ignoredBlockers);\n            }\n        }\n        const bridge = this.tileOccupation.getBridgeOnTile(tile);\n        if (bridge && (forcePassable || this.getPassableSpeed(tile, speedType, onBridge, true, ignoredBlockers))) {\n            const nodeId = this.getNodeId(tile, true);\n            if (!graph.hasNode(nodeId)) {\n                graph.addNode(nodeId, { tile, onBridge: bridge });\n            }\n            for (const direction of directions) {\n                this.connectTiles(tile, bridge, direction, speedType, onBridge, graph, ignoredBlockers);\n            }\n        }\n    }\n    private connectTiles(tile: Tile, bridge: Bridge | undefined, direction: TileDirection, speedType: SpeedType, onBridge: boolean, graph: Graph<NodeData>, ignoredBlockers: GameObject[] = []): void {\n        const neighborTile = this.tiles.getNeighbourTile(tile, direction);\n        if (!neighborTile)\n            return;\n        let neighborBridge = this.tileOccupation.getBridgeOnTile(neighborTile);\n        const maxElevationDiff = (bridge || neighborBridge) ? 0 : 1;\n        const elevationDiff = Math.abs(tile.z + (bridge?.tileElevation ?? 0) -\n            (neighborTile.z + (neighborBridge?.tileElevation ?? 0)));\n        if (elevationDiff > maxElevationDiff) {\n            if ((!neighborBridge?.isHighBridge() && !bridge?.isHighBridge()) ||\n                Math.abs(tile.z - neighborTile.z) !== 0 ||\n                !graph.hasNode(this.getNodeId(tile, false))) {\n                return;\n            }\n            bridge = neighborBridge = undefined;\n        }\n        if (!this.getPassableSpeed(neighborTile, speedType, onBridge, !!neighborBridge, ignoredBlockers)) {\n            return;\n        }\n        const neighborNodeId = this.getNodeId(neighborTile, !!neighborBridge);\n        const neighborNode = graph.getNode(neighborNodeId) ??\n            graph.addNode(neighborNodeId, { tile: neighborTile, onBridge: neighborBridge });\n        const currentNodeId = this.getNodeId(tile, !!bridge);\n        const currentNode = graph.getNode(currentNodeId);\n        if (currentNode) {\n            currentNode.addLink(neighborNode);\n        }\n    }\n    private getNodeId(tile: Tile, onBridge: boolean): string {\n        return tile.id + (onBridge ? \"_bridge\" : \"\");\n    }\n    private computeIslandIds(graph: Graph<NodeData>): void {\n        let islandId = 1;\n        graph.forEachNode(node => {\n            node.data.islandId = undefined;\n        });\n        graph.forEachNode(node => {\n            if (!node.data.islandId) {\n                this.floodIslandId(node, islandId++);\n            }\n        });\n    }\n    private floodIslandId(startNode: GraphNode<NodeData>, islandId: number): void {\n        const queue = [startNode];\n        while (queue.length) {\n            const node = queue.pop()!;\n            node.data.islandId = islandId;\n            for (const neighbor of node.neighbors) {\n                if (!neighbor.data.islandId) {\n                    queue.push(neighbor);\n                }\n            }\n        }\n    }\n    private getIslandIdMap(speedType: SpeedType, onBridge: boolean) {\n        const graph = this.computePassabilityGraph(speedType, onBridge);\n        return {\n            get: (tile: Tile, onBridge: boolean): number | undefined => {\n                const nodeId = this.getNodeId(tile, onBridge);\n                return graph.getNode(nodeId)?.data.islandId;\n            }\n        };\n    }\n    public getPassableSpeed(tile: Tile, speedType: SpeedType, onBridge: boolean, bridgeLevel: boolean, ignoredBlockers: GameObject[] = [], skipBlockerCheck = false): number {\n        if (!this.mapBounds.isWithinBounds(tile))\n            return 0;\n        let landType = bridgeLevel ? tile.onBridgeLandType : tile.landType;\n        if (landType === undefined)\n            return 0;\n        if (landType === LandType.Wall && speedType === SpeedType.Track) {\n            landType = getLandType(tile.terrainType);\n        }\n        const landRules = this.rules.getLandRules(landType);\n        const speedModifier = landRules.getSpeedModifier(speedType);\n        if (!speedModifier)\n            return 0;\n        if (!skipBlockerCheck) {\n            for (const obj of this.tileOccupation.getObjectsOnTile(tile)) {\n                if (this.isBlockerObject(obj, tile, bridgeLevel, speedType, onBridge) &&\n                    !ignoredBlockers.includes(obj)) {\n                    return 0;\n                }\n            }\n        }\n        return speedModifier;\n    }\n    private isBlockerObject(obj: GameObject, tile: Tile, bridgeLevel: boolean, speedType: SpeedType, isInfantry: boolean): boolean {\n        if (obj.isTerrain() && isInfantry &&\n            obj.rules.getOccupationBits(this.theaterType) !== OccupationBits.All) {\n            return false;\n        }\n        if (obj.isBuilding()) {\n            if (obj.rules.invisibleInGame)\n                return false;\n            if (obj.isDestroyed && obj.rules.leaveRubble)\n                return false;\n            if (obj.rules.gate)\n                return false;\n            const foundation = obj.art.foundation;\n            let impassableRows = obj.rules.numberImpassableRows;\n            if (isInfantry) {\n                impassableRows = foundation.width;\n            }\n            else if (obj.rules.weaponsFactory && !impassableRows) {\n                impassableRows = foundation.width - 1;\n            }\n            const rect = {\n                x: obj.tile.rx,\n                y: obj.tile.ry,\n                width: (impassableRows || foundation.width) - 1,\n                height: foundation.height - 1\n            };\n            return rectContainsPoint(rect, { x: tile.rx, y: tile.ry });\n        }\n        if (obj.isAircraft() || obj.isInfantry() || obj.isVehicle() || obj.isSmudge()) {\n            return false;\n        }\n        if (obj.isOverlay()) {\n            if ((bridgeLevel && obj.isBridge()) ||\n                (!bridgeLevel && obj.isHighBridge()) ||\n                obj.isTiberium() ||\n                obj.rules.crate ||\n                obj.isBridgePlaceholder()) {\n                return false;\n            }\n        }\n        if ([SpeedType.Track, SpeedType.Hover].includes(speedType) && obj.rules.crushable) {\n            return false;\n        }\n        return true;\n    }\n    findObstacles(pathNode: PathNode, unit: GameObject): Obstacle[] {\n        const speedType = unit.rules.speedType;\n        const isInfantry = unit.isInfantry();\n        const obstacles: Obstacle[] = [];\n        for (const obj of this.tileOccupation.getGroundObjectsOnTile(pathNode.tile)) {\n            if (obj === unit)\n                continue;\n            const isStaticBlocker = this.isBlockerObject(obj, pathNode.tile, !!pathNode.onBridge, speedType, isInfantry);\n            let shouldInclude = false;\n            if (isStaticBlocker) {\n                shouldInclude = true;\n            }\n            else if (obj.isUnit()) {\n                const sameLocation = (obj.tile === pathNode.tile && obj.onBridge === !!pathNode.onBridge);\n                const inReservedPath = obj.moveTrait.reservedPathNodes.find((node: PathNode) => node.tile === pathNode.tile && !!node.onBridge === !!pathNode.onBridge);\n                if (sameLocation || inReservedPath) {\n                    shouldInclude = true;\n                }\n            }\n            else if ([SpeedType.Track, SpeedType.Hover].includes(speedType) && obj.rules.crushable) {\n                shouldInclude = true;\n            }\n            else if (isInfantry && obj.isTerrain()) {\n                shouldInclude = true;\n            }\n            else if (obj.isBuilding() && obj.rules.gate) {\n                shouldInclude = true;\n            }\n            if (shouldInclude) {\n                const obstacle: Obstacle = { obj, static: isStaticBlocker };\n                if (obj.isInfantry() && isInfantry) {\n                    if (obj.position.desiredSubCell === unit.position.desiredSubCell) {\n                        obstacles.push(obstacle);\n                    }\n                }\n                else {\n                    const skipForInfantryTerrain = obj.isTerrain() &&\n                        isInfantry &&\n                        !obj.rules.getOccupiedSubCells(this.theaterType).includes(unit.position.desiredSubCell);\n                    if (!skipForInfantryTerrain) {\n                        obstacles.push(obstacle);\n                    }\n                }\n            }\n        }\n        return obstacles;\n    }\n    dispose(): void {\n        this.tileOccupation.onChange.unsubscribe(this.handleTileOccupationUpdate);\n        this.mapBounds.onLocalResize.unsubscribe(this.handleMapBoundsResize);\n    }\n}\n"
  },
  {
    "path": "src/game/map/Tile.ts",
    "content": "export type { Tile } from './TileCollection';\n"
  },
  {
    "path": "src/game/map/TileCollection.ts",
    "content": "import { LandType, getLandType } from '@/game/type/LandType';\nimport { TerrainType } from '@/engine/type/TerrainType';\nimport { isNotNullOrUndefined } from '@/util/typeGuard';\nexport enum TileDirection {\n    Top = 0,\n    TopLeft = 1,\n    TopRight = 2,\n    Left = 3,\n    Right = 4,\n    BottomLeft = 5,\n    Bottom = 6,\n    BottomRight = 7\n}\ninterface TileData {\n    rx: number;\n    ry: number;\n    dx: number;\n    dy: number;\n    z: number;\n    tileNum: number;\n    subTile: number;\n}\ninterface TileImage {\n    terrainType: TerrainType;\n    rampType: number;\n    height: number;\n    radarLeft: {\n        clone(): {\n            multiplyScalar(factor: number): any;\n        };\n    };\n}\ninterface TileSets {\n    getTileImage(tileNum: number, subTile: number, randomIndexSelector: (min: number, max: number) => number): TileImage;\n    isCliffTile(tileNum: number): boolean;\n    isHighBridgeBoundaryTile(tileNum: number): boolean;\n}\ninterface GeneralRules {\n    cliffBackImpassability: number;\n}\ninterface Size {\n    width: number;\n    height: number;\n}\ninterface Rectangle {\n    x?: number;\n    y?: number;\n    rx?: number;\n    ry?: number;\n    width: number;\n    height: number;\n}\nexport interface Tile extends TileData {\n    terrainType: TerrainType;\n    landType: LandType;\n    onBridgeLandType: LandType | undefined;\n    rampType: number;\n    id: string;\n    occluded: boolean;\n}\nexport class TileCollection {\n    private tileSets: TileSets;\n    private generalRules: GeneralRules;\n    private rSize: Size;\n    private dSize: Size;\n    private tilesByRxy: (Tile | undefined)[];\n    private tilesByDxy: (Tile | undefined)[];\n    private tiles: Tile[];\n    private bridgeSetTiles: Tile[];\n    private minTileHeight: number;\n    private maxTileHeight: number;\n    private cutoffTileHeight: number;\n    constructor(tileData: TileData[], tileSets: TileSets, generalRules: GeneralRules, randomIndexSelector: (min: number, max: number) => number) {\n        this.tileSets = tileSets;\n        this.generalRules = generalRules;\n        const rSize = this.rSize = { width: 0, height: 0 };\n        const dSize = this.dSize = { width: 0, height: 0 };\n        for (let i = 0, len = tileData.length; i < len; ++i) {\n            rSize.width = Math.max(rSize.width, tileData[i].rx);\n            rSize.height = Math.max(rSize.height, tileData[i].ry);\n            dSize.width = Math.max(dSize.width, tileData[i].dx);\n            dSize.height = Math.max(dSize.height, tileData[i].dy);\n        }\n        rSize.width++;\n        rSize.height++;\n        dSize.width++;\n        dSize.height++;\n        const tilesByRxy = this.tilesByRxy = new Array<Tile | undefined>(rSize.width * rSize.height);\n        tilesByRxy.fill(undefined);\n        const tilesByDxy = this.tilesByDxy = new Array<Tile | undefined>(dSize.width * dSize.height);\n        tilesByDxy.fill(undefined);\n        const tiles = this.tiles = new Array<Tile>(tileData.length);\n        const cliffTiles: Tile[] = [];\n        const bridgeSetTiles = this.bridgeSetTiles = [];\n        const terrainTypes = new Set(Object.values(TerrainType));\n        this.minTileHeight = Number.POSITIVE_INFINITY;\n        this.maxTileHeight = 0;\n        for (let i = 0, len = tileData.length; i < len; ++i) {\n            const tileDataItem = tileData[i];\n            const tileImage = tileSets.getTileImage(tileDataItem.tileNum, tileDataItem.subTile, randomIndexSelector);\n            const terrainType = tileImage.terrainType;\n            if (!terrainTypes.has(terrainType)) {\n                throw new Error(`Tile (${tileDataItem.rx}, ${tileDataItem.ry}) has unknown terrain type \"${terrainType}\"`);\n            }\n            const tile: Tile = {\n                ...tileDataItem,\n                terrainType,\n                landType: getLandType(terrainType),\n                onBridgeLandType: undefined,\n                rampType: tileImage.rampType,\n                id: tileDataItem.rx + \"_\" + tileDataItem.ry,\n                occluded: false\n            };\n            const rx = tile.rx;\n            const ry = tile.ry;\n            const dx = tile.dx;\n            const dy = tile.dy;\n            tiles[i] = tile;\n            tilesByRxy[rx + ry * rSize.width] = tile;\n            tilesByDxy[dx + dy * dSize.width] = tile;\n            this.minTileHeight = Math.min(this.minTileHeight, tile.z);\n            this.maxTileHeight = Math.max(this.maxTileHeight, tile.z);\n            if (tileImage.height === 4 &&\n                (tile.terrainType === TerrainType.Cliff || tileSets.isCliffTile(tile.tileNum))) {\n                cliffTiles.push(tile);\n            }\n            if (tileSets.isHighBridgeBoundaryTile(tileDataItem.tileNum)) {\n                bridgeSetTiles.push(tile);\n            }\n        }\n        this.computeLandBehindCliffTiles(cliffTiles);\n        this.cutoffTileHeight = this.computeCutoffTileHeight();\n    }\n    private computeLandBehindCliffTiles(cliffTiles: Tile[]): void {\n        if (this.generalRules.cliffBackImpassability < 2) {\n            return;\n        }\n        const offsets: [\n            number,\n            number\n        ][] = [\n            [-2, -2],\n            [-1, -1],\n            [-1, 1],\n            [1, -1],\n            [0, 1],\n            [1, 0]\n        ];\n        cliffTiles.forEach((cliffTile) => {\n            for (const [offsetX, offsetY] of offsets) {\n                const neighborTile = this.getByMapCoords(cliffTile.rx + offsetX, cliffTile.ry + offsetY);\n                if (neighborTile &&\n                    neighborTile.z < cliffTile.z &&\n                    neighborTile.terrainType !== TerrainType.Cliff &&\n                    neighborTile.terrainType !== TerrainType.Rough &&\n                    neighborTile.rampType === 0) {\n                    neighborTile.landType = LandType.Rock;\n                }\n            }\n        });\n    }\n    getTileRadarColor(tile: Tile): any {\n        const tileImage = this.tileSets.getTileImage(tile.tileNum, tile.subTile, () => 0);\n        return tileImage.radarLeft.clone().multiplyScalar(0.5);\n    }\n    getAll(): Tile[] {\n        return [...this.tiles];\n    }\n    forEach(callback: (tile: Tile, index: number) => void): void {\n        for (let i = 0, len = this.tiles.length; i < len; ++i) {\n            callback(this.tiles[i], i);\n        }\n    }\n    reduce<T>(reducer: (accumulator: T, tile: Tile) => T, initialValue: T): T {\n        let result = initialValue;\n        this.forEach((tile) => {\n            result = reducer(result, tile);\n        });\n        return result;\n    }\n    getMinTileHeight(): number {\n        return this.minTileHeight;\n    }\n    getMaxTileHeight(): number {\n        return this.maxTileHeight;\n    }\n    getCutoffTileHeight(): number {\n        return this.cutoffTileHeight;\n    }\n    private computeCutoffTileHeight(): number {\n        const maxWidth = this.dSize.width - 1;\n        let maxHeight = this.dSize.height - 1;\n        let maxZ = 0;\n        let shouldContinue = true;\n        while (shouldContinue && maxHeight > 0) {\n            for (let x = 1; x < maxWidth - 3; x++) {\n                const tile = this.getByDisplayCoords(x, maxHeight);\n                if (tile) {\n                    shouldContinue = false;\n                    if (tile.z > maxZ) {\n                        maxZ = tile.z;\n                    }\n                }\n            }\n            if (shouldContinue) {\n                maxHeight--;\n            }\n        }\n        return maxZ;\n    }\n    getAllBridgeSetTiles(): Tile[] {\n        return this.bridgeSetTiles;\n    }\n    getAllNeighbourTiles(tile: Tile): Tile[] {\n        const rx = tile.rx;\n        const ry = tile.ry;\n        return [\n            this.getByMapCoords(rx + 1, ry + 1),\n            this.getByMapCoords(rx - 1, ry - 1),\n            this.getByMapCoords(rx - 1, ry + 1),\n            this.getByMapCoords(rx + 1, ry - 1),\n            this.getByMapCoords(rx, ry + 1),\n            this.getByMapCoords(rx + 1, ry),\n            this.getByMapCoords(rx - 1, ry),\n            this.getByMapCoords(rx, ry - 1)\n        ].filter(isNotNullOrUndefined);\n    }\n    getNeighbourTile(tile: Tile, direction: TileDirection): Tile | undefined {\n        const rx = tile.rx;\n        const ry = tile.ry;\n        switch (direction) {\n            case TileDirection.Bottom:\n                return this.getByMapCoords(rx + 1, ry + 1);\n            case TileDirection.Top:\n                return this.getByMapCoords(rx - 1, ry - 1);\n            case TileDirection.Left:\n                return this.getByMapCoords(rx - 1, ry + 1);\n            case TileDirection.Right:\n                return this.getByMapCoords(rx + 1, ry - 1);\n            case TileDirection.BottomLeft:\n                return this.getByMapCoords(rx, ry + 1);\n            case TileDirection.BottomRight:\n                return this.getByMapCoords(rx + 1, ry);\n            case TileDirection.TopLeft:\n                return this.getByMapCoords(rx - 1, ry);\n            case TileDirection.TopRight:\n                return this.getByMapCoords(rx, ry - 1);\n            default:\n                throw new Error(\"Invalid direction\");\n        }\n    }\n    getByDisplayCoords(x: number, y: number): Tile | undefined {\n        if (x >= this.dSize.width || y >= this.dSize.height) {\n            return undefined;\n        }\n        return this.tilesByDxy[x + y * this.dSize.width];\n    }\n    getByMapCoords(x: number, y: number): Tile | undefined {\n        if (x >= this.rSize.width || y >= this.rSize.height) {\n            return undefined;\n        }\n        return this.tilesByRxy[x + y * this.rSize.width];\n    }\n    getMapSize(): Size {\n        return this.rSize;\n    }\n    getDisplaySize(): Size {\n        return this.dSize;\n    }\n    getInRectangle(rectangle: Rectangle, size?: Size): Tile[] {\n        let startX: number;\n        let startY: number;\n        let width: number;\n        let height: number;\n        if (size) {\n            startX = rectangle.rx!;\n            startY = rectangle.ry!;\n            width = size.width;\n            height = size.height;\n        }\n        else {\n            startX = rectangle.x!;\n            startY = rectangle.y!;\n            width = rectangle.width;\n            height = rectangle.height;\n        }\n        const result: Tile[] = [];\n        for (let dx = 0; dx < width; dx++) {\n            for (let dy = 0; dy < height; dy++) {\n                const x = startX + dx;\n                const y = startY + dy;\n                const tile = this.getByMapCoords(x, y);\n                if (tile) {\n                    result.push(tile);\n                }\n            }\n        }\n        return result;\n    }\n    getPlaceholderTile(rx: number, ry: number): Tile {\n        const referenceTile = this.tiles[0];\n        const offset = referenceTile.dx - referenceTile.rx + referenceTile.ry + 1;\n        return {\n            rx,\n            ry,\n            dx: rx - ry + offset - 1,\n            dy: rx + ry - offset - 1,\n            z: 0,\n            id: rx + \"_\" + ry,\n            landType: LandType.Rock,\n            terrainType: TerrainType.Rock1,\n            rampType: 0,\n            subTile: 0,\n            tileNum: 0,\n            occluded: false,\n            onBridgeLandType: undefined\n        };\n    }\n}\n"
  },
  {
    "path": "src/game/map/TileOcclusion.ts",
    "content": "import * as m from \"@/game/math/Vector2\";\nexport class TileOcclusion {\n    tiles: any;\n    tileOcclusion: any[][];\n    constructor(e: any) {\n        this.tiles = e;\n        this.tileOcclusion = [];\n        let t = this.tileOcclusion;\n        for (var i of e.getAll())\n            (t[i.rx] = t[i.rx] || []), (t[i.rx][i.ry] = new Set());\n    }\n    addOccluder(t: any) {\n        let e = this.calculateTilesForGameObject(t);\n        e.forEach((e: any) => this.occludeTile(e, t));\n    }\n    removeOccluder(t: any) {\n        let e = this.calculateTilesForGameObject(t);\n        e.forEach((e: any) => this.unoccludeTile(e, t));\n    }\n    calculateTilesForGameObject(e: any) {\n        var t = e.art.occupyHeight, i = Math.max(0, t - 2);\n        let r = [];\n        var s = e.getFoundation();\n        for (let u = 1; u <= i; u++)\n            for (let e = 0; e < s.width; e++)\n                r.push(new m.Vector2(e - u, -u));\n        for (let d = 1; d <= i; d++)\n            for (let e = 1; e < s.height; e++)\n                r.push(new m.Vector2(-d, e - d));\n        r.push(...e.art.addOccupy);\n        for (let { x: g, y: p } of e.art.removeOccupy) {\n            var a = r.findIndex((e: any) => e.x === g && e.y === p);\n            -1 !== a && r.splice(a, 1);\n        }\n        var n: any, o: any, l = e.tile;\n        let c = [];\n        for ({ x: n, y: o } of r) {\n            var h = this.tiles.getByMapCoords(l.rx + n, l.ry + o);\n            h && c.push(h);\n        }\n        return c;\n    }\n    occludeTile(e: any, t: any) {\n        this.tileOcclusion[e.rx][e.ry].add(t);\n        e.occluded = true;\n    }\n    unoccludeTile(e: any, t: any) {\n        let i = this.tileOcclusion[e.rx][e.ry];\n        i.delete(t);\n        e.occluded = 0 < i.size;\n    }\n    isTileOccluded(e: any) {\n        return 0 < this.tileOcclusion[e.rx][e.ry].size;\n    }\n}\n"
  },
  {
    "path": "src/game/map/TileOccupation.ts",
    "content": "import { LandType, getLandType } from '@/game/type/LandType';\nimport { EventDispatcher } from '@/util/event';\nimport { ZoneType, getZoneType } from '@/game/gameobject/unit/ZoneType';\nexport enum LayerType {\n    All = 0,\n    Ground = 1,\n    Air = 2\n}\nexport class TileOccupation {\n    private tiles: any;\n    private tileOccupation: Set<any>[][];\n    private emptyTiles: Set<any>;\n    private _onChange: EventDispatcher<TileOccupation>;\n    get onChange() {\n        return this._onChange.asEvent();\n    }\n    constructor(tiles: any) {\n        this.tiles = tiles;\n        this.tileOccupation = [];\n        this.emptyTiles = new Set();\n        this._onChange = new EventDispatcher();\n        let occupation = this.tileOccupation;\n        for (const tile of tiles.getAll()) {\n            occupation[tile.rx] = occupation[tile.rx] || [];\n            occupation[tile.rx][tile.ry] = new Set();\n            this.emptyTiles.add(tile);\n        }\n    }\n    occupyTileRange(pos: any, obj: any) {\n        const tiles = this.calculateTilesForGameObject(pos, obj);\n        tiles.forEach(tile => this.occupyTile(tile, obj));\n        this._onChange.dispatch(this, {\n            tiles,\n            object: obj,\n            type: 'added'\n        });\n    }\n    unoccupyTileRange(pos: any, obj: any) {\n        const tiles = this.calculateTilesForGameObject(pos, obj);\n        tiles.forEach(tile => this.unoccupyTile(tile, obj));\n        this._onChange.dispatch(this, {\n            tiles,\n            object: obj,\n            type: 'removed'\n        });\n    }\n    occupySingleTile(tile: any, obj: any) {\n        this.occupyTile(tile, obj);\n        this._onChange.dispatch(this, {\n            tiles: [tile],\n            object: obj,\n            type: 'added'\n        });\n    }\n    unoccupySingleTile(tile: any, obj: any) {\n        this.unoccupyTile(tile, obj);\n        this._onChange.dispatch(this, {\n            tiles: [tile],\n            object: obj,\n            type: 'removed'\n        });\n    }\n    calculateTilesForGameObject(pos: any, obj: any) {\n        return this.tiles.getInRectangle(pos, obj.getFoundation());\n    }\n    occupyTile(tile: any, obj: any) {\n        const occupation = this.tileOccupation[tile.rx]?.[tile.ry];\n        if (occupation) {\n            occupation.add(obj);\n            this.emptyTiles.delete(tile);\n            tile.landType = this.computeTileLandType(tile);\n            tile.onBridgeLandType = this.computeOnBridgeLandType(tile);\n        }\n    }\n    unoccupyTile(tile: any, obj: any) {\n        const occupation = this.tileOccupation[tile.rx]?.[tile.ry];\n        if (occupation) {\n            occupation.delete(obj);\n            if (!occupation.size) {\n                this.emptyTiles.add(tile);\n            }\n            tile.landType = this.computeTileLandType(tile);\n            tile.onBridgeLandType = this.computeOnBridgeLandType(tile);\n        }\n    }\n    isTileOccupiedBy(tile: any, obj: any): boolean {\n        return !!this.tileOccupation[tile.rx]?.[tile.ry]?.has(obj);\n    }\n    computeTileLandType(tile: any): LandType {\n        if (tile.landType === LandType.Rock)\n            return LandType.Rock;\n        const baseLandType = getLandType(tile.terrainType);\n        for (const obj of this.tileOccupation[tile.rx]?.[tile.ry] ?? []) {\n            if ((obj.isOverlay() || obj.isBuilding()) && obj.rules.wall) {\n                return LandType.Wall;\n            }\n            if (obj.isOverlay() && obj.isTiberium()) {\n                return LandType.Tiberium;\n            }\n            if (obj.isOverlay() &&\n                obj.rules.land !== LandType.Clear &&\n                !obj.isBridge() &&\n                !obj.isBridgePlaceholder()) {\n                return obj.rules.land;\n            }\n        }\n        return baseLandType;\n    }\n    computeOnBridgeLandType(tile: any): LandType | undefined {\n        for (const obj of this.tileOccupation[tile.rx]?.[tile.ry] ?? []) {\n            if (obj.isOverlay() && obj.isBridge()) {\n                return obj.isHighBridge() ? LandType.Road : obj.rules.land;\n            }\n        }\n    }\n    getTileZone(tile: any, useBaseLandType: boolean = false): ZoneType {\n        return getZoneType(useBaseLandType ? tile.landType : (tile.onBridgeLandType ?? tile.landType));\n    }\n    getBridgeOnTile(tile: any) {\n        for (const obj of this.tileOccupation[tile.rx]?.[tile.ry] ?? []) {\n            if (obj.isOverlay() && obj.isBridge()) {\n                return obj;\n            }\n        }\n    }\n    getObjectsOnTile(tile: any): any[] {\n        return [...(this.tileOccupation[tile.rx]?.[tile.ry] ?? [])];\n    }\n    getGroundObjectsOnTile(tile: any): any[] {\n        const objects: any[] = [];\n        for (const obj of this.tileOccupation[tile.rx]?.[tile.ry] ?? []) {\n            if (!(obj.isTechno() && !obj.isBuilding() && obj.zone === ZoneType.Air)) {\n                objects.push(obj);\n            }\n        }\n        return objects;\n    }\n    getAirObjectsOnTile(tile: any): any[] {\n        const objects: any[] = [];\n        for (const obj of this.tileOccupation[tile.rx]?.[tile.ry] ?? []) {\n            if (obj.isUnit() && obj.zone === ZoneType.Air) {\n                objects.push(obj);\n            }\n        }\n        return objects;\n    }\n    getObjectsOnTileByLayer(tile: any, layer: LayerType): any[] {\n        switch (layer) {\n            case LayerType.Ground:\n                return this.getGroundObjectsOnTile(tile);\n            case LayerType.Air:\n                return this.getAirObjectsOnTile(tile);\n            case LayerType.All:\n                return this.getObjectsOnTile(tile);\n            default:\n                throw new Error(`Unhandled layer type \"${layer}\"`);\n        }\n    }\n    getEmptyTiles(): any[] {\n        return [...this.emptyTiles];\n    }\n}\n"
  },
  {
    "path": "src/game/map/pathFinder/NodeHeap.ts",
    "content": "interface Node {\n    fScore: number;\n    heapIndex: number;\n}\nexport class NodeHeap {\n    private data: Node[];\n    public length: number;\n    constructor(initialData: Node[] = []) {\n        this.data = initialData;\n        this.length = initialData.length;\n        if (this.length > 0) {\n            for (let i = this.length >> 1; i >= 0; i--) {\n                this.down(i);\n            }\n        }\n        for (let i = 0; i < this.length; ++i) {\n            this.setNodeId(this.data[i], i);\n        }\n    }\n    private compare(a: Node, b: Node): number {\n        return a.fScore - b.fScore;\n    }\n    private setNodeId(node: Node, index: number): void {\n        node.heapIndex = index;\n    }\n    push(node: Node): void {\n        this.data.push(node);\n        this.setNodeId(node, this.length);\n        this.length++;\n        this.up(this.length - 1);\n    }\n    pop(): Node | undefined {\n        if (this.length === 0) {\n            return undefined;\n        }\n        const result = this.data[0];\n        this.length--;\n        if (this.length > 0) {\n            this.data[0] = this.data[this.length];\n            this.setNodeId(this.data[0], 0);\n            this.down(0);\n        }\n        this.data.pop();\n        return result;\n    }\n    peek(): Node | undefined {\n        return this.data[0];\n    }\n    updateItem(index: number): void {\n        this.down(index);\n        this.up(index);\n    }\n    private up(index: number): void {\n        const data = this.data;\n        const item = data[index];\n        while (index > 0) {\n            const parentIndex = (index - 1) >> 1;\n            const parent = data[parentIndex];\n            if (this.compare(item, parent) >= 0) {\n                break;\n            }\n            data[index] = parent;\n            this.setNodeId(parent, index);\n            index = parentIndex;\n        }\n        data[index] = item;\n        this.setNodeId(item, index);\n    }\n    private down(index: number): void {\n        const data = this.data;\n        const halfLength = this.length >> 1;\n        const item = data[index];\n        while (index < halfLength) {\n            let childIndex = 1 + (index << 1);\n            const rightChildIndex = childIndex + 1;\n            let child = data[childIndex];\n            if (rightChildIndex < this.length && this.compare(data[rightChildIndex], child) < 0) {\n                childIndex = rightChildIndex;\n                child = data[rightChildIndex];\n            }\n            if (this.compare(child, item) >= 0) {\n                break;\n            }\n            data[index] = child;\n            this.setNodeId(child, index);\n            index = childIndex;\n        }\n        data[index] = item;\n        this.setNodeId(item, index);\n    }\n}\n"
  },
  {
    "path": "src/game/map/pathFinder/PathFinder.ts",
    "content": "import { NodeHeap } from './NodeHeap';\nimport { SearchStatePool } from './SearchStatePool';\ninterface PathFinderOptions {\n    bestEffort?: boolean;\n    maxExpandedNodes?: number;\n    heuristic?: (from: any, to: any, state?: any) => number;\n    distance?: (from: any, to: any) => number;\n    excludedNodes?: (data: any) => boolean;\n}\ninterface SearchState {\n    node: any;\n    parent: SearchState | null;\n    fScore: number;\n    distanceToSource: number;\n    open: number;\n    closed: boolean;\n    heapIndex: number;\n}\ninterface Graph {\n    getNode(id: string): any;\n}\nexport class PathFinder {\n    private readonly bestEffort: boolean;\n    private readonly maxExpandedNodes: number;\n    private readonly heuristic: (from: any, to: any, state?: any) => number;\n    private readonly distance: (from: any, to: any) => number;\n    private readonly excludedNodes?: (data: any) => boolean;\n    private readonly searchStatePool: SearchStatePool;\n    private readonly graph: Graph;\n    constructor(graph: Graph, options: PathFinderOptions = {}) {\n        this.bestEffort = options.bestEffort ?? false;\n        this.maxExpandedNodes = options.maxExpandedNodes ?? Number.POSITIVE_INFINITY;\n        this.heuristic = options.heuristic ?? (() => 0);\n        this.distance = options.distance ?? (() => 1);\n        this.excludedNodes = options.excludedNodes;\n        this.searchStatePool = new SearchStatePool();\n        this.graph = graph;\n    }\n    private reconstructPath(state: SearchState): any[] {\n        const path = [state.node];\n        let current = state.parent;\n        while (current) {\n            path.push(current.node);\n            current = current.parent;\n        }\n        return path;\n    }\n    find(fromId: string, toId: string): any[] {\n        const fromNode = this.graph.getNode(fromId);\n        if (!fromNode) {\n            throw new Error(`fromId is not defined in this graph: ${fromId}`);\n        }\n        const toNode = this.graph.getNode(toId);\n        if (!toNode) {\n            throw new Error(`toId is not defined in this graph: ${toId}`);\n        }\n        if (fromNode === toNode) {\n            return [];\n        }\n        this.searchStatePool.reset();\n        const states = new Map<string, SearchState>();\n        const openSet = new NodeHeap();\n        const startState = this.searchStatePool.createNewState(fromNode);\n        states.set(fromId, startState);\n        startState.fScore = this.excludedNodes?.(toNode.data)\n            ? Number.POSITIVE_INFINITY\n            : this.heuristic(fromNode, toNode);\n        if (!Number.isFinite(startState.fScore) && fromNode.neighbors.has(toNode)) {\n            return [];\n        }\n        startState.distanceToSource = 0;\n        openSet.push(startState);\n        startState.open = 1;\n        let current: SearchState;\n        let bestState = startState;\n        let expandedCount = 0;\n        while (openSet.length > 0) {\n            current = openSet.pop()! as unknown as SearchState;\n            if (current.node === toNode) {\n                return this.reconstructPath(current);\n            }\n            expandedCount++;\n            if (expandedCount > this.maxExpandedNodes) {\n                break;\n            }\n            current.closed = true;\n            current.node.neighbors.forEach((neighbor: any) => {\n                let neighborState = states.get(neighbor.id);\n                if (!neighborState) {\n                    neighborState = this.searchStatePool.createNewState(neighbor);\n                    states.set(neighbor.id, neighborState);\n                }\n                if (neighborState.closed) {\n                    return;\n                }\n                if (neighborState.open === 0) {\n                    openSet.push(neighborState);\n                    neighborState.open = 1;\n                }\n                const tentativeDistance = this.excludedNodes?.(neighbor.data)\n                    ? Number.POSITIVE_INFINITY\n                    : current.distanceToSource + this.distance(current.node, neighbor);\n                if (tentativeDistance >= neighborState.distanceToSource) {\n                    return;\n                }\n                neighborState.parent = current;\n                neighborState.distanceToSource = tentativeDistance;\n                if (this.excludedNodes?.(toNode.data)) {\n                    neighborState.fScore = Number.POSITIVE_INFINITY;\n                }\n                else {\n                    neighborState.fScore = tentativeDistance + this.heuristic(neighborState.node, toNode, neighborState);\n                }\n                if (neighborState.fScore - neighborState.distanceToSource < bestState.fScore - bestState.distanceToSource) {\n                    bestState = neighborState;\n                }\n                openSet.updateItem(neighborState.heapIndex);\n            });\n        }\n        return this.bestEffort ? this.reconstructPath(bestState) : [];\n    }\n}\n"
  },
  {
    "path": "src/game/map/pathFinder/SearchStatePool.ts",
    "content": "interface SearchState {\n    node: any;\n    parent: SearchState | undefined;\n    closed: boolean;\n    open: number;\n    distanceToSource: number;\n    fScore: number;\n    heapIndex: number;\n}\nclass SearchStatePool {\n    private index: number = 0;\n    private pool: SearchState[] = [];\n    createNewState(node: any): SearchState {\n        let state = this.pool[this.index];\n        if (state) {\n            state.node = node;\n            state.parent = undefined;\n            state.closed = false;\n            state.open = 0;\n            state.distanceToSource = Number.POSITIVE_INFINITY;\n            state.fScore = Number.POSITIVE_INFINITY;\n            state.heapIndex = -1;\n        }\n        else {\n            state = new SearchState(node);\n            this.pool[this.index] = state;\n        }\n        this.index++;\n        return state;\n    }\n    reset(): void {\n        this.index = 0;\n    }\n}\nclass SearchState implements SearchState {\n    node: any;\n    parent: SearchState | undefined;\n    closed: boolean;\n    open: number;\n    distanceToSource: number;\n    fScore: number;\n    heapIndex: number;\n    constructor(node: any) {\n        this.node = node;\n        this.closed = false;\n        this.open = 0;\n        this.distanceToSource = Number.POSITIVE_INFINITY;\n        this.fScore = Number.POSITIVE_INFINITY;\n        this.heapIndex = -1;\n    }\n}\nexport { SearchStatePool, SearchState };\n"
  },
  {
    "path": "src/game/map/tileFinder/CardinalTileFinder.ts",
    "content": "import { Vector2 } from '@/game/math/Vector2';\nimport type { Tile } from \"@/game/map/TileCollection\";\nimport type { MapBounds } from \"@/game/map/MapBounds\";\ninterface TileCollection {\n    getByMapCoords(x: number, y: number): Tile | undefined;\n}\nexport class CardinalTileFinder {\n    private tiles: TileCollection;\n    private mapBounds: MapBounds;\n    private startTile: Tile;\n    private maxDistance: number;\n    private predicate: (tile: Tile) => boolean;\n    private dirVec: Vector2;\n    private finished: boolean;\n    public diagonal: boolean;\n    private distance: number;\n    constructor(tiles: TileCollection, mapBounds: MapBounds, startTile: Tile, distance: number, maxDistance: number, predicate: (tile: Tile) => boolean = () => true) {\n        this.tiles = tiles;\n        this.mapBounds = mapBounds;\n        this.startTile = startTile;\n        this.maxDistance = maxDistance;\n        this.predicate = predicate;\n        this.dirVec = new Vector2(10, 0);\n        this.finished = false;\n        this.diagonal = true;\n        this.distance = distance;\n    }\n    getNextTile(): Tile | undefined {\n        if (!this.finished) {\n            let result: Tile | undefined;\n            do {\n                let coords = { x: this.startTile.rx, y: this.startTile.ry };\n                coords.x += this.distance * Math.sign(this.dirVec.x);\n                coords.y += this.distance * Math.sign(this.dirVec.y);\n                this.dirVec\n                    .rotateAround(new Vector2(), (Math.PI / 4) * (this.diagonal ? 1 : 2))\n                    .round();\n                const tile = this.tiles.getByMapCoords(coords.x, coords.y);\n                if (tile &&\n                    this.mapBounds.isWithinBounds(tile) &&\n                    this.predicate(tile) &&\n                    (result = tile),\n                    !this.dirVec.angle()) {\n                    if (this.maxDistance && this.distance >= this.maxDistance) {\n                        this.finished = true;\n                        return result;\n                    }\n                    this.distance++;\n                }\n            } while (!result);\n            return result;\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/map/tileFinder/DirectionalTileFinder.ts",
    "content": "import type { Tile } from \"@/game/map/TileCollection\";\nimport type { MapBounds } from \"@/game/map/MapBounds\";\ninterface TileCollection {\n    getByMapCoords(x: number, y: number): Tile | undefined;\n}\nexport class DirectionalTileFinder {\n    private tiles: TileCollection;\n    private mapBounds: MapBounds;\n    private startTile: Tile;\n    private maxDistance: number;\n    private dirX: number;\n    private dirY: number;\n    private predicate: (tile: Tile) => boolean;\n    private checkBounds: boolean;\n    private finished: boolean;\n    private distance: number;\n    constructor(tiles: TileCollection, mapBounds: MapBounds, startTile: Tile, distance: number, maxDistance: number, dirX: number, dirY: number, predicate: (tile: Tile) => boolean = () => true, checkBounds: boolean = true) {\n        this.tiles = tiles;\n        this.mapBounds = mapBounds;\n        this.startTile = startTile;\n        this.maxDistance = maxDistance;\n        this.dirX = dirX;\n        this.dirY = dirY;\n        this.predicate = predicate;\n        this.checkBounds = checkBounds;\n        this.finished = false;\n        this.distance = distance;\n    }\n    getNextTile(): Tile | undefined {\n        if (!this.finished) {\n            let result: Tile | undefined;\n            do {\n                let coords = { x: this.startTile.rx, y: this.startTile.ry };\n                coords.x += this.distance * Math.sign(this.dirX);\n                coords.y += this.distance * Math.sign(this.dirY);\n                const tile = this.tiles.getByMapCoords(coords.x, coords.y);\n                if (tile &&\n                    (!this.checkBounds || this.mapBounds.isWithinBounds(tile)) &&\n                    this.predicate(tile) &&\n                    (result = tile),\n                    this.maxDistance && this.distance >= this.maxDistance) {\n                    this.finished = true;\n                    return result;\n                }\n            } while ((this.distance++, !result));\n            return result;\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/map/tileFinder/FloodTileFinder.ts",
    "content": "import type { Tile } from \"@/game/map/TileCollection\";\nimport type { MapBounds } from \"@/game/map/MapBounds\";\ninterface TileCollection {\n    getAllNeighbourTiles(tile: Tile): Tile[];\n}\nexport class FloodTileFinder {\n    private tiles: TileCollection;\n    private mapBounds: MapBounds;\n    private startTile: Tile;\n    private areConnected: (tile1: Tile, tile2: Tile) => boolean;\n    private predicate: (tile: Tile) => boolean;\n    private checkBounds: boolean;\n    private generator: Generator<Tile | undefined>;\n    constructor(tiles: TileCollection, mapBounds: MapBounds, startTile: Tile, areConnected: (tile1: Tile, tile2: Tile) => boolean, predicate: (tile: Tile) => boolean, checkBounds: boolean = true) {\n        this.tiles = tiles;\n        this.mapBounds = mapBounds;\n        this.startTile = startTile;\n        this.areConnected = areConnected;\n        this.predicate = predicate;\n        this.checkBounds = checkBounds;\n        this.generator = this.generate();\n    }\n    getNextTile(): Tile | undefined {\n        return this.generator.next().value;\n    }\n    private *generate(): Generator<Tile | undefined> {\n        let queue = [this.startTile];\n        let visited = new Set<Tile>();\n        while (queue.length) {\n            const current = queue.pop()!;\n            if (!visited.has(current)) {\n                visited.add(current);\n                if (!(this.checkBounds && !this.mapBounds.isWithinBounds(current)) &&\n                    this.predicate(current)) {\n                    yield current;\n                }\n                for (const neighbor of this.tiles.getAllNeighbourTiles(current)) {\n                    if (this.areConnected(neighbor, current)) {\n                        queue.push(neighbor);\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/map/tileFinder/RadialBackFirstTileFinder.ts",
    "content": "import type { Tile } from \"@/game/map/TileCollection\";\nimport type { MapBounds } from \"@/game/map/MapBounds\";\ninterface TileCollection {\n    getByMapCoords(x: number, y: number): Tile | undefined;\n}\ninterface Foundation {\n    width: number;\n    height: number;\n}\nexport class RadialBackFirstTileFinder {\n    private tiles: TileCollection;\n    private mapBounds: MapBounds;\n    private startTile: Tile;\n    private foundation: Foundation;\n    private maxDistance: number;\n    private predicate: (tile: Tile) => boolean;\n    private checkBounds: boolean;\n    private distance: number;\n    private generator: Generator<Tile | undefined>;\n    constructor(tiles: TileCollection, mapBounds: MapBounds, startTile: Tile, foundation: Foundation, distance: number, maxDistance: number, predicate: (tile: Tile) => boolean, checkBounds: boolean = true) {\n        this.tiles = tiles;\n        this.mapBounds = mapBounds;\n        this.startTile = startTile;\n        this.foundation = foundation;\n        this.maxDistance = maxDistance;\n        this.predicate = predicate;\n        this.checkBounds = checkBounds;\n        this.distance = distance;\n        this.generator = this.generate();\n    }\n    getNextTile(): Tile | undefined {\n        return this.generator.next().value;\n    }\n    private *generate(): Generator<Tile | undefined> {\n        const getTile = (x: number, y: number): Tile | undefined => {\n            const tile = this.tiles.getByMapCoords(x, y);\n            if (tile &&\n                (!this.checkBounds || this.mapBounds.isWithinBounds(tile)) &&\n                this.predicate(tile)) {\n                return tile;\n            }\n        };\n        do {\n            const left = this.startTile.rx - this.distance;\n            const top = this.startTile.ry - this.distance;\n            const right = this.startTile.rx + this.foundation.width - 1 + this.distance;\n            const bottom = this.startTile.ry + this.foundation.height - 1 + this.distance;\n            if (this.distance > 0) {\n                for (let y = top + 1; y < bottom; y++) {\n                    const tile = getTile(left, y);\n                    if (tile)\n                        yield tile;\n                }\n                for (let x = left; x < right; x++) {\n                    const tile = getTile(x, top);\n                    if (tile)\n                        yield tile;\n                }\n                for (let y = bottom - 1; y >= top; y--) {\n                    const tile = getTile(right, y);\n                    if (tile)\n                        yield tile;\n                }\n                for (let x = right; x >= left; x--) {\n                    const tile = getTile(x, bottom);\n                    if (tile)\n                        yield tile;\n                }\n            }\n            else if (this.predicate(this.startTile)) {\n                yield this.startTile;\n            }\n        } while (++this.distance <= this.maxDistance);\n    }\n}\n"
  },
  {
    "path": "src/game/map/tileFinder/RadialTileFinder.ts",
    "content": "import type { Tile } from \"@/game/map/TileCollection\";\nimport type { MapBounds } from \"@/game/map/MapBounds\";\ninterface TileCollection {\n    getByMapCoords(x: number, y: number): Tile | undefined;\n}\ninterface Foundation {\n    width: number;\n    height: number;\n}\nexport class RadialTileFinder {\n    private tiles: TileCollection;\n    private mapBounds: MapBounds;\n    private startTile: Tile;\n    private foundation: Foundation;\n    private maxDistance: number;\n    private predicate: (tile: Tile) => boolean;\n    private checkBounds: boolean;\n    private distance: number;\n    private generator: Generator<Tile | undefined>;\n    constructor(tiles: TileCollection, mapBounds: MapBounds, startTile: Tile, foundation: Foundation, distance: number, maxDistance: number, predicate: (tile: Tile) => boolean, checkBounds: boolean = true) {\n        this.tiles = tiles;\n        this.mapBounds = mapBounds;\n        this.startTile = startTile;\n        this.foundation = foundation;\n        this.maxDistance = maxDistance;\n        this.predicate = predicate;\n        this.checkBounds = checkBounds;\n        this.distance = distance;\n        this.generator = this.generate();\n    }\n    getNextTile(): Tile | undefined {\n        return this.generator.next().value;\n    }\n    private *generate(): Generator<Tile | undefined> {\n        const getTile = (x: number, y: number): Tile | undefined => {\n            const tile = this.tiles.getByMapCoords(x, y);\n            if (tile &&\n                (!this.checkBounds || this.mapBounds.isWithinBounds(tile)) &&\n                this.predicate(tile)) {\n                return tile;\n            }\n        };\n        do {\n            const left = this.startTile.rx - this.distance;\n            const top = this.startTile.ry - this.distance;\n            const right = this.startTile.rx + this.foundation.width - 1 + this.distance;\n            const bottom = this.startTile.ry + this.foundation.height - 1 + this.distance;\n            if (this.distance > 0) {\n                for (let x = right; x >= left; x--) {\n                    const tile = getTile(x, bottom);\n                    if (tile)\n                        yield tile;\n                }\n                for (let y = bottom - 1; y >= top; y--) {\n                    const tile = getTile(right, y);\n                    if (tile)\n                        yield tile;\n                }\n                for (let x = left; x < right; x++) {\n                    const tile = getTile(x, top);\n                    if (tile)\n                        yield tile;\n                }\n                for (let y = top + 1; y < bottom; y++) {\n                    const tile = getTile(left, y);\n                    if (tile)\n                        yield tile;\n                }\n            }\n            else if (this.predicate(this.startTile)) {\n                yield this.startTile;\n            }\n        } while (++this.distance <= this.maxDistance);\n    }\n}\n"
  },
  {
    "path": "src/game/map/tileFinder/RandomTileFinder.ts",
    "content": "import { GameMath } from '@/game/math/GameMath';\nimport type { Tile } from \"@/game/map/TileCollection\";\nimport type { MapBounds } from \"@/game/map/MapBounds\";\ninterface TileCollection {\n    getByMapCoords(x: number, y: number): Tile | undefined;\n}\ninterface RNG {\n    generateRandomInt(min: number, max: number): number;\n}\nexport class RandomTileFinder {\n    private tiles: TileCollection;\n    private mapBounds: MapBounds;\n    private startTile: Tile;\n    private maxDistance: number;\n    private rng: RNG;\n    private predicate: (tile: Tile) => boolean;\n    private includeStartTile: boolean;\n    private checkBounds: boolean;\n    private pool: number[];\n    private generator: Generator<Tile | undefined>;\n    constructor(tiles: TileCollection, mapBounds: MapBounds, startTile: Tile, maxDistance: number, rng: RNG, predicate: (tile: Tile) => boolean, includeStartTile: boolean = false, checkBounds: boolean = true) {\n        this.tiles = tiles;\n        this.mapBounds = mapBounds;\n        this.startTile = startTile;\n        this.maxDistance = maxDistance;\n        this.rng = rng;\n        this.predicate = predicate;\n        this.includeStartTile = includeStartTile;\n        this.checkBounds = checkBounds;\n        this.pool = [];\n        this.pool = new Array(GameMath.pow(2 * this.maxDistance + 1, 2))\n            .fill(0)\n            .map((_, i) => i);\n        this.generator = this.generate();\n    }\n    getNextTile(): Tile | undefined {\n        return this.generator.next().value;\n    }\n    private *generate(): Generator<Tile | undefined> {\n        const getTile = (x: number, y: number): Tile | undefined => {\n            const tile = this.tiles.getByMapCoords(x, y);\n            if (this.includeStartTile || tile !== this.startTile) {\n                return tile &&\n                    (!this.checkBounds || this.mapBounds.isWithinBounds(tile)) &&\n                    this.predicate(tile)\n                    ? tile\n                    : undefined;\n            }\n        };\n        const size = 2 * this.maxDistance + 1;\n        while (this.pool.length) {\n            const index = this.pool.length > 1\n                ? this.rng.generateRandomInt(0, this.pool.length)\n                : 0;\n            const value = this.pool.splice(index, 1)[0];\n            const x = value % size;\n            const y = Math.floor(value / size);\n            const tile = getTile(this.startTile.rx - this.maxDistance + x, this.startTile.ry - this.maxDistance + y);\n            if (tile) {\n                yield tile;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/map/wallTypes.ts",
    "content": "export const wallTypes = [\n    [0, 0, 0, 0],\n    [1, 0, 0, 0],\n    [0, 1, 0, 0],\n    [1, 1, 0, 0],\n    [0, 0, 1, 0],\n    [1, 0, 1, 0],\n    [0, 1, 1, 0],\n    [1, 1, 1, 0],\n    [0, 0, 0, 1],\n    [1, 0, 0, 1],\n    [0, 1, 0, 1],\n    [1, 1, 0, 1],\n    [0, 0, 1, 1],\n    [1, 0, 1, 1],\n    [0, 1, 1, 1],\n    [1, 1, 1, 1],\n];\n"
  },
  {
    "path": "src/game/math/Box2.ts",
    "content": "import * as THREE from 'three';\nexport class Box2 extends THREE.Box2 {\n    constructor(min?: THREE.Vector2, max?: THREE.Vector2) {\n        super(min, max);\n    }\n}\n"
  },
  {
    "path": "src/game/math/CubicBezierCurve3.ts",
    "content": "import * as THREE from 'three';\nimport { Vector3 } from './Vector3';\nexport class CubicBezierCurve3 extends THREE.CubicBezierCurve3 {\n    constructor(v0?: THREE.Vector3, v1?: THREE.Vector3, v2?: THREE.Vector3, v3?: THREE.Vector3) {\n        super(v0 || new Vector3(), v1 || new Vector3(), v2 || new Vector3(), v3 || new Vector3());\n    }\n    getPoint(t: number, optionalTarget?: THREE.Vector3): THREE.Vector3 {\n        return super.getPoint(t, optionalTarget || new Vector3());\n    }\n}\n"
  },
  {
    "path": "src/game/math/CurvePath.ts",
    "content": "import * as THREE from 'three';\nimport { LineCurve } from './LineCurve';\nexport class CurvePath extends THREE.CurvePath<THREE.Vector3> {\n    closePath(): this {\n        const start = this.curves[0].getPoint(0);\n        const end = this.curves[this.curves.length - 1].getPoint(1);\n        if (!start.equals(end)) {\n            this.curves.push(new LineCurve(end as any, start as any) as any);\n        }\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/math/Cylindrical.ts",
    "content": "import * as THREE from 'three';\nimport { GameMath } from './GameMath';\nexport class Cylindrical extends THREE.Cylindrical {\n    setFromVector3(v: THREE.Vector3): this {\n        this.radius = GameMath.sqrt(v.x * v.x + v.z * v.z);\n        this.theta = GameMath.atan2(v.x, v.z);\n        this.y = v.y;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/math/Euler.ts",
    "content": "import * as THREE from 'three';\nimport { GameMath } from './GameMath';\nimport { Quaternion } from './Quaternion';\nimport { Vector3 } from './Vector3';\nimport { clamp } from '../../util/math';\nexport class Euler extends THREE.Euler {\n    constructor(...args: any[]) {\n        super(...args);\n    }\n    setFromRotationMatrix(matrix: THREE.Matrix4, order?: string, update?: boolean): this {\n        const elements = matrix.elements;\n        const m11 = elements[0];\n        const m12 = elements[4];\n        const m13 = elements[8];\n        const m21 = elements[1];\n        const m22 = elements[5];\n        const m23 = elements[9];\n        const m31 = elements[2];\n        const m32 = elements[6];\n        const m33 = elements[10];\n        order = order || this.order;\n        if (order === 'XYZ') {\n            this.y = GameMath.asin(clamp(m13, -1, 1));\n            if (Math.abs(m13) < 0.99999) {\n                this.x = GameMath.atan2(-m23, m33);\n                this.z = GameMath.atan2(-m12, m11);\n            }\n            else {\n                this.x = GameMath.atan2(m32, m22);\n                this.z = 0;\n            }\n        }\n        else if (order === 'YXZ') {\n            this.x = GameMath.asin(-clamp(m23, -1, 1));\n            if (Math.abs(m23) < 0.99999) {\n                this.y = GameMath.atan2(m13, m33);\n                this.z = GameMath.atan2(m21, m22);\n            }\n            else {\n                this.y = GameMath.atan2(-m31, m11);\n                this.z = 0;\n            }\n        }\n        else if (order === 'ZXY') {\n            this.x = GameMath.asin(clamp(m32, -1, 1));\n            if (Math.abs(m32) < 0.99999) {\n                this.y = GameMath.atan2(-m31, m33);\n                this.z = GameMath.atan2(-m12, m22);\n            }\n            else {\n                this.y = 0;\n                this.z = GameMath.atan2(m21, m11);\n            }\n        }\n        else if (order === 'ZYX') {\n            this.y = GameMath.asin(-clamp(m31, -1, 1));\n            if (Math.abs(m31) < 0.99999) {\n                this.x = GameMath.atan2(m32, m33);\n                this.z = GameMath.atan2(m21, m11);\n            }\n            else {\n                this.x = 0;\n                this.z = GameMath.atan2(-m12, m22);\n            }\n        }\n        else if (order === 'YZX') {\n            this.z = GameMath.asin(clamp(m21, -1, 1));\n            if (Math.abs(m21) < 0.99999) {\n                this.x = GameMath.atan2(-m23, m22);\n                this.y = GameMath.atan2(-m31, m11);\n            }\n            else {\n                this.x = 0;\n                this.y = GameMath.atan2(m13, m33);\n            }\n        }\n        else if (order === 'XZY') {\n            this.z = GameMath.asin(-clamp(m12, -1, 1));\n            if (Math.abs(m12) < 0.99999) {\n                this.x = GameMath.atan2(m32, m22);\n                this.y = GameMath.atan2(m13, m11);\n            }\n            else {\n                this.x = GameMath.atan2(-m23, m33);\n                this.y = 0;\n            }\n        }\n        else {\n            console.warn('THREE.Euler: .setFromRotationMatrix() given unsupported order: ' + order);\n        }\n        this.order = order as THREE.EulerOrder;\n        if (update !== false) {\n            this._onChangeCallback();\n        }\n        return this;\n    }\n    reorder(newOrder: THREE.EulerOrder): this {\n        const quaternion = new Quaternion();\n        quaternion.setFromEuler(this);\n        return this.setFromQuaternion(quaternion, newOrder) as this;\n    }\n    toVector3(optionalTarget?: THREE.Vector3): THREE.Vector3 {\n        if (optionalTarget) {\n            return optionalTarget.set(this.x, this.y, this.z);\n        }\n        return new Vector3(this.x, this.y, this.z);\n    }\n}\n"
  },
  {
    "path": "src/game/math/GameMath.ts",
    "content": "import { isBetween } from '../../util/math';\nexport class GameMath {\n    static readonly PRECISION = 6;\n    static readonly EXP_10_PRECISION = 10 ** GameMath.PRECISION;\n    static readonly SIN_TABLE: number[] = [\n        0, 0.004848117819001859, 0.009696121685978396, 0.014543897651582654,\n        0.01939133177182437, 0.024238310110748135, 0.0290847187431114,\n        0.03393044375706223, 0.03877537125681671, 0.043619387365336,\n        0.04846237822700296, 0.05330423001029823, 0.05814482891047582,\n        0.06298406115223795, 0.06782181299240934, 0.0726579707226106,\n        0.07749242067193093, 0.08232504920959989, 0.08715574274765817,\n        0.09198438774362744, 0.09681087070317909, 0.10163507818280187,\n        0.10645689679246824, 0.11127621319829964, 0.11609291412523022,\n        0.12090688635966935, 0.12571801675216268, 0.13052619222005157,\n        0.13533129975013108, 0.14013322640130627, 0.14493185930724672,\n        0.1497270856790396, 0.15451879280784048, 0.15930686806752256,\n        0.16409119891732396, 0.1688716729044928, 0.17364817766693033,\n        0.17842060093583212, 0.18318883053832663, 0.18795275440011186,\n        0.19271226054808968, 0.19746723711299752, 0.20221757233203794,\n        0.20696315455150538, 0.2117038722294107, 0.21643961393810288,\n        0.22117026836688775, 0.2258957243246448, 0.23061587074244014,\n        0.2353305966761376, 0.24003979130900588, 0.2447433439543238,\n        0.24944114405798126, 0.25413308120107847, 0.25881904510252074,\n        0.26349892562161076, 0.2681726127606373, 0.272839996667461,\n        0.27750096763809573, 0.28215541611928774, 0.2868032327110902,\n        0.29144430816943495, 0.2960785334086999, 0.3007057995042731,\n        0.3053259976951131, 0.30993901938630514, 0.31454475615161365,\n        0.31914309973603083, 0.323733942058321, 0.328317175213561,\n        0.33289269147567657, 0.3374603832999741, 0.3420201433256687,\n        0.34657186437840753, 0.3511154394727888, 0.3556507618148765,\n        0.3601777248047104, 0.36469622203881186, 0.3692061473126844,\n        0.3737073946233105, 0.3781998581716425, 0.3826834323650898,\n        0.3871580118200006, 0.3916234913641391, 0.3960797660391568,\n        0.4005267311030606, 0.4049642820326736, 0.4093923145260926,\n        0.4138107245051391, 0.4182194081178064, 0.42261826174069944,\n        0.4270071819814715, 0.4313860656812534, 0.4357548099170794,\n        0.4401133120043048, 0.44446146949902104, 0.4487991802004621,\n        0.4531263421534082, 0.4574428536505808, 0.4617486132350339,\n        0.46604351970253877, 0.4703274721039625, 0.4746003697476404,\n        0.4788621122017435, 0.4831125992966384, 0.48735173112724234,\n        0.49157940805537054, 0.49579553071207916, 0.49999999999999994,\n        0.5041927170956704, 0.5083735834518556, 0.5125425007998652,\n        0.5166993711518628, 0.5208440968031697, 0.5249765803345602,\n        0.5290967246145525, 0.5332044328016912, 0.5372996083468239,\n        0.5413821549953696, 0.5454519767895825, 0.549508978070806,\n        0.5535530634817223, 0.5575841379685927, 0.5616021067834929,\n        0.5656068754865385, 0.5695983499481065, 0.573576436351046,\n        0.5775410411928851, 0.5814920712880266, 0.5854294337699405,\n        0.5893530360933448, 0.593262786036382, 0.5971585917027862,\n        0.6010403615240428, 0.6049080042615417, 0.6087614290087207,\n        0.6126005451932028, 0.6164252625789254, 0.62023549126826,\n        0.6240311417041269, 0.6278121246720986, 0.6315783513024975,\n        0.6353297330724851, 0.6390661818081416, 0.6427876096865393,\n        0.6464939292378065, 0.6501850533471834, 0.6538608952570697,\n        0.6575213685690636, 0.6611663872459932, 0.6647958656139378,\n        0.6684097183642425, 0.6720078605555224, 0.6755902076156601,\n        0.6791566753437932, 0.6827071799122926, 0.6862416378687335,\n        0.6897599661378576, 0.6932620820235242, 0.696747903210655,\n        0.7002173477671685, 0.7036703341459059, 0.7071067811865475,\n        0.710526608117521, 0.713929734557899, 0.7173160805192894,\n        0.7206855664077146, 0.7240381130254825, 0.7273736415730487,\n        0.7306920736508674, 0.7339933312612352, 0.7372773368101241,\n        0.7405440131090046, 0.7437932833766612, 0.747025071240996,\n        0.7502393007408245, 0.7534358963276606, 0.7566147828674927,\n        0.7597758856425494, 0.7629191303530553, 0.766044443118978,\n        0.7691517504817651, 0.7722409794060692, 0.7753120572814658,\n        0.7783649119241599, 0.7813994715786823, 0.7844156649195757,\n        0.7874134210530723, 0.7903926695187593, 0.7933533402912352,\n        0.7962953637817558, 0.7992186708398696, 0.8021231927550437,\n        0.8050088612582783, 0.8078756085237111, 0.8107233671702122,\n        0.8135520702629676, 0.8163616513150519, 0.8191520442889918,\n        0.821923183598318, 0.8246750041091067, 0.8274074411415104,\n        0.8301204304712788, 0.8328139083312671, 0.8354878114129364,\n        0.8381420768678404, 0.8407766423091032, 0.8433914458128856,\n        0.845986425919841, 0.848561521636559, 0.8511166724369997,\n        0.8536518182639162, 0.8561668995302665, 0.8586618571206132,\n        0.8611366323925137, 0.8635911671778986, 0.8660254037844386,\n        0.8684392849969005, 0.870832754078492, 0.8732057547721958,\n        0.8755582313020908, 0.8778901283746645, 0.8802013911801111,\n        0.8824919653936212, 0.8847617971766577, 0.8870108331782216,\n        0.8892390205361062, 0.8914463068781385, 0.8936326403234122,\n        0.8957979694835052, 0.8979422434636881, 0.9000654118641211,\n        0.9021674247810376, 0.9042482328079179, 0.9063077870366499,\n        0.9083460390586793, 0.9103629409661466, 0.9123584453530141,\n        0.9143325053161794, 0.9162850744565779, 0.918216106880274,\n        0.9201255571995389, 0.9220133805339185, 0.9238795325112867,\n        0.9257239692688903, 0.9275466474543786, 0.9293475242268224,\n        0.9311265572577219, 0.9328837047320004, 0.9346189253489884,\n        0.9363321783233931, 0.9380234233862578, 0.9396926207859083,\n        0.9413397312888874, 0.942964716180876, 0.9445675372676047,\n        0.9461481568757504, 0.947706537853822, 0.9492426435730339,\n        0.9507564379281666, 0.9522478853384153, 0.9537169507482269,\n        0.955163599628123, 0.9565877979755122, 0.9579895123154889,\n        0.9593687097016201, 0.9607253577167205, 0.9620594244736131,\n        0.9633708786158803, 0.9646596893185995, 0.9659258262890683,\n        0.9671692597675166, 0.9683899605278059, 0.969587899878116,\n        0.97076304966162, 0.9719153822571454, 0.9730448705798238,\n        0.9741514880817275, 0.9752352087524931, 0.9762960071199334,\n        0.9773338582506355, 0.9783487377505475, 0.9793406217655515,\n        0.980309486982024, 0.9812553106273847, 0.9821780704706308,\n        0.98307774482286, 0.9839543125377807, 0.984807753012208,\n        0.9856380461865492, 0.9864451725452739, 0.987229113117374,\n        0.987989849476809, 0.9887273637429388, 0.9894416385809445,\n        0.9901326572022359, 0.9908004033648453, 0.9914448613738104,\n        0.9920660160815423, 0.9926638528881819, 0.993238357741943,\n        0.9937895171394426, 0.9943173181260184, 0.9948217482960331,\n        0.9953027957931658, 0.9957604493106914, 0.9961946980917455,\n        0.9966055319295779, 0.996992941167792, 0.9973569167005722,\n        0.9976974499728977, 0.9980145329807433, 0.9983081582712682,\n        0.9985783189429907, 0.9988250086459504, 0.9990482215818578,\n        0.99924795250423, 0.9994241967185149, 0.9995769500822006,\n        0.9997062090049132, 0.9998119704485015, 0.9998942319271075,\n        0.9999529915072262, 0.9999882478077495, 1, 0.9999882478077495,\n        0.9999529915072262, 0.9998942319271075, 0.9998119704485015,\n        0.9997062090049132, 0.9995769500822006, 0.9994241967185149,\n        0.99924795250423, 0.9990482215818578, 0.9988250086459504,\n        0.9985783189429907, 0.9983081582712682, 0.9980145329807433,\n        0.9976974499728977, 0.9973569167005722, 0.996992941167792,\n        0.9966055319295779, 0.9961946980917455, 0.9957604493106914,\n        0.9953027957931658, 0.9948217482960331, 0.9943173181260184,\n        0.9937895171394426, 0.993238357741943, 0.9926638528881819,\n        0.9920660160815423, 0.9914448613738105, 0.9908004033648453,\n        0.9901326572022359, 0.9894416385809446, 0.9887273637429388,\n        0.987989849476809, 0.987229113117374, 0.9864451725452739,\n        0.9856380461865492, 0.984807753012208, 0.9839543125377807,\n        0.98307774482286, 0.9821780704706307, 0.9812553106273847,\n        0.9803094869820241, 0.9793406217655516, 0.9783487377505476,\n        0.9773338582506356, 0.9762960071199334, 0.9752352087524931,\n        0.9741514880817276, 0.9730448705798238, 0.9719153822571455,\n        0.9707630496616201, 0.969587899878116, 0.9683899605278059,\n        0.9671692597675166, 0.9659258262890683, 0.9646596893185995,\n        0.9633708786158803, 0.9620594244736133, 0.9607253577167205,\n        0.9593687097016202, 0.9579895123154889, 0.9565877979755123,\n        0.9551635996281231, 0.9537169507482269, 0.9522478853384153,\n        0.9507564379281666, 0.949242643573034, 0.9477065378538221,\n        0.9461481568757505, 0.9445675372676048, 0.942964716180876,\n        0.9413397312888873, 0.9396926207859084, 0.9380234233862579,\n        0.9363321783233931, 0.9346189253489885, 0.9328837047320006,\n        0.9311265572577219, 0.9293475242268225, 0.9275466474543786,\n        0.9257239692688904, 0.9238795325112867, 0.9220133805339185,\n        0.920125557199539, 0.918216106880274, 0.916285074456578,\n        0.9143325053161794, 0.9123584453530141, 0.9103629409661467,\n        0.9083460390586793, 0.90630778703665, 0.904248232807918,\n        0.9021674247810377, 0.9000654118641213, 0.8979422434636883,\n        0.8957979694835051, 0.8936326403234123, 0.8914463068781386,\n        0.8892390205361062, 0.8870108331782218, 0.8847617971766579,\n        0.8824919653936212, 0.8802013911801111, 0.8778901283746644,\n        0.8755582313020909, 0.8732057547721958, 0.8708327540784921,\n        0.8684392849969006, 0.8660254037844387, 0.8635911671778987,\n        0.8611366323925138, 0.858661857120613, 0.8561668995302665,\n        0.8536518182639163, 0.8511166724369997, 0.8485615216365591,\n        0.8459864259198412, 0.8433914458128856, 0.8407766423091031,\n        0.8381420768678404, 0.8354878114129364, 0.8328139083312672,\n        0.8301204304712789, 0.8274074411415107, 0.8246750041091069,\n        0.8219231835983182, 0.819152044288992, 0.8163616513150518,\n        0.8135520702629675, 0.8107233671702123, 0.8078756085237112,\n        0.8050088612582784, 0.802123192755044, 0.7992186708398695,\n        0.7962953637817556, 0.7933533402912352, 0.7903926695187593,\n        0.7874134210530723, 0.7844156649195758, 0.7813994715786824,\n        0.7783649119241601, 0.775312057281466, 0.7722409794060693,\n        0.769151750481765, 0.766044443118978, 0.7629191303530551,\n        0.7597758856425494, 0.756614782867493, 0.7534358963276608,\n        0.7502393007408243, 0.7470250712409959, 0.7437932833766611,\n        0.7405440131090045, 0.7372773368101241, 0.7339933312612353,\n        0.7306920736508675, 0.7273736415730488, 0.7240381130254827,\n        0.7206855664077148, 0.7173160805192896, 0.713929734557899,\n        0.710526608117521, 0.7071067811865476, 0.703670334145906,\n        0.7002173477671687, 0.6967479032106549, 0.6932620820235241,\n        0.6897599661378576, 0.6862416378687336, 0.6827071799122926,\n        0.6791566753437933, 0.6755902076156604, 0.6720078605555225,\n        0.6684097183642426, 0.6647958656139381, 0.6611663872459935,\n        0.6575213685690636, 0.6538608952570697, 0.6501850533471835,\n        0.6464939292378067, 0.6427876096865395, 0.6390661818081418,\n        0.635329733072485, 0.6315783513024975, 0.6278121246720986,\n        0.6240311417041269, 0.6202354912682602, 0.6164252625789255,\n        0.612600545193203, 0.6087614290087209, 0.6049080042615419,\n        0.6010403615240432, 0.5971585917027862, 0.593262786036382,\n        0.5893530360933449, 0.5854294337699406, 0.5814920712880268,\n        0.5775410411928852, 0.5735764363510459, 0.5695983499481064,\n        0.5656068754865385, 0.5616021067834929, 0.5575841379685929,\n        0.5535530634817224, 0.5495089780708062, 0.5454519767895827,\n        0.5413821549953699, 0.5372996083468241, 0.5332044328016912,\n        0.5290967246145525, 0.5249765803345602, 0.5208440968031698,\n        0.516699371151863, 0.5125425007998654, 0.5083735834518555,\n        0.5041927170956703, 0.49999999999999994, 0.49579553071207916,\n        0.49157940805537065, 0.48735173112724245, 0.48311259929663863,\n        0.4788621122017437, 0.47460036974764064, 0.47032747210396275,\n        0.46604351970253877, 0.4617486132350339, 0.45744285365058085,\n        0.4531263421534083, 0.44879918020046233, 0.4444614694990212,\n        0.4401133120043047, 0.43575480991707927, 0.43138606568125343,\n        0.4270071819814714, 0.4226182617406995, 0.4182194081178065,\n        0.4138107245051393, 0.40939231452609276, 0.4049642820326738,\n        0.40052673110306086, 0.3960797660391572, 0.39162349136413904,\n        0.38715801182000065, 0.3826834323650899, 0.37819985817164264,\n        0.3737073946233107, 0.3692061473126843, 0.36469622203881175,\n        0.3601777248047104, 0.3556507618148765, 0.35111543947278884,\n        0.34657186437840765, 0.3420201433256689, 0.3374603832999743,\n        0.33289269147567685, 0.32831717521356135, 0.3237339420583214,\n        0.31914309973603083, 0.3145447561516137, 0.30993901938630525,\n        0.30532599769511326, 0.30070579950427334, 0.29607853340870016,\n        0.2914443081694349, 0.2868032327110902, 0.28215541611928774,\n        0.2775009676380958, 0.2728399966674611, 0.26817261276063753,\n        0.263498925621611, 0.258819045102521, 0.2541330812010788,\n        0.24944114405798165, 0.24474334395432376, 0.24003979130900596,\n        0.2353305966761377, 0.23061587074244033, 0.225895724324645,\n        0.22117026836688802, 0.21643961393810274, 0.21170387222941067,\n        0.20696315455150538, 0.20221757233203796, 0.19746723711299763,\n        0.19271226054808982, 0.18795275440011205, 0.18318883053832688,\n        0.17842060093583242, 0.17364817766693072, 0.16887167290449276,\n        0.16409119891732402, 0.15930686806752267, 0.15451879280784062,\n        0.1497270856790398, 0.14493185930724697, 0.14013322640130613,\n        0.135331299750131, 0.13052619222005157, 0.12571801675216274,\n        0.12090688635966945, 0.11609291412523036, 0.11127621319829985,\n        0.1064568967924685, 0.10163507818280217, 0.09681087070317945,\n        0.09198438774362741, 0.0871557427476582, 0.08232504920959997,\n        0.07749242067193107, 0.07265797072261079, 0.0678218129924096,\n        0.06298406115223781, 0.05814482891047573, 0.0533042300102982,\n        0.04846237822700297, 0.04361938736533607, 0.038775371256816835,\n        0.03393044375706242, 0.029084718743111644, 0.02423831011074843,\n        0.01939133177182472, 0.014543897651583058, 0.009696121685978408,\n        0.004848117819001927, 12246467991473532e-32, -0.004848117819001238,\n        -0.009696121685978163, -0.01454389765158237, -0.019391331771824474,\n        -0.02423831011074774, -0.029084718743111398, -0.03393044375706173,\n        -0.03877537125681659, -0.04361938736533583, -0.048462378227003174,\n        -0.05330423001029795, -0.05814482891047593, -0.06298406115223758,\n        -0.06782181299240934, -0.07265797072261056, -0.07749242067193128,\n        -0.08232504920959974, -0.08715574274765794, -0.09198438774362716,\n        -0.09681087070317876, -0.10163507818280193, -0.10645689679246781,\n        -0.1112762131982996, -0.11609291412523012, -0.12090688635966965,\n        -0.1257180167521625, -0.13052619222005177, -0.13533129975013078,\n        -0.14013322640130632, -0.14493185930724675, -0.14972708567904,\n        -0.1545187928078404, -0.15930686806752198, -0.16409119891732377,\n        -0.16887167290449254, -0.17364817766693047, -0.17842060093583176,\n        -0.18318883053832663, -0.1879527544001114, -0.1927122605480896,\n        -0.19746723711299738, -0.20221757233203816, -0.20696315455150513,\n        -0.21170387222941087, -0.21643961393810252, -0.2211702683668878,\n        -0.22589572432464475, -0.23061587074244053, -0.23533059667613745,\n        -0.2400397913090053, -0.2447433439543235, -0.24944114405798098,\n        -0.2541330812010786, -0.25881904510252035, -0.2634989256216107,\n        -0.26817261276063686, -0.2728399966674609, -0.27750096763809556,\n        -0.2821554161192879, -0.28680323271108993, -0.29144430816943506,\n        -0.29607853340869994, -0.30070579950427306, -0.30532599769511304,\n        -0.3099390193863046, -0.3145447561516135, -0.3191430997360306,\n        -0.3237339420583211, -0.3283171752135607, -0.33289269147567657,\n        -0.3374603832999737, -0.34202014332566866, -0.3465718643784074,\n        -0.35111543947278906, -0.35565076181487626, -0.36017772480471055,\n        -0.3646962220388115, -0.3692061473126845, -0.3737073946233105,\n        -0.3781998581716428, -0.38268343236508967, -0.38715801182000004,\n        -0.3916234913641388, -0.39607976603915657, -0.40052673110306064,\n        -0.4049642820326732, -0.40939231452609254, -0.41381072450513867,\n        -0.4182194081178062, -0.4226182617406993, -0.4270071819814716,\n        -0.4313860656812532, -0.43575480991707943, -0.4401133120043045,\n        -0.444461469499021, -0.4487991802004621, -0.4531263421534085,\n        -0.4574428536505806, -0.46174861323503374, -0.46604351970253854,\n        -0.47032747210396214, -0.4746003697476404, -0.47886211220174313,\n        -0.4831125992966384, -0.4873517311272422, -0.4915794080553708,\n        -0.49579553071207894, -0.5000000000000001, -0.50419271709567,\n        -0.5083735834518556, -0.5125425007998652, -0.5166993711518633,\n        -0.5208440968031696, -0.5249765803345596, -0.5290967246145523,\n        -0.533204432801691, -0.5372996083468239, -0.5413821549953693,\n        -0.5454519767895825, -0.5495089780708056, -0.5535530634817222,\n        -0.5575841379685926, -0.5616021067834931, -0.5656068754865384,\n        -0.5695983499481065, -0.5735764363510458, -0.577541041192885,\n        -0.5814920712880266, -0.5854294337699408, -0.5893530360933448,\n        -0.5932627860363815, -0.597158591702786, -0.6010403615240426,\n        -0.6049080042615417, -0.6087614290087203, -0.6126005451932028,\n        -0.6164252625789249, -0.6202354912682599, -0.6240311417041268,\n        -0.6278121246720987, -0.6315783513024973, -0.6353297330724852,\n        -0.6390661818081416, -0.6427876096865393, -0.6464939292378065,\n        -0.6501850533471829, -0.6538608952570695, -0.6575213685690635,\n        -0.6611663872459933, -0.6647958656139376, -0.6684097183642425,\n        -0.6720078605555221, -0.6755902076156601, -0.6791566753437931,\n        -0.6827071799122927, -0.6862416378687334, -0.6897599661378577,\n        -0.693262082023524, -0.696747903210655, -0.7002173477671685,\n        -0.7036703341459061, -0.7071067811865475, -0.7105266081175206,\n        -0.7139297345578989, -0.7173160805192892, -0.7206855664077146,\n        -0.7240381130254823, -0.7273736415730487, -0.7306920736508671,\n        -0.7339933312612352, -0.737277336810124, -0.7405440131090048,\n        -0.743793283376661, -0.747025071240996, -0.7502393007408242,\n        -0.7534358963276607, -0.7566147828674927, -0.7597758856425493,\n        -0.7629191303530554, -0.7660444431189779, -0.7691517504817651,\n        -0.7722409794060688, -0.7753120572814659, -0.7783649119241597,\n        -0.7813994715786822, -0.7844156649195754, -0.7874134210530722,\n        -0.7903926695187589, -0.7933533402912349, -0.7962953637817558,\n        -0.79921867083987, -0.8021231927550437, -0.8050088612582785,\n        -0.8078756085237111, -0.8107233671702119, -0.8135520702629674,\n        -0.8163616513150515, -0.8191520442889916, -0.8219231835983181,\n        -0.8246750041091064, -0.8274074411415104, -0.830120430471279,\n        -0.8328139083312671, -0.8354878114129365, -0.8381420768678401,\n        -0.8407766423091032, -0.8433914458128855, -0.8459864259198411,\n        -0.8485615216365587, -0.8511166724369996, -0.8536518182639165,\n        -0.8561668995302664, -0.8586618571206132, -0.8611366323925135,\n        -0.8635911671778986, -0.8660254037844385, -0.8684392849969005,\n        -0.8708327540784918, -0.8732057547721956, -0.8755582313020905,\n        -0.8778901283746643, -0.8802013911801112, -0.8824919653936215,\n        -0.8847617971766578, -0.8870108331782218, -0.889239020536106,\n        -0.8914463068781383, -0.8936326403234122, -0.8957979694835049,\n        -0.897942243463688, -0.9000654118641208, -0.9021674247810375,\n        -0.9042482328079179, -0.90630778703665, -0.9083460390586792,\n        -0.9103629409661468, -0.912358445353014, -0.9143325053161795,\n        -0.9162850744565778, -0.918216106880274, -0.9201255571995388,\n        -0.9220133805339183, -0.9238795325112865, -0.9257239692688903,\n        -0.9275466474543786, -0.9293475242268223, -0.9311265572577219,\n        -0.9328837047320003, -0.9346189253489884, -0.9363321783233929,\n        -0.9380234233862578, -0.9396926207859082, -0.9413397312888873,\n        -0.9429647161808761, -0.9445675372676049, -0.9461481568757504,\n        -0.9477065378538222, -0.9492426435730339, -0.9507564379281666,\n        -0.9522478853384153, -0.9537169507482267, -0.9551635996281229,\n        -0.956587797975512, -0.9579895123154888, -0.9593687097016201,\n        -0.9607253577167205, -0.9620594244736131, -0.9633708786158804,\n        -0.9646596893185994, -0.9659258262890683, -0.9671692597675166,\n        -0.9683899605278059, -0.9695878998781159, -0.97076304966162,\n        -0.9719153822571452, -0.9730448705798238, -0.9741514880817276,\n        -0.975235208752493, -0.9762960071199334, -0.9773338582506355,\n        -0.9783487377505475, -0.9793406217655514, -0.980309486982024,\n        -0.9812553106273846, -0.9821780704706307, -0.98307774482286,\n        -0.9839543125377805, -0.984807753012208, -0.9856380461865493,\n        -0.9864451725452739, -0.9872291131173742, -0.9879898494768089,\n        -0.9887273637429387, -0.9894416385809445, -0.9901326572022358,\n        -0.9908004033648452, -0.9914448613738104, -0.9920660160815423,\n        -0.9926638528881818, -0.993238357741943, -0.9937895171394426,\n        -0.9943173181260184, -0.994821748296033, -0.9953027957931658,\n        -0.9957604493106913, -0.9961946980917455, -0.9966055319295779,\n        -0.996992941167792, -0.9973569167005722, -0.9976974499728977,\n        -0.9980145329807433, -0.9983081582712682, -0.9985783189429907,\n        -0.9988250086459504, -0.9990482215818578, -0.99924795250423,\n        -0.9994241967185149, -0.9995769500822006, -0.9997062090049132,\n        -0.9998119704485015, -0.9998942319271076, -0.9999529915072262,\n        -0.9999882478077495, -1, -0.9999882478077495, -0.9999529915072262,\n        -0.9998942319271076, -0.9998119704485015, -0.9997062090049132,\n        -0.9995769500822006, -0.9994241967185149, -0.99924795250423,\n        -0.9990482215818578, -0.9988250086459504, -0.9985783189429907,\n        -0.9983081582712682, -0.9980145329807433, -0.9976974499728977,\n        -0.9973569167005724, -0.996992941167792, -0.9966055319295779,\n        -0.9961946980917455, -0.9957604493106914, -0.9953027957931658,\n        -0.9948217482960331, -0.9943173181260185, -0.9937895171394426,\n        -0.993238357741943, -0.9926638528881819, -0.9920660160815424,\n        -0.9914448613738105, -0.9908004033648453, -0.9901326572022358,\n        -0.9894416385809446, -0.9887273637429387, -0.987989849476809,\n        -0.9872291131173742, -0.986445172545274, -0.9856380461865493,\n        -0.9848077530122081, -0.9839543125377807, -0.9830777448228601,\n        -0.9821780704706308, -0.9812553106273846, -0.9803094869820241,\n        -0.9793406217655515, -0.9783487377505476, -0.9773338582506355,\n        -0.9762960071199335, -0.9752352087524931, -0.9741514880817276,\n        -0.9730448705798239, -0.9719153822571454, -0.9707630496616201,\n        -0.969587899878116, -0.968389960527806, -0.9671692597675167,\n        -0.9659258262890684, -0.9646596893185995, -0.9633708786158806,\n        -0.9620594244736133, -0.9607253577167206, -0.9593687097016202,\n        -0.9579895123154889, -0.9565877979755121, -0.955163599628123,\n        -0.9537169507482268, -0.9522478853384154, -0.9507564379281668,\n        -0.949242643573034, -0.9477065378538223, -0.9461481568757506,\n        -0.944567537267605, -0.9429647161808762, -0.9413397312888874,\n        -0.9396926207859083, -0.9380234233862579, -0.936332178323393,\n        -0.9346189253489885, -0.9328837047320004, -0.9311265572577221,\n        -0.9293475242268224, -0.9275466474543788, -0.9257239692688904,\n        -0.9238795325112866, -0.9220133805339186, -0.9201255571995389,\n        -0.9182161068802742, -0.9162850744565779, -0.9143325053161796,\n        -0.9123584453530141, -0.9103629409661469, -0.9083460390586794,\n        -0.9063077870366503, -0.9042482328079181, -0.9021674247810376,\n        -0.9000654118641209, -0.8979422434636882, -0.895797969483505,\n        -0.8936326403234123, -0.8914463068781384, -0.8892390205361063,\n        -0.887010833178222, -0.8847617971766579, -0.8824919653936216,\n        -0.8802013911801113, -0.8778901283746645, -0.8755582313020907,\n        -0.8732057547721959, -0.870832754078492, -0.8684392849969007,\n        -0.8660254037844386, -0.8635911671778989, -0.8611366323925137,\n        -0.8586618571206134, -0.8561668995302666, -0.8536518182639167,\n        -0.8511166724369998, -0.848561521636559, -0.8459864259198413,\n        -0.8433914458128857, -0.8407766423091034, -0.8381420768678404,\n        -0.8354878114129367, -0.8328139083312672, -0.8301204304712791,\n        -0.8274074411415107, -0.8246750041091067, -0.8219231835983183,\n        -0.8191520442889918, -0.8163616513150517, -0.8135520702629676,\n        -0.8107233671702121, -0.8078756085237113, -0.8050088612582788,\n        -0.802123192755044, -0.7992186708398701, -0.796295363781756,\n        -0.7933533402912352, -0.7903926695187591, -0.7874134210530724,\n        -0.7844156649195756, -0.7813994715786825, -0.7783649119241599,\n        -0.7753120572814661, -0.7722409794060691, -0.7691517504817653,\n        -0.7660444431189781, -0.7629191303530556, -0.7597758856425495,\n        -0.7566147828674927, -0.753435896327661, -0.7502393007408246,\n        -0.7470250712409963, -0.7437932833766612, -0.740544013109005,\n        -0.7372773368101242, -0.7339933312612357, -0.7306920736508676,\n        -0.7273736415730492, -0.7240381130254828, -0.7206855664077145,\n        -0.7173160805192891, -0.7139297345578991, -0.7105266081175208,\n        -0.7071067811865477, -0.7036703341459063, -0.7002173477671687,\n        -0.6967479032106556, -0.6932620820235246, -0.6897599661378577,\n        -0.6862416378687334, -0.6827071799122926, -0.679156675343793,\n        -0.6755902076156605, -0.6720078605555223, -0.6684097183642427,\n        -0.6647958656139378, -0.6611663872459935, -0.6575213685690637,\n        -0.6538608952570701, -0.6501850533471836, -0.6464939292378064,\n        -0.6427876096865396, -0.6390661818081416, -0.6353297330724854,\n        -0.6315783513024976, -0.627812124672099, -0.624031141704127,\n        -0.6202354912682606, -0.6164252625789255, -0.6126005451932034,\n        -0.6087614290087209, -0.6049080042615417, -0.6010403615240425,\n        -0.5971585917027863, -0.5932627860363818, -0.589353036093345,\n        -0.585429433769941, -0.5814920712880269, -0.5775410411928856,\n        -0.5735764363510465, -0.5695983499481065, -0.5656068754865391,\n        -0.561602106783493, -0.5575841379685926, -0.5535530634817225,\n        -0.5495089780708059, -0.5454519767895828, -0.5413821549953696,\n        -0.5372996083468242, -0.5332044328016913, -0.5290967246145529,\n        -0.5249765803345603, -0.5208440968031696, -0.5166993711518632,\n        -0.5125425007998651, -0.5083735834518559, -0.5041927170956704,\n        -0.5000000000000004, -0.49579553071207927, -0.49157940805537115,\n        -0.48735173112724256, -0.48311259929663913, -0.4788621122017438,\n        -0.47460036974764036, -0.4703274721039621, -0.4660435197025389,\n        -0.46174861323503363, -0.45744285365058096, -0.4531263421534088,\n        -0.44879918020046244, -0.4444614694990217, -0.4401133120043052,\n        -0.43575480991708015, -0.4313860656812539, -0.42700718198147153,\n        -0.4226182617406992, -0.4182194081178066, -0.413810724505139,\n        -0.40939231452609287, -0.40496428203267354, -0.400526731103061,\n        -0.3960797660391569, -0.39162349136413954, -0.38715801182000076,\n        -0.3826834323650904, -0.37819985817164276, -0.3737073946233104,\n        -0.3692061473126848, -0.36469622203881186, -0.3601777248047109,\n        -0.3556507618148766, -0.3511154394727894, -0.34657186437840776,\n        -0.34202014332566943, -0.3374603832999744, -0.3328926914756765,\n        -0.32831717521356063, -0.32373394205832107, -0.31914309973603056,\n        -0.3145447561516138, -0.3099390193863049, -0.30532599769511337,\n        -0.30070579950427384, -0.2960785334087003, -0.29144430816943584,\n        -0.2868032327110907, -0.28215541611928785, -0.2775009676380955,\n        -0.2728399966674612, -0.2681726127606372, -0.2634989256216111,\n        -0.2588190451025207, -0.2541330812010789, -0.24944114405798135,\n        -0.24474334395432432, -0.24003979130900607, -0.23533059667613823,\n        -0.23061587074244044, -0.2258957243246447, -0.22117026836688813,\n        -0.21643961393810288, -0.21170387222941123, -0.2069631545515055,\n        -0.20221757233203852, -0.19746723711299774, -0.19271226054809037,\n        -0.1879527544001122, -0.18318883053832655, -0.17842060093583256,\n        -0.1736481776669304, -0.16887167290449245, -0.16409119891732413,\n        -0.15930686806752234, -0.15451879280784075, -0.14972708567904036,\n        -0.1449318593072471, -0.14013322640130713, -0.13533129975013158,\n        -0.13052619222005168, -0.1257180167521624, -0.12090688635966958,\n        -0.11609291412523004, -0.11127621319829996, -0.10645689679246818,\n        -0.10163507818280229, -0.09681087070317913, -0.09198438774362797,\n        -0.08715574274765832, -0.08232504920960054, -0.0774924206719312,\n        -0.07265797072261047, -0.06782181299240972, -0.06298406115223794,\n        -0.0581448289104763, -0.05330423001029832, -0.04846237822700354,\n        -0.043619387365336194, -0.038775371256817404, -0.03393044375706254,\n        -0.02908471874311221, -0.02423831011074855, -0.019391331771824397,\n        -0.014543897651582293, -0.009696121685978531, -0.004848117819001606,\n    ];\n    static reverseSinTableLookup(value: number, start: number, end: number, reverse: boolean): number {\n        while (start <= end) {\n            const mid = Math.floor((start + end) / 2);\n            const midValue = this.SIN_TABLE[mid];\n            if (midValue === value)\n                return mid;\n            if (reverse ? value < midValue : midValue < value) {\n                start = mid + 1;\n            }\n            else {\n                end = mid - 1;\n            }\n        }\n        return Math.abs(value - this.SIN_TABLE[start]) < Math.abs(value - this.SIN_TABLE[end])\n            ? start\n            : end;\n    }\n    static pow(base: number, exponent: number): number {\n        if (!Number.isFinite(base) || !Number.isFinite(exponent) ||\n            (Number.isSafeInteger(base) && Number.isSafeInteger(exponent))) {\n            return base ** exponent;\n        }\n        if (!Number.isSafeInteger(exponent)) {\n            throw new Error(\"Exponent must be a safe integer\");\n        }\n        return Math.floor(base * this.EXP_10_PRECISION) ** exponent /\n            this.EXP_10_PRECISION ** exponent;\n    }\n    static sqrt(value: number): number {\n        if (value === 0)\n            return 0;\n        let prev;\n        let result = value / 3;\n        while (Math.abs((prev = result) - (result = (value / result + result) / 2)) > 5e-15)\n            ;\n        return result;\n    }\n    static sin(angle: number): number {\n        if (!Number.isFinite(angle))\n            return NaN;\n        if (!angle)\n            return angle;\n        const normalized = angle / (2 * Math.PI) - Math.floor(angle / (2 * Math.PI));\n        const index = Math.floor(normalized * this.SIN_TABLE.length);\n        return this.SIN_TABLE[index];\n    }\n    static cos(angle: number): number {\n        return this.sin(angle + Math.PI / 2);\n    }\n    static asin(value: number): number {\n        if (!isBetween(value, -1, 1))\n            return NaN;\n        if (!value)\n            return 0;\n        const tableLength = this.SIN_TABLE.length;\n        return value > 0\n            ? (2 * Math.PI * this.reverseSinTableLookup(value, 0, tableLength / 4, false)) / tableLength\n            : Math.PI - (2 * Math.PI * this.reverseSinTableLookup(value, tableLength / 2, 0.75 * tableLength, true)) / tableLength;\n    }\n    static acos(value: number): number {\n        return Math.PI / 2 - this.asin(value);\n    }\n    static atan2(y: number, x: number): number {\n        if (Number.isNaN(x) || Number.isNaN(y))\n            return NaN;\n        if (Number.isFinite(y) || Number.isFinite(x)) {\n            if (Number.isFinite(x) && x !== 0) {\n                if (Number.isFinite(y) && y !== 0) {\n                    return this.atan2FiniteNonZero(y, x);\n                }\n                return this.signIncZero(y) * (x < 0 ? Math.PI : 0);\n            }\n            return this.signIncZero(y) * Math.PI * 0.5;\n        }\n        return Math.sign(y) * Math.PI * (Math.sign(x) > 0 ? 0.25 : 0.75);\n    }\n    static atan2FiniteNonZero(y: number, x: number): number {\n        const absX = Math.abs(x);\n        const absY = Math.abs(y);\n        const ratio = Math.min(absX, absY) / Math.max(absX, absY);\n        const ratioSquared = ratio * ratio;\n        let result = ((-0.0464964749 * ratioSquared + 0.15931422) * ratioSquared - 0.327622764) * ratioSquared * ratio + ratio;\n        if (absX < absY)\n            result = Math.PI / 2 - result;\n        if (x < 0)\n            result = Math.PI - result;\n        if (y < 0)\n            result = -result;\n        return result;\n    }\n    static signIncZero(value: number): number {\n        return value === -Infinity || 1 / value < 0 ? -1 : 1;\n    }\n}\n"
  },
  {
    "path": "src/game/math/LineCurve.ts",
    "content": "import { Vector2 } from \"./Vector2\";\nimport { LineCurve as ThreeLineCurve } from \"three\";\nexport class LineCurve extends ThreeLineCurve {\n    constructor(v1?: Vector2, v2?: Vector2) {\n        super(v1 || new Vector2(), v2 || new Vector2());\n    }\n    getPoint(t: number, optionalTarget?: Vector2): Vector2 {\n        return super.getPoint(t, optionalTarget || new Vector2());\n    }\n}\n"
  },
  {
    "path": "src/game/math/Matrix4.ts",
    "content": "import { GameMath } from './GameMath';\nimport { Vector3 } from './Vector3';\nimport * as THREE from 'three';\nexport class Matrix4 extends THREE.Matrix4 {\n    private static _v1 = new Vector3();\n    private static _v2 = new Vector3();\n    private static _v3 = new Vector3();\n    private static _v4 = new Vector3();\n    private static _matrix = new Matrix4();\n    extractRotation(matrix: THREE.Matrix4): this {\n        const elements = this.elements;\n        const matrixElements = matrix.elements;\n        const scaleX = 1 / Matrix4._v1.setFromMatrixColumn(matrix, 0).length();\n        const scaleY = 1 / Matrix4._v1.setFromMatrixColumn(matrix, 1).length();\n        const scaleZ = 1 / Matrix4._v1.setFromMatrixColumn(matrix, 2).length();\n        elements[0] = matrixElements[0] * scaleX;\n        elements[1] = matrixElements[1] * scaleX;\n        elements[2] = matrixElements[2] * scaleX;\n        elements[3] = 0;\n        elements[4] = matrixElements[4] * scaleY;\n        elements[5] = matrixElements[5] * scaleY;\n        elements[6] = matrixElements[6] * scaleY;\n        elements[7] = 0;\n        elements[8] = matrixElements[8] * scaleZ;\n        elements[9] = matrixElements[9] * scaleZ;\n        elements[10] = matrixElements[10] * scaleZ;\n        elements[11] = 0;\n        elements[12] = 0;\n        elements[13] = 0;\n        elements[14] = 0;\n        elements[15] = 1;\n        return this;\n    }\n    makeRotationFromEuler(euler: THREE.Euler): this {\n        if (!euler || !euler.isEuler) {\n            console.error('THREE.Matrix4: .makeRotationFromEuler() now expects a Euler rotation rather than a Vector3 and order.');\n        }\n        const elements = this.elements;\n        const x = euler.x;\n        const y = euler.y;\n        const z = euler.z;\n        const a = GameMath.cos(x);\n        const b = GameMath.sin(x);\n        const c = GameMath.cos(y);\n        const d = GameMath.sin(y);\n        const e = GameMath.cos(z);\n        const f = GameMath.sin(z);\n        if (euler.order === 'XYZ') {\n            const ae = a * e;\n            const af = a * f;\n            const be = b * e;\n            const bf = b * f;\n            elements[0] = c * e;\n            elements[4] = -c * f;\n            elements[8] = d;\n            elements[1] = af + be * d;\n            elements[5] = ae - bf * d;\n            elements[9] = -b * c;\n            elements[2] = bf - ae * d;\n            elements[6] = be + af * d;\n            elements[10] = a * c;\n        }\n        else if (euler.order === 'YXZ') {\n            const ce = c * e;\n            const cf = c * f;\n            const de = d * e;\n            const df = d * f;\n            elements[0] = ce + df * b;\n            elements[4] = de * b - cf;\n            elements[8] = a * d;\n            elements[1] = a * f;\n            elements[5] = a * e;\n            elements[9] = -b;\n            elements[2] = cf * b - de;\n            elements[6] = df + ce * b;\n            elements[10] = a * c;\n        }\n        else if (euler.order === 'ZXY') {\n            const ce = c * e;\n            const cf = c * f;\n            const de = d * e;\n            const df = d * f;\n            elements[0] = ce - df * b;\n            elements[4] = -a * f;\n            elements[8] = de + cf * b;\n            elements[1] = cf + de * b;\n            elements[5] = a * e;\n            elements[9] = df - ce * b;\n            elements[2] = -a * d;\n            elements[6] = b;\n            elements[10] = a * c;\n        }\n        else if (euler.order === 'ZYX') {\n            const ae = a * e;\n            const af = a * f;\n            const be = b * e;\n            const bf = b * f;\n            elements[0] = c * e;\n            elements[4] = be * d - af;\n            elements[8] = ae * d + bf;\n            elements[1] = c * f;\n            elements[5] = bf * d + ae;\n            elements[9] = af * d - be;\n            elements[2] = -d;\n            elements[6] = b * c;\n            elements[10] = a * c;\n        }\n        else if (euler.order === 'YZX') {\n            const ac = a * c;\n            const ad = a * d;\n            const bc = b * c;\n            const bd = b * d;\n            elements[0] = c * e;\n            elements[4] = bd - ac * f;\n            elements[8] = bc * f + ad;\n            elements[1] = f;\n            elements[5] = a * e;\n            elements[9] = -b * e;\n            elements[2] = -d * e;\n            elements[6] = ad * f + bc;\n            elements[10] = ac - bd * f;\n        }\n        else if (euler.order === 'XZY') {\n            const ac = a * c;\n            const ad = a * d;\n            const bc = b * c;\n            const bd = b * d;\n            elements[0] = c * e;\n            elements[4] = -f;\n            elements[8] = d * e;\n            elements[1] = ac * f + bd;\n            elements[5] = a * e;\n            elements[9] = ad * f - bc;\n            elements[2] = bc * f - ad;\n            elements[6] = b * e;\n            elements[10] = bd * f + ac;\n        }\n        elements[3] = 0;\n        elements[7] = 0;\n        elements[11] = 0;\n        elements[12] = 0;\n        elements[13] = 0;\n        elements[14] = 0;\n        elements[15] = 1;\n        return this;\n    }\n    lookAt(eye: THREE.Vector3, target: THREE.Vector3, up: THREE.Vector3): this {\n        const x = Matrix4._v1;\n        const y = Matrix4._v2;\n        const z = Matrix4._v3;\n        const elements = this.elements;\n        z.subVectors(eye, target);\n        if (z.lengthSq() === 0) {\n            z.z = 1;\n        }\n        z.normalize();\n        x.crossVectors(up, z);\n        if (x.lengthSq() === 0) {\n            if (Math.abs(up.z) === 1) {\n                z.x += 0.0001;\n            }\n            else {\n                z.z += 0.0001;\n            }\n            z.normalize();\n            x.crossVectors(up, z);\n        }\n        x.normalize();\n        y.crossVectors(z, x);\n        elements[0] = x.x;\n        elements[4] = y.x;\n        elements[8] = z.x;\n        elements[1] = x.y;\n        elements[5] = y.y;\n        elements[9] = z.y;\n        elements[2] = x.z;\n        elements[6] = y.z;\n        elements[10] = z.z;\n        return this;\n    }\n    getMaxScaleOnAxis(): number {\n        const elements = this.elements;\n        const scaleXSq = elements[0] * elements[0] + elements[1] * elements[1] + elements[2] * elements[2];\n        const scaleYSq = elements[4] * elements[4] + elements[5] * elements[5] + elements[6] * elements[6];\n        const scaleZSq = elements[8] * elements[8] + elements[9] * elements[9] + elements[10] * elements[10];\n        return GameMath.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq));\n    }\n    makeRotationX(theta: number): this {\n        const c = GameMath.cos(theta);\n        const s = GameMath.sin(theta);\n        this.set(1, 0, 0, 0, 0, c, -s, 0, 0, s, c, 0, 0, 0, 0, 1);\n        return this;\n    }\n    makeRotationY(theta: number): this {\n        const c = GameMath.cos(theta);\n        const s = GameMath.sin(theta);\n        this.set(c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1);\n        return this;\n    }\n    makeRotationZ(theta: number): this {\n        const c = GameMath.cos(theta);\n        const s = GameMath.sin(theta);\n        this.set(c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);\n        return this;\n    }\n    makeRotationAxis(axis: THREE.Vector3, angle: number): this {\n        const c = GameMath.cos(angle);\n        const s = GameMath.sin(angle);\n        const t = 1 - c;\n        const x = axis.x;\n        const y = axis.y;\n        const z = axis.z;\n        const tx = t * x;\n        const ty = t * y;\n        this.set(tx * x + c, tx * y - s * z, tx * z + s * y, 0, tx * y + s * z, ty * y + c, ty * z - s * x, 0, tx * z - s * y, ty * z + s * x, t * z * z + c, 0, 0, 0, 0, 1);\n        return this;\n    }\n    decompose(position: THREE.Vector3, quaternion: THREE.Quaternion, scale: THREE.Vector3): this {\n        const elements = this.elements;\n        let sx = Matrix4._v1.set(elements[0], elements[1], elements[2]).length();\n        const sy = Matrix4._v1.set(elements[4], elements[5], elements[6]).length();\n        const sz = Matrix4._v1.set(elements[8], elements[9], elements[10]).length();\n        if (this.determinant() < 0) {\n            sx = -sx;\n        }\n        position.x = elements[12];\n        position.y = elements[13];\n        position.z = elements[14];\n        Matrix4._matrix.copy(this);\n        const invSX = 1 / sx;\n        const invSY = 1 / sy;\n        const invSZ = 1 / sz;\n        Matrix4._matrix.elements[0] *= invSX;\n        Matrix4._matrix.elements[1] *= invSX;\n        Matrix4._matrix.elements[2] *= invSX;\n        Matrix4._matrix.elements[4] *= invSY;\n        Matrix4._matrix.elements[5] *= invSY;\n        Matrix4._matrix.elements[6] *= invSY;\n        Matrix4._matrix.elements[8] *= invSZ;\n        Matrix4._matrix.elements[9] *= invSZ;\n        Matrix4._matrix.elements[10] *= invSZ;\n        quaternion.setFromRotationMatrix(Matrix4._matrix);\n        scale.x = sx;\n        scale.y = sy;\n        scale.z = sz;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/math/QuadraticBezierCurve.ts",
    "content": "import { Vector2 } from './Vector2';\nimport * as THREE from 'three';\nexport class QuadraticBezierCurve extends THREE.QuadraticBezierCurve {\n    constructor(v0?: THREE.Vector2, v1?: THREE.Vector2, v2?: THREE.Vector2) {\n        super(v0 || new Vector2(), v1 || new Vector2(), v2 || new Vector2());\n    }\n    getPoint(t: number, optionalTarget?: THREE.Vector2): THREE.Vector2 {\n        return super.getPoint(t, optionalTarget || new Vector2());\n    }\n}\n"
  },
  {
    "path": "src/game/math/Quaternion.ts",
    "content": "import * as THREE from 'three';\nimport { GameMath } from './GameMath';\nexport class Quaternion extends THREE.Quaternion {\n    setFromEuler(euler: THREE.Euler, update: boolean = true): this {\n        if (!euler || !euler.isEuler) {\n            throw new Error('THREE.Quaternion: .setFromEuler() now expects an Euler rotation rather than a Vector3 and order.');\n        }\n        const x = euler.x;\n        const y = euler.y;\n        const z = euler.z;\n        const order = euler.order;\n        const cx = GameMath.cos(x / 2);\n        const cy = GameMath.cos(y / 2);\n        const cz = GameMath.cos(z / 2);\n        const sx = GameMath.sin(x / 2);\n        const sy = GameMath.sin(y / 2);\n        const sz = GameMath.sin(z / 2);\n        if (order === 'XYZ') {\n            this.x = sx * cy * cz + cx * sy * sz;\n            this.y = cx * sy * cz - sx * cy * sz;\n            this.z = cx * cy * sz + sx * sy * cz;\n            this.w = cx * cy * cz - sx * sy * sz;\n        }\n        else if (order === 'YXZ') {\n            this.x = sx * cy * cz + cx * sy * sz;\n            this.y = cx * sy * cz - sx * cy * sz;\n            this.z = cx * cy * sz - sx * sy * cz;\n            this.w = cx * cy * cz + sx * sy * sz;\n        }\n        else if (order === 'ZXY') {\n            this.x = sx * cy * cz - cx * sy * sz;\n            this.y = cx * sy * cz + sx * cy * sz;\n            this.z = cx * cy * sz + sx * sy * cz;\n            this.w = cx * cy * cz - sx * sy * sz;\n        }\n        else if (order === 'ZYX') {\n            this.x = sx * cy * cz - cx * sy * sz;\n            this.y = cx * sy * cz + sx * cy * sz;\n            this.z = cx * cy * sz - sx * sy * cz;\n            this.w = cx * cy * cz + sx * sy * sz;\n        }\n        else if (order === 'YZX') {\n            this.x = sx * cy * cz + cx * sy * sz;\n            this.y = cx * sy * cz + sx * cy * sz;\n            this.z = cx * cy * sz - sx * sy * cz;\n            this.w = cx * cy * cz - sx * sy * sz;\n        }\n        else if (order === 'XZY') {\n            this.x = sx * cy * cz - cx * sy * sz;\n            this.y = cx * sy * cz - sx * cy * sz;\n            this.z = cx * cy * sz + sx * sy * cz;\n            this.w = cx * cy * cz + sx * sy * sz;\n        }\n        if (update !== false) {\n            this._onChangeCallback();\n        }\n        return this;\n    }\n    setFromAxisAngle(axis: THREE.Vector3, angle: number): this {\n        const halfAngle = angle / 2;\n        const s = GameMath.sin(halfAngle);\n        this.x = axis.x * s;\n        this.y = axis.y * s;\n        this.z = axis.z * s;\n        this.w = GameMath.cos(halfAngle);\n        this._onChangeCallback();\n        return this;\n    }\n    setFromRotationMatrix(m: THREE.Matrix4): this {\n        const te = m.elements;\n        const m11 = te[0];\n        const m12 = te[4];\n        const m13 = te[8];\n        const m21 = te[1];\n        const m22 = te[5];\n        const m23 = te[9];\n        const m31 = te[2];\n        const m32 = te[6];\n        const m33 = te[10];\n        const trace = m11 + m22 + m33;\n        let s;\n        if (trace > 0) {\n            s = 0.5 / GameMath.sqrt(trace + 1.0);\n            this.w = 0.25 / s;\n            this.x = (m32 - m23) * s;\n            this.y = (m13 - m31) * s;\n            this.z = (m21 - m12) * s;\n        }\n        else if (m11 > m22 && m11 > m33) {\n            s = 2.0 * GameMath.sqrt(1.0 + m11 - m22 - m33);\n            this.w = (m32 - m23) / s;\n            this.x = 0.25 * s;\n            this.y = (m12 + m21) / s;\n            this.z = (m13 + m31) / s;\n        }\n        else if (m22 > m33) {\n            s = 2.0 * GameMath.sqrt(1.0 + m22 - m11 - m33);\n            this.w = (m13 - m31) / s;\n            this.x = (m12 + m21) / s;\n            this.y = 0.25 * s;\n            this.z = (m23 + m32) / s;\n        }\n        else {\n            s = 2.0 * GameMath.sqrt(1.0 + m33 - m11 - m22);\n            this.w = (m21 - m12) / s;\n            this.x = (m13 + m31) / s;\n            this.y = (m23 + m32) / s;\n            this.z = 0.25 * s;\n        }\n        this._onChangeCallback();\n        return this;\n    }\n    length(): number {\n        return GameMath.sqrt(this.x * this.x +\n            this.y * this.y +\n            this.z * this.z +\n            this.w * this.w);\n    }\n    slerp(qb: THREE.Quaternion, t: number): this {\n        if (t === 0)\n            return this;\n        if (t === 1)\n            return this.copy(qb);\n        const x = this.x;\n        const y = this.y;\n        const z = this.z;\n        const w = this.w;\n        let cosHalfTheta = w * qb.w + x * qb.x + y * qb.y + z * qb.z;\n        if (cosHalfTheta < 0) {\n            this.w = -qb.w;\n            this.x = -qb.x;\n            this.y = -qb.y;\n            this.z = -qb.z;\n            cosHalfTheta = -cosHalfTheta;\n        }\n        else {\n            this.copy(qb);\n        }\n        if (cosHalfTheta >= 1.0) {\n            this.w = w;\n            this.x = x;\n            this.y = y;\n            this.z = z;\n            return this;\n        }\n        const sqrSinHalfTheta = 1.0 - cosHalfTheta * cosHalfTheta;\n        if (sqrSinHalfTheta <= Number.EPSILON) {\n            const s = 1 - t;\n            this.w = s * w + t * this.w;\n            this.x = s * x + t * this.x;\n            this.y = s * y + t * this.y;\n            this.z = s * z + t * this.z;\n            return this.normalize();\n        }\n        const sinHalfTheta = GameMath.sqrt(sqrSinHalfTheta);\n        const halfTheta = GameMath.atan2(sinHalfTheta, cosHalfTheta);\n        const ratioA = GameMath.sin((1 - t) * halfTheta) / sinHalfTheta;\n        const ratioB = GameMath.sin(t * halfTheta) / sinHalfTheta;\n        this.w = w * ratioA + this.w * ratioB;\n        this.x = x * ratioA + this.x * ratioB;\n        this.y = y * ratioA + this.y * ratioB;\n        this.z = z * ratioA + this.z * ratioB;\n        this._onChangeCallback();\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/math/Spherical.ts",
    "content": "import { clamp } from '../../util/math';\nimport { GameMath } from './GameMath';\nimport * as THREE from 'three';\nexport class Spherical extends THREE.Spherical {\n    setFromVector3(v: THREE.Vector3): this {\n        this.radius = v.length();\n        if (this.radius === 0) {\n            this.theta = 0;\n            this.phi = 0;\n        }\n        else {\n            this.theta = GameMath.atan2(v.x, v.z);\n            this.phi = GameMath.acos(clamp(v.y / this.radius, -1, 1));\n        }\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/math/Vector2.ts",
    "content": "import { GameMath } from './GameMath';\nimport * as THREE from 'three';\nexport class Vector2 extends THREE.Vector2 {\n    length(): number {\n        return GameMath.sqrt(this.x * this.x + this.y * this.y);\n    }\n    angle(): number {\n        let angle = GameMath.atan2(this.y, this.x);\n        return angle < 0 ? angle + 2 * Math.PI : angle;\n    }\n    distanceTo(v: THREE.Vector2): number {\n        return GameMath.sqrt(this.distanceToSquared(v));\n    }\n    rotateAround(center: THREE.Vector2, angle: number): this {\n        const cos = GameMath.cos(angle);\n        const sin = GameMath.sin(angle);\n        const x = this.x - center.x;\n        const y = this.y - center.y;\n        this.x = x * cos - y * sin + center.x;\n        this.y = x * sin + y * cos + center.y;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/math/Vector3.ts",
    "content": "import { clamp } from '../../util/math';\nimport { GameMath } from './GameMath';\nimport { Quaternion } from './Quaternion';\nimport * as THREE from 'three';\nexport class Vector3 extends THREE.Vector3 {\n    private static _quaternion = new Quaternion();\n    private static _vector = new Vector3();\n    applyEuler(euler: THREE.Euler): this {\n        if (!euler || !euler.isEuler) {\n            console.error('THREE.Vector3: .applyEuler() now expects an Euler rotation rather than a Vector3 and order.');\n        }\n        return this.applyQuaternion(Vector3._quaternion.setFromEuler(euler));\n    }\n    applyAxisAngle(axis: THREE.Vector3, angle: number): this {\n        return this.applyQuaternion(Vector3._quaternion.setFromAxisAngle(axis, angle));\n    }\n    length(): number {\n        return GameMath.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);\n    }\n    projectOnPlane(planeNormal: THREE.Vector3): this {\n        Vector3._vector.copy(this).projectOnVector(planeNormal);\n        return this.sub(Vector3._vector);\n    }\n    reflect(normal: THREE.Vector3): this {\n        return this.sub(Vector3._vector.copy(normal).multiplyScalar(2 * this.dot(normal)));\n    }\n    angleTo(v: THREE.Vector3): number {\n        const theta = this.dot(v) / GameMath.sqrt(this.lengthSq() * v.lengthSq());\n        return GameMath.acos(clamp(theta, -1, 1));\n    }\n    distanceTo(v: THREE.Vector3): number {\n        return GameMath.sqrt(this.distanceToSquared(v));\n    }\n    setFromSpherical(spherical: THREE.Spherical): this {\n        const sinPhiRadius = GameMath.sin(spherical.phi) * spherical.radius;\n        this.x = sinPhiRadius * GameMath.sin(spherical.theta);\n        this.y = GameMath.cos(spherical.phi) * spherical.radius;\n        this.z = sinPhiRadius * GameMath.cos(spherical.theta);\n        return this;\n    }\n    setFromCylindrical(cylindrical: THREE.Cylindrical): this {\n        this.x = cylindrical.radius * GameMath.sin(cylindrical.theta);\n        this.y = cylindrical.y;\n        this.z = cylindrical.radius * GameMath.cos(cylindrical.theta);\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/math/geometry.ts",
    "content": "import { clamp } from \"../../util/math\";\nimport { GameMath } from \"./GameMath\";\nimport { Matrix4 } from \"./Matrix4\";\nimport { Quaternion } from \"./Quaternion\";\nimport { Vector2 } from \"./Vector2\";\nimport { Vector3 } from \"./Vector3\";\nconst RAD2DEG = 180 / Math.PI;\nconst DEG2RAD = Math.PI / 180;\nexport function radToDeg(rad: number): number {\n    return rad * RAD2DEG;\n}\nexport function degToRad(deg: number): number {\n    return deg * DEG2RAD;\n}\nexport function rotateVec2(vec: Vector2, angle: number): Vector2 {\n    const rad = degToRad(Math.floor(angle));\n    return vec.rotateAround(new Vector2(), rad);\n}\nexport function angleDegFromVec2(vec: Vector2): number {\n    return Math.round(radToDeg(vec.angle()));\n}\nexport function angleDegBetweenVec2(vec1: Vector2, vec2: Vector2): number {\n    const angle1 = angleDegFromVec2(vec1);\n    const angle2 = angleDegFromVec2(vec2);\n    return Math.min((angle1 - angle2 + 360) % 360, (angle2 - angle1 + 360) % 360);\n}\nexport function angleDegBetweenVec3(vec1: Vector3, vec2: Vector3): number {\n    return angleBetweenQuaternions(quaternionFromVec3(vec1, new Quaternion()), quaternionFromVec3(vec2, new Quaternion()));\n}\nexport function quaternionFromVec3(vec: Vector3, quat: Quaternion = new Quaternion()): Quaternion {\n    return quat.setFromRotationMatrix(new Matrix4().lookAt(vec, new Vector3(0, 0, 0), new Vector3(0, 1, 0)));\n}\nexport function rotateVec3Towards(vec: Vector3, target: Vector3, maxAngle: number): void {\n    const length = vec.length();\n    const targetQuat = quaternionFromVec3(target, new Quaternion());\n    const currentQuat = quaternionFromVec3(vec, new Quaternion());\n    const angle = angleBetweenQuaternions(currentQuat, targetQuat);\n    if (angle !== 0) {\n        const t = Math.min(1, maxAngle / angle);\n        currentQuat.slerp(targetQuat, t);\n    }\n    vec.set(0, 0, 1).applyQuaternion(currentQuat).setLength(length);\n}\nfunction angleBetweenQuaternions(q1: Quaternion, q2: Quaternion): number {\n    const angle = radToDeg(2 * GameMath.acos(Math.abs(clamp(q1.dot(q2), -1, 1))));\n    return Math.round(angle);\n}\n"
  },
  {
    "path": "src/game/order/AttackMoveOrder.ts",
    "content": "import { OrderType } from \"@/game/order/OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { AttackMoveTask } from \"@/game/gameobject/task/move/AttackMoveTask\";\nimport { OrderFeedbackType } from \"@/game/order/OrderFeedbackType\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { AttackOrder } from \"@/game/order/AttackOrder\";\nimport { PlantC4Task } from \"@/game/gameobject/task/PlantC4Task\";\nimport { AttackMoveTargetTask } from \"@/game/gameobject/task/move/AttackMoveTargetTask\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { AttackTask } from \"@/game/gameobject/task/AttackTask\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\nexport class AttackMoveOrder extends AttackOrder {\n    private map: any;\n    constructor(game: any, map: any) {\n        super(game);\n        this.map = map;\n        this.orderType = OrderType.AttackMove;\n        this.targetOptional = false;\n        this.feedbackType = OrderFeedbackType.Move;\n    }\n    getPointerType(isMini: boolean, context: any): PointerType {\n        if (this.isTargetted()) {\n            let pointerType = super.getPointerType(isMini, context);\n            if (pointerType === PointerType.AttackRange || pointerType === PointerType.AttackNoRange) {\n                pointerType = PointerType.AttackMove;\n            }\n            return pointerType;\n        }\n        let isAllowed = this.isAllowed();\n        if (isAllowed) {\n            const hasBridge = !!this.target.getBridge();\n            const speedType = this.sourceObject.rules.speedType;\n            const isInfantry = this.sourceObject.isInfantry();\n            const canFly = this.sourceObject.rules.movementZone === MovementZone.Fly;\n            isAllowed = canFly ||\n                this.map.terrain.getPassableSpeed(this.target.tile, speedType, isInfantry, hasBridge) > 0 ||\n                !!this.game.mapShroudTrait\n                    .getPlayerShroud(this.sourceObject.owner)\n                    ?.isShrouded(this.target.tile, this.target.obj?.tileElevation);\n        }\n        if (isMini) {\n            return isAllowed ? PointerType.AttackMini : PointerType.NoActionMini;\n        }\n        else {\n            return isAllowed ? PointerType.AttackMove : PointerType.NoMove;\n        }\n    }\n    isValid(): boolean {\n        const isValid = this.sourceObject.isUnit() &&\n            !!this.sourceObject.attackTrait &&\n            !this.sourceObject.rules.preventAttackMove &&\n            !(this.game.mapShroudTrait\n                .getPlayerShroud(this.sourceObject.owner)\n                ?.isShrouded(this.target.tile, this.target.obj?.tileElevation) &&\n                !this.sourceObject.rules.moveToShroud) &&\n            (!this.isTargetted() || super.isValid());\n        this.feedbackType = OrderFeedbackType.Move;\n        return isValid;\n    }\n    isAllowed(): boolean {\n        return !(!this.isTargetted() &&\n            this.sourceObject.moveTrait.isDisabled()) && super.isAllowed();\n    }\n    process(): any[] {\n        if (this.isTargetted()) {\n            if (this.isC4) {\n                return [new PlantC4Task(this.game, this.target.obj)];\n            }\n            const weapon = this.sourceObject.attackTrait.selectWeaponVersus(this.sourceObject, this.target, this.game);\n            return [new AttackMoveTargetTask(this.game, this.target, weapon)];\n        }\n        return [\n            new AttackMoveTask(this.game, this.target.tile, !!this.target.getBridge(), { closeEnoughTiles: this.game.rules.general.closeEnough })\n        ];\n    }\n    isTargetted(): boolean {\n        return this.target.obj?.isTechno();\n    }\n    onAdd(taskList: any[], isQueued: boolean): boolean {\n        const unit = this.sourceObject;\n        if (!isQueued && unit.isUnit() && this.isValid() && this.isAllowed()) {\n            if (unit.rules.movementZone === MovementZone.Fly) {\n                const existingTask = taskList.find(task => [MoveTask, AttackTask, AttackMoveTask, AttackMoveTargetTask]\n                    .includes(task.constructor) && !task.isCancelling());\n                if (existingTask) {\n                    if (this.isTargetted()) {\n                        if ((unit.moveTrait.currentWaypoint?.tile === this.target.tile ||\n                            unit.isAircraft() ||\n                            existingTask.constructor !== MoveTask) &&\n                            existingTask.forceCancel(unit)) {\n                            taskList.splice(taskList.indexOf(existingTask), 1);\n                        }\n                    }\n                    else {\n                        if (existingTask.constructor === AttackMoveTask) {\n                            existingTask.updateTarget(this.target.tile, !!this.target.getBridge());\n                            taskList.splice(taskList.indexOf(existingTask) + 1);\n                            unit.unitOrderTrait.clearOrders();\n                            return false;\n                        }\n                        if (existingTask.forceCancel(unit)) {\n                            taskList.splice(taskList.indexOf(existingTask), 1);\n                        }\n                    }\n                }\n            }\n            else if (this.isTargetted() &&\n                taskList.length &&\n                unit.isUnit() &&\n                (unit.rules.locomotor === LocomotorType.Vehicle ||\n                    unit.rules.locomotor === LocomotorType.Ship)) {\n                unit.moveTrait.speedPenalty = 0.5;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/order/AttackOrder.ts",
    "content": "import { Order } from \"@/game/order/Order\";\nimport { OrderType } from \"@/game/order/OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { AttackTask } from \"@/game/gameobject/task/AttackTask\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { OrderFeedbackType } from \"@/game/order/OrderFeedbackType\";\nimport { LosHelper } from \"@/game/gameobject/unit/LosHelper\";\nimport { ArmorType } from \"@/game/type/ArmorType\";\nimport { PlantC4Task } from \"@/game/gameobject/task/PlantC4Task\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\ninterface AttackOrderOptions {\n    forceAttack?: boolean;\n    noIvanBomb?: boolean;\n}\nexport class AttackOrder extends Order {\n    protected game: any;\n    protected isC4: boolean = false;\n    protected forceAttack: boolean;\n    protected ivanBombAllowed: boolean;\n    public targetOptional: boolean = false;\n    public feedbackType: OrderFeedbackType = OrderFeedbackType.None;\n    protected rangeHelper: RangeHelper;\n    protected losHelper: LosHelper;\n    public terminal: boolean = false;\n    constructor(game: any, options: AttackOrderOptions = {}) {\n        const { forceAttack = false, noIvanBomb = false } = options;\n        super(forceAttack ? OrderType.ForceAttack : OrderType.Attack);\n        this.game = game;\n        this.forceAttack = forceAttack;\n        this.ivanBombAllowed = !noIvanBomb || forceAttack;\n        this.rangeHelper = new RangeHelper(this.game.map.tileOccupation);\n        this.losHelper = new LosHelper(this.game.map.tiles, game.map.tileOccupation);\n    }\n    getPointerType(isMini: boolean, units: any[]): PointerType {\n        if (!this.isAllowed()) {\n            return isMini ? PointerType.NoActionMini : PointerType.NoAction;\n        }\n        if (this.isC4) {\n            return PointerType.C4;\n        }\n        const weapon = this.sourceObject.attackTrait?.selectWeaponVersus(this.sourceObject, this.target, this.game, this.forceAttack);\n        if (weapon?.rules.sabotageCursor) {\n            return PointerType.C4;\n        }\n        if (this.ivanBombAllowed &&\n            this.sourceObject.rules.ivan &&\n            weapon?.warhead.rules.ivanBomb) {\n            return PointerType.Dynamite;\n        }\n        if (weapon?.warhead.rules.bombDisarm) {\n            return PointerType.DefuseBomb;\n        }\n        if (weapon && weapon.rules.damage < 0) {\n            return PointerType.RepairMove;\n        }\n        const allUnitsInRange = units.every((unit) => {\n            if (!unit.attackTrait)\n                return true;\n            const unitWeapon = unit.attackTrait.selectWeaponVersus(unit, this.target, this.game, this.forceAttack);\n            if (!unitWeapon)\n                return true;\n            return (this.rangeHelper.isInWeaponRange(unit, this.target.obj || this.target.tile, unitWeapon, this.game.rules) &&\n                this.losHelper.hasLineOfSight(unit, this.target.obj || this.target.tile, unitWeapon));\n        });\n        if (isMini) {\n            return PointerType.AttackMini;\n        }\n        return allUnitsInRange ? PointerType.AttackRange : PointerType.AttackNoRange;\n    }\n    isValid(): boolean {\n        if (!this.sourceObject.attackTrait)\n            return false;\n        if (this.forceAttack &&\n            this.game.mapShroudTrait\n                .getPlayerShroud(this.sourceObject.owner)\n                ?.isShrouded(this.target.tile, this.target.obj?.tileElevation) &&\n            !this.sourceObject.isBuilding()) {\n            return false;\n        }\n        const targetObj = this.target.obj;\n        const terrainObj = this.game.map\n            .getGroundObjectsOnTile(this.target.tile)\n            .find((obj: any) => obj.isTerrain());\n        this.terminal = !targetObj && !terrainObj;\n        if (this.sourceObject.c4 &&\n            targetObj?.isBuilding() &&\n            targetObj.c4ChargeTrait &&\n            (this.forceAttack ||\n                !this.game.areFriendly(targetObj, this.sourceObject) ||\n                targetObj.cabHutTrait)) {\n            this.isC4 = true;\n            this.feedbackType = OrderFeedbackType.SpecialAttack;\n            return true;\n        }\n        this.isC4 = false;\n        this.feedbackType = OrderFeedbackType.Attack;\n        if (!this.game.isValidTarget(targetObj))\n            return false;\n        if (!targetObj && terrainObj?.rules.immune)\n            return false;\n        if (!targetObj &&\n            this.target.tile === this.sourceObject.tile &&\n            !(this.sourceObject.isUnit() && this.sourceObject.zone === ZoneType.Air)) {\n            return false;\n        }\n        if (targetObj === this.sourceObject)\n            return false;\n        const weapon = this.sourceObject.attackTrait.selectWeaponVersus(this.sourceObject, this.target, this.game, this.forceAttack);\n        if (!weapon)\n            return false;\n        if (!this.ivanBombAllowed && weapon.warhead.rules.ivanBomb)\n            return false;\n        if (targetObj?.isBuilding() &&\n            targetObj.cabHutTrait &&\n            !weapon.warhead.rules.ivanBomb &&\n            !weapon.warhead.rules.bombDisarm) {\n            return false;\n        }\n        const canMoveOrInRange = (this.sourceObject.isUnit() &&\n            this.sourceObject.moveTrait &&\n            !this.sourceObject.moveTrait.isDisabled()) ||\n            this.rangeHelper.isInWeaponRange(this.sourceObject, targetObj || this.target.tile, weapon, this.game.rules);\n        if (!canMoveOrInRange)\n            return false;\n        if (this.sourceObject.airSpawnTrait &&\n            weapon.rules.spawner &&\n            !this.game.map.isWithinBounds(this.target.tile)) {\n            return false;\n        }\n        if (this.forceAttack)\n            return true;\n        if (targetObj?.isBuilding() && targetObj.hospitalTrait)\n            return false;\n        if (!targetObj || !targetObj.healthTrait)\n            return false;\n        if (targetObj.isDestroyed || targetObj.isCrashing)\n            return false;\n        if (targetObj.isOverlay() &&\n            (weapon.warhead.rules.wall ||\n                (weapon.warhead.rules.wood && targetObj.rules.armor === ArmorType.Wood)) &&\n            !targetObj.isTechno()) {\n            return false;\n        }\n        return true;\n    }\n    isAllowed(): boolean {\n        return !this.sourceObject.attackTrait.isDisabled();\n    }\n    process(): any[] {\n        if (this.isC4) {\n            return [new PlantC4Task(this.game, this.target.obj)];\n        }\n        const weapon = this.sourceObject.attackTrait.selectWeaponVersus(this.sourceObject, this.target, this.game, this.forceAttack);\n        return [\n            new AttackTask(this.game, this.target, weapon, {\n                force: this.forceAttack,\n            }),\n        ];\n    }\n    onAdd(tasks: any[], isQueued: boolean): boolean {\n        const unit = this.sourceObject;\n        if (!isQueued && unit.isUnit() && this.isValid() && this.isAllowed()) {\n            if (unit.rules.movementZone === MovementZone.Fly) {\n                const existingTask = tasks.find((task) => (task.constructor === MoveTask || task.constructor === AttackTask) &&\n                    !task.isCancelling());\n                if (existingTask &&\n                    (unit.moveTrait.currentWaypoint?.tile === this.target.tile ||\n                        unit.isAircraft() ||\n                        existingTask.constructor === AttackTask) &&\n                    existingTask.forceCancel(unit)) {\n                    const taskIndex = tasks.indexOf(existingTask);\n                    tasks.splice(taskIndex, 1);\n                }\n            }\n            else {\n                if (tasks.length &&\n                    unit.isUnit() &&\n                    (unit.rules.locomotor === LocomotorType.Vehicle ||\n                        unit.rules.locomotor === LocomotorType.Ship)) {\n                    unit.moveTrait.speedPenalty = 0.5;\n                }\n                const existingAttackTask = tasks.find((task) => task.constructor === AttackTask && !task.isCancelling());\n                if (existingAttackTask?.getWeapon().warhead.rules.temporal) {\n                    existingAttackTask.setForceAttack(this.forceAttack);\n                    existingAttackTask.requestTargetUpdate(this.target);\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/order/CaptureOrder.ts",
    "content": "import { Order } from \"./Order\";\nimport { OrderType } from \"./OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { CaptureBuildingTask } from \"@/game/gameobject/task/CaptureBuildingTask\";\nimport { OrderFeedbackType } from \"./OrderFeedbackType\";\nexport class CaptureOrder extends Order {\n    private game: any;\n    constructor(game: any) {\n        super(OrderType.Capture);\n        this.game = game;\n        this.targetOptional = false;\n        this.terminal = true;\n        this.feedbackType = OrderFeedbackType.Capture;\n    }\n    getPointerType(isMini: boolean): PointerType {\n        if (!this.isAllowed()) {\n            return isMini ? PointerType.NoActionMini : PointerType.NoOccupy;\n        }\n        if (isMini) {\n            return PointerType.OccupyMini;\n        }\n        if (this.game.gameOpts.multiEngineer) {\n            const generalRules = this.game.rules.general;\n            const targetObj = this.target.obj;\n            if ((!targetObj.owner.isNeutral || !generalRules.engineerAlwaysCaptureTech) &&\n                targetObj.healthTrait.health > 100 * generalRules.engineerCaptureLevel) {\n                return PointerType.EngineerDamage;\n            }\n        }\n        return PointerType.Occupy;\n    }\n    isValid(): boolean {\n        return (!(this.target.obj?.isDestroyed ||\n            !this.target.obj?.isBuilding() ||\n            !this.sourceObject.isInfantry()) &&\n            this.target.obj.rules.capturable &&\n            this.sourceObject.rules.engineer &&\n            !this.game.areFriendly(this.sourceObject, this.target.obj));\n    }\n    isAllowed(): boolean {\n        return true;\n    }\n    process(): CaptureBuildingTask[] {\n        return [new CaptureBuildingTask(this.game, this.target.obj)];\n    }\n    onAdd(tasks: any[], isQueued: boolean): boolean {\n        if (!isQueued) {\n            const existingCaptureTask = tasks.find((task) => task instanceof CaptureBuildingTask);\n            if (this.isValid() &&\n                this.isAllowed() &&\n                existingCaptureTask &&\n                !existingCaptureTask.isCancelling() &&\n                existingCaptureTask.target === this.target.obj) {\n                if (new RangeHelper(this.game.map.tileOccupation).isInTileRange(this.sourceObject, this.target.obj, 0, Math.SQRT2)) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/order/CheerOrder.ts",
    "content": "import { Order } from \"./Order\";\nimport { OrderType } from \"./OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { CheerTask } from \"@/game/gameobject/task/CheerTask\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nexport class CheerOrder extends Order {\n    constructor() {\n        super(OrderType.Cheer);\n        this.getPointerType = () => PointerType.NoAction;\n    }\n    isValid(): boolean {\n        return (this.sourceObject.isInfantry() &&\n            [StanceType.None, StanceType.Guard].includes(this.sourceObject.stance));\n    }\n    isAllowed(): boolean {\n        return true;\n    }\n    process(): CheerTask[] {\n        return [new CheerTask()];\n    }\n}\n"
  },
  {
    "path": "src/game/order/DeployOrder.ts",
    "content": "import { Order } from \"./Order\";\nimport { OrderType } from \"./OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { DeployIntoTask } from \"@/game/gameobject/task/morph/DeployIntoTask\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { UnitDeployUndeployEvent } from \"@/game/event/UnitDeployUndeployEvent\";\nimport { PrimaryFactoryChangeEvent } from \"@/game/event/PrimaryFactoryChangeEvent\";\nimport { EvacuateTransportTask } from \"@/game/gameobject/task/EvacuateTransportTask\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { Task } from \"@/game/gameobject/task/system/Task\";\nexport class DeployOrder extends Order {\n    private game: any;\n    private targeted: boolean;\n    constructor(game: any, targeted: boolean) {\n        super(targeted ? OrderType.Deploy : OrderType.DeploySelected);\n        this.game = game;\n        this.targeted = targeted;\n        this.minimapAllowed = false;\n        this.targetOptional = !targeted;\n        this.singleSelectionRequired = targeted;\n    }\n    getPointerType = (): PointerType => {\n        return this.isAllowed() ? PointerType.Deploy : PointerType.NoDeploy;\n    };\n    isValid(): boolean {\n        if (this.targeted &&\n            (!this.target.obj || this.target.obj !== this.sourceObject)) {\n            return false;\n        }\n        const sourceObject = this.sourceObject;\n        return !!((sourceObject.isInfantry() &&\n            sourceObject.deployerTrait &&\n            ![StanceType.Cheer].includes(sourceObject.stance)) ||\n            (sourceObject.isVehicle() && sourceObject.deployerTrait) ||\n            (sourceObject.isVehicle() && sourceObject.rules.deploysInto) ||\n            (sourceObject.isVehicle() && sourceObject.transportTrait) ||\n            (sourceObject.isBuilding() &&\n                sourceObject.rules.factory &&\n                !sourceObject.owner.production?.isPrimaryFactory(sourceObject)) ||\n            (sourceObject.isBuilding() && sourceObject.garrisonTrait?.units.length));\n    }\n    isAllowed(): boolean {\n        const sourceObject = this.sourceObject;\n        if (sourceObject.isVehicle() && sourceObject.transportTrait) {\n            return !!(sourceObject.transportTrait.units.length &&\n                0 <\n                    this.game.map.terrain.getPassableSpeed(sourceObject.tile, SpeedType.Foot, false, sourceObject.onBridge));\n        }\n        if ((sourceObject.isInfantry() || sourceObject.isVehicle()) && sourceObject.deployerTrait) {\n            return true;\n        }\n        if (sourceObject.isVehicle() && sourceObject.rules.deploysInto) {\n            if (sourceObject.parasiteableTrait?.isInfested() &&\n                !sourceObject.parasiteableTrait.beingBoarded) {\n                return false;\n            }\n            const constructionWorker = this.game.getConstructionWorker(sourceObject.owner);\n            if (sourceObject.moveTrait.currentWaypoint?.onBridge) {\n                return false;\n            }\n            const tile = sourceObject.moveTrait.currentWaypoint?.tile ?? sourceObject.tile;\n            return constructionWorker.canPlaceAt(sourceObject.rules.deploysInto, tile, {\n                ignoreObjects: [sourceObject],\n                ignoreAdjacent: true,\n            });\n        }\n        if (sourceObject.isBuilding() && sourceObject.rules.factory) {\n            return true;\n        }\n        if (sourceObject.isBuilding() && sourceObject.garrisonTrait?.units.length) {\n            return true;\n        }\n        throw new Error(\"Shouldn't reach this point. Missed a case.\");\n    }\n    process(): Task[] | undefined {\n        const sourceObject = this.sourceObject;\n        if (sourceObject.isVehicle() && sourceObject.transportTrait) {\n            return [new EvacuateTransportTask(this.game, true)];\n        }\n        if (sourceObject.isBuilding() && sourceObject.rules.factory) {\n            return undefined;\n        }\n        if (sourceObject.isVehicle() && sourceObject.rules.deploysInto) {\n            return [new DeployIntoTask(this.game)];\n        }\n        if ((sourceObject.isInfantry() || sourceObject.isVehicle()) && sourceObject.deployerTrait) {\n            return [\n                new CallbackTask(() => {\n                    sourceObject.deployerTrait.toggleDeployed();\n                    this.game.events.dispatch(new UnitDeployUndeployEvent(sourceObject, sourceObject.deployerTrait.isDeployed() ? \"undeploy\" : \"deploy\"));\n                }),\n            ];\n        }\n        if (sourceObject.isBuilding() && sourceObject.garrisonTrait?.units.length) {\n            return [\n                new CallbackTask(() => {\n                    sourceObject.garrisonTrait.evacuate(this.game, true);\n                }),\n            ];\n        }\n        return undefined;\n    }\n    onAdd(tasks: Task[], isQueued: boolean): boolean {\n        const sourceObject = this.sourceObject;\n        if (sourceObject.isBuilding() && sourceObject.rules.factory) {\n            sourceObject.owner.production.setPrimaryFactory(sourceObject);\n            this.game.events.dispatch(new PrimaryFactoryChangeEvent(sourceObject));\n            return false;\n        }\n        if (sourceObject.isVehicle() &&\n            sourceObject.transportTrait &&\n            !isQueued &&\n            this.isValid() &&\n            this.isAllowed()) {\n            const existingEvacTask = tasks.find((task: any) => task.constructor === EvacuateTransportTask &&\n                !task.isCancelling()) as any;\n            if (existingEvacTask) {\n                existingEvacTask.forceEvac();\n                return false;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/order/DockOrder.ts",
    "content": "import { Order } from \"./Order\";\nimport { OrderType } from \"./OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { Building, BuildStatus } from \"@/game/gameobject/Building\";\nimport { ReturnOreTask } from \"@/game/gameobject/task/harvester/ReturnOreTask\";\nimport { OrderFeedbackType } from \"./OrderFeedbackType\";\nimport { MoveToDockTask } from \"@/game/gameobject/task/MoveToDockTask\";\nexport class DockOrder extends Order {\n    private game: any;\n    constructor(game: any) {\n        super(OrderType.Dock);\n        this.game = game;\n        this.targetOptional = false;\n        this.feedbackType = OrderFeedbackType.Move;\n    }\n    getPointerType(isMini: boolean): PointerType {\n        if (isMini) {\n            return this.isAllowed() ? PointerType.OccupyMini : PointerType.NoActionMini;\n        }\n        return this.isAllowed() ? PointerType.Occupy : PointerType.NoOccupy;\n    }\n    isValid(): boolean {\n        const targetObj = this.target.obj;\n        if (!targetObj?.isBuilding() ||\n            targetObj.isDestroyed ||\n            !targetObj.dockTrait ||\n            targetObj.buildStatus !== BuildStatus.Ready ||\n            !this.sourceObject.isUnit() ||\n            targetObj.warpedOutTrait.isActive()) {\n            return false;\n        }\n        const isDock = !(targetObj.rules.refinery || targetObj.unitRepairTrait);\n        return (this.game.areFriendly(targetObj, this.sourceObject) &&\n            targetObj.dockTrait.isValidUnitForDock(this.sourceObject) &&\n            !targetObj.dockTrait.isDocked(this.sourceObject) &&\n            !(targetObj.unitRepairTrait &&\n                !this.sourceObject.rules.dock.includes(targetObj.name) &&\n                this.sourceObject.healthTrait.health === 100) &&\n            (!isDock ||\n                (targetObj.dockTrait.getAvailableDockCount() ?? 0) > 0 ||\n                targetObj.dockTrait.hasReservedDockForUnit(this.sourceObject)));\n    }\n    isAllowed(): boolean {\n        return true;\n    }\n    process(): (ReturnOreTask | MoveToDockTask)[] {\n        const targetObj = this.target.obj;\n        if (targetObj.rules.refinery &&\n            this.sourceObject.isVehicle() &&\n            this.sourceObject.harvesterTrait) {\n            return [new ReturnOreTask(this.game, targetObj, true, true)];\n        }\n        if (targetObj.unitRepairTrait ||\n            this.sourceObject.rules.dock.includes(targetObj.name)) {\n            return [new MoveToDockTask(this.game, targetObj)];\n        }\n        return [];\n    }\n}\n"
  },
  {
    "path": "src/game/order/EnterTransportOrder.ts",
    "content": "import { Order } from \"./Order\";\nimport { OrderType } from \"./OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { OrderFeedbackType } from \"./OrderFeedbackType\";\nimport { EnterTransportTask } from \"@/game/gameobject/task/EnterTransportTask\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { MoveState } from \"@/game/gameobject/trait/MoveTrait\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nexport class EnterTransportOrder extends Order {\n    private game: any;\n    constructor(game: any) {\n        super(OrderType.EnterTransport);\n        this.game = game;\n        this.targetOptional = false;\n        this.terminal = true;\n        this.feedbackType = OrderFeedbackType.Enter;\n    }\n    getPointerType(isMini: boolean): PointerType {\n        if (isMini) {\n            return this.isAllowed() ? PointerType.OccupyMini : PointerType.NoActionMini;\n        }\n        return this.isAllowed() ? PointerType.Occupy : PointerType.NoOccupy;\n    }\n    isValid(): boolean {\n        return !(!this.target.obj?.isVehicle() ||\n            !this.target.obj.transportTrait ||\n            this.target.obj.isDestroyed ||\n            this.target.obj === this.sourceObject ||\n            !this.game.areFriendly(this.target.obj, this.sourceObject) ||\n            (!this.sourceObject.isVehicle() && !this.sourceObject.isInfantry()));\n    }\n    isAllowed(): boolean {\n        const target = this.target.obj;\n        const source = this.sourceObject;\n        return (source.zone !== ZoneType.Air &&\n            target.zone !== ZoneType.Air &&\n            target.transportTrait.unitFitsInside(source) &&\n            target.moveTrait.moveState === MoveState.Idle &&\n            !target.warpedOutTrait.isActive() &&\n            !source.mindControllableTrait?.isActive() &&\n            !source.mindControllerTrait?.isActive());\n    }\n    process(): (EnterTransportTask | CallbackTask)[] {\n        const source = this.sourceObject;\n        const target = this.target.obj;\n        if (this.game.map.terrain.getPassableSpeed(target.tile, source.rules.speedType, source.isInfantry(), source.onBridge)) {\n            return [new EnterTransportTask(this.game, target)];\n        }\n        return [\n            new CallbackTask(() => {\n                target.unitOrderTrait.addTask(new MoveTask(this.game, source.tile, source.onBridge));\n                target.unitOrderTrait.addTask(new CallbackTask(() => {\n                    if (this.game.map.terrain.getPassableSpeed(target.tile, source.rules.speedType, source.isInfantry(), source.onBridge)) {\n                        source.unitOrderTrait.addTask(new EnterTransportTask(this.game, target));\n                    }\n                }));\n            })\n        ];\n    }\n    onAdd(tasks: any[], isQueued: boolean): boolean {\n        if (!isQueued) {\n            const existingEnterTask = tasks.find((task) => task instanceof EnterTransportTask);\n            if (this.isValid() &&\n                this.isAllowed() &&\n                existingEnterTask &&\n                !existingEnterTask.isCancelling() &&\n                existingEnterTask.target === this.target.obj) {\n                if (new RangeHelper(this.game.map.tileOccupation).isInTileRange(this.sourceObject, this.target.obj, 0, Math.SQRT2)) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/order/GatherOrder.ts",
    "content": "import { Order } from \"./Order\";\nimport { OrderType } from \"./OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { GatherOreTask } from \"@/game/gameobject/task/harvester/GatherOreTask\";\nimport { OrderFeedbackType } from \"./OrderFeedbackType\";\nexport class GatherOrder extends Order {\n    private game: any;\n    constructor(game: any) {\n        super(OrderType.Gather);\n        this.game = game;\n        this.targetOptional = false;\n        this.feedbackType = OrderFeedbackType.Move;\n    }\n    getPointerType(isMini: boolean): PointerType {\n        return isMini ? PointerType.AttackMini : PointerType.AttackNoRange;\n    }\n    isValid(): boolean {\n        if (!this.target) {\n            return false;\n        }\n        return (!(!this.sourceObject.isVehicle() ||\n            !this.sourceObject.harvesterTrait ||\n            this.sourceObject.moveTrait.isDisabled() ||\n            this.game.mapShroudTrait\n                .getPlayerShroud(this.sourceObject.owner)\n                ?.isShrouded(this.target.tile, this.target.obj?.tileElevation)) && this.target.isOre);\n    }\n    isAllowed(): boolean {\n        return true;\n    }\n    process(): GatherOreTask[] {\n        return [new GatherOreTask(this.game, this.target.tile, true)];\n    }\n}\n"
  },
  {
    "path": "src/game/order/GuardAreaOrder.ts",
    "content": "import { Order } from \"./Order\";\nimport { OrderType } from \"./OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { CallbackTask } from \"@/game/gameobject/task/system/CallbackTask\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { OrderFeedbackType } from \"./OrderFeedbackType\";\nimport { MoveTrait, MoveResult } from \"@/game/gameobject/trait/MoveTrait\";\nimport { GatherOreTask } from \"@/game/gameobject/task/harvester/GatherOreTask\";\nexport class GuardAreaOrder extends Order {\n    private game: any;\n    private targeted: boolean;\n    constructor(game: any, targeted: boolean) {\n        super(targeted ? OrderType.GuardArea : OrderType.Guard);\n        this.game = game;\n        this.targeted = targeted;\n        this.terminal = true;\n        this.targetOptional = !targeted;\n        this.minimapAllowed = targeted;\n        this.feedbackType = targeted ? OrderFeedbackType.Move : OrderFeedbackType.None;\n    }\n    getPointerType(isMini: boolean): PointerType {\n        if (isMini) {\n            return this.isAllowed() ? PointerType.GuardMini : PointerType.NoActionMini;\n        }\n        return this.isAllowed() ? PointerType.Guard : PointerType.NoMove;\n    }\n    isValid(): boolean {\n        return (this.sourceObject.isUnit() &&\n            (!!this.targetOptional || !this.sourceObject.moveTrait.isDisabled()) &&\n            !(this.target &&\n                this.game.mapShroudTrait\n                    .getPlayerShroud(this.sourceObject.owner)\n                    ?.isShrouded(this.target.tile, this.target.obj?.tileElevation) &&\n                !this.sourceObject.rules.moveToShroud));\n    }\n    isAllowed(): boolean {\n        return true;\n    }\n    process(): (MoveTask | CallbackTask | GatherOreTask)[] {\n        const targetTile = this.targeted ? this.target.tile : undefined;\n        const sourceObject = this.sourceObject;\n        const tasks: (MoveTask | CallbackTask | GatherOreTask)[] = [];\n        if (targetTile) {\n            tasks.push(new MoveTask(this.game, targetTile, !!this.target.getBridge(), {\n                closeEnoughTiles: this.game.rules.general.closeEnough,\n            }));\n        }\n        if (sourceObject.isVehicle() && sourceObject.harvesterTrait) {\n            tasks.push(new CallbackTask(() => {\n                sourceObject.harvesterTrait.lastOreSite = undefined;\n            }), new GatherOreTask(this.game, undefined, true));\n        }\n        else {\n            tasks.push(new CallbackTask(() => {\n                if (!targetTile ||\n                    [\n                        MoveResult.Success,\n                        MoveResult.CloseEnough,\n                    ].includes(this.sourceObject.moveTrait?.lastMoveResult)) {\n                    this.sourceObject.guardMode = true;\n                }\n            }));\n        }\n        return tasks;\n    }\n}\n"
  },
  {
    "path": "src/game/order/MoveOrder.ts",
    "content": "import { Order } from \"@/game/order/Order\";\nimport { OrderType } from \"@/game/order/OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { UndeployIntoTask } from \"@/game/gameobject/task/morph/UndeployIntoTask\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { OrderFeedbackType } from \"@/game/order/OrderFeedbackType\";\nimport { RallyPointChangeEvent } from \"@/game/event/RallyPointChangeEvent\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { AttackTask } from \"@/game/gameobject/task/AttackTask\";\nimport { BuildStatus } from \"@/game/gameobject/Building\";\nimport { WaitForBuildUpTask } from \"@/game/gameobject/task/WaitForBuildUpTask\";\nimport { MoveToBlockTask } from \"@/game/gameobject/task/move/MoveToBlockTask\";\nimport { LandType } from \"@/game/type/LandType\";\nimport { AttackMoveTask } from \"@/game/gameobject/task/move/AttackMoveTask\";\nimport { AttackMoveTargetTask } from \"@/game/gameobject/task/move/AttackMoveTargetTask\";\nimport { MoveTargetTask } from \"@/game/gameobject/task/move/MoveTargetTask\";\nexport class MoveOrder extends Order {\n    private game: any;\n    private map: any;\n    private unitSelection: any;\n    private forceMove: boolean;\n    public targetOptional: boolean = false;\n    public feedbackType: OrderFeedbackType = OrderFeedbackType.Move;\n    constructor(game: any, map: any, unitSelection: any, forceMove: boolean = false) {\n        super(forceMove ? OrderType.ForceMove : OrderType.Move);\n        this.game = game;\n        this.map = map;\n        this.unitSelection = unitSelection;\n        this.forceMove = forceMove;\n    }\n    getPointerType(isMini: boolean): PointerType {\n        let canMove = this.isAllowed();\n        if (!canMove ||\n            this.forceMove ||\n            this.sourceObject.isBuilding() ||\n            this.game.mapShroudTrait\n                .getPlayerShroud(this.sourceObject.owner)\n                ?.isShrouded(this.target.tile, this.target.obj?.tileElevation)) {\n            const hasBridge = !!this.target.getBridge();\n            const speedType = this.sourceObject.rules.speedType;\n            const isInfantry = this.sourceObject.isInfantry();\n            const isFlying = this.sourceObject.rules.movementZone === MovementZone.Fly;\n            const hasTerrainDisguise = this.map\n                .getObjectsOnTile(this.target.tile)\n                .some((obj: any) => (obj.isInfantry() || obj.isVehicle()) &&\n                obj.disguiseTrait?.hasTerrainDisguise());\n            if (isFlying) {\n                canMove = this.sourceObject.rules.airportBound ||\n                    this.target.tile.landType === LandType.Cliff ||\n                    (this.map.terrain.getPassableSpeed(this.target.tile, SpeedType.Amphibious, false, hasBridge) > 0 && !hasTerrainDisguise);\n            }\n            else {\n                canMove = this.map.terrain.getPassableSpeed(this.target.tile, speedType, isInfantry, hasBridge) > 0 &&\n                    !hasTerrainDisguise &&\n                    !(this.target.obj?.isTechno() &&\n                        !this.game.areFriendly(this.target.obj, this.sourceObject));\n            }\n        }\n        if (isMini) {\n            return canMove ? PointerType.MoveMini : PointerType.NoActionMini;\n        }\n        else {\n            return canMove ? PointerType.Move : PointerType.NoMove;\n        }\n    }\n    isValid(): boolean {\n        if (this.sourceObject.isBuilding() &&\n            (!this.sourceObject.rules.undeploysInto ||\n                (this.sourceObject.rules.constructionYard &&\n                    !this.game.gameOpts.mcvRepacks)) &&\n            !this.sourceObject.rallyTrait?.getRallyPoint()) {\n            return false;\n        }\n        if (this.forceMove) {\n            return true;\n        }\n        if (!this.target.obj) {\n            return true;\n        }\n        if ((this.target.obj.isOverlay() || this.target.obj.isBuilding()) &&\n            this.target.obj.rules.wall) {\n            return true;\n        }\n        if (this.target.obj.isTechno() &&\n            this.target.obj.owner === this.sourceObject.owner &&\n            this.unitSelection.isSelected(this.target.obj)) {\n            return true;\n        }\n        if ((this.target.obj.isInfantry() || this.target.obj.isVehicle()) &&\n            !!this.target.obj.disguiseTrait?.hasTerrainDisguise()) {\n            return true;\n        }\n        if (this.target.obj.isTechno() &&\n            !this.game.areFriendly(this.target.obj, this.sourceObject)) {\n            return true;\n        }\n        return false;\n    }\n    isAllowed(): boolean {\n        if (this.sourceObject.isUnit() &&\n            this.sourceObject.moveTrait.isDisabled()) {\n            return false;\n        }\n        const isShrouded = this.game.mapShroudTrait\n            .getPlayerShroud(this.sourceObject.owner)\n            ?.isShrouded(this.target.tile, this.target.obj?.tileElevation);\n        if (isShrouded) {\n            return this.sourceObject.rules.moveToShroud;\n        }\n        if (!this.forceMove &&\n            this.target.obj?.isTechno() &&\n            this.target.obj.owner === this.sourceObject.owner &&\n            this.unitSelection.isSelected(this.target.obj)) {\n            return false;\n        }\n        return true;\n    }\n    process(): any[] | undefined {\n        const sourceObject = this.sourceObject;\n        if (sourceObject.isBuilding() && sourceObject.rallyTrait?.getRallyPoint()) {\n            return undefined;\n        }\n        const closeEnoughTiles = this.game.rules.general.closeEnough;\n        if (sourceObject.isBuilding() && sourceObject.rules.undeploysInto) {\n            return [\n                new UndeployIntoTask(this.game),\n                new MoveTask(this.game, this.target.tile, !!this.target.getBridge(), { closeEnoughTiles, forceMove: this.forceMove })\n            ];\n        }\n        if (sourceObject.isUnit()) {\n            if (this.isEnemyBuildingBlock()) {\n                return [new MoveToBlockTask(this.game, this.target.obj)];\n            }\n            if (this.isFollowMove()) {\n                return [new MoveTargetTask(this.game, this.target.obj)];\n            }\n            return [\n                new MoveTask(this.game, this.target.tile, !!this.target.getBridge(), { closeEnoughTiles, forceMove: this.forceMove })\n            ];\n        }\n        return undefined;\n    }\n    private isEnemyBuildingBlock(): boolean {\n        return this.forceMove &&\n            this.sourceObject.isVehicle() &&\n            !this.sourceObject.rules.consideredAircraft &&\n            this.target.obj?.isBuilding() &&\n            !this.game.areFriendly(this.sourceObject, this.target.obj);\n    }\n    private isFollowMove(): boolean {\n        return this.forceMove &&\n            this.target.obj?.isInfantry() &&\n            this.sourceObject.isVehicle() &&\n            !this.sourceObject.rules.consideredAircraft &&\n            !this.target.obj.moveTrait.isIdle();\n    }\n    onAdd(tasks: any[], isQueued: boolean): boolean {\n        const isUndeployableBuilding = this.sourceObject.isBuilding() &&\n            this.sourceObject.rules.undeploysInto;\n        if (isUndeployableBuilding &&\n            this.sourceObject.buildStatus === BuildStatus.BuildUp) {\n            const waitTask = this.sourceObject.unitOrderTrait\n                .getTasks()\n                .find((task: any) => task instanceof WaitForBuildUpTask);\n            waitTask?.setCancellable(true);\n            return true;\n        }\n        if (!isUndeployableBuilding &&\n            this.sourceObject.isBuilding() &&\n            this.sourceObject.rallyTrait?.getRallyPoint()) {\n            this.sourceObject.rallyTrait.changeRallyPoint(this.target.tile, this.sourceObject, this.game);\n            this.game.events.dispatch(new RallyPointChangeEvent(this.sourceObject));\n            return false;\n        }\n        if (!this.isEnemyBuildingBlock() &&\n            !this.isFollowMove() &&\n            !isQueued &&\n            this.isValid() &&\n            this.isAllowed()) {\n            this.sourceObject.attackTrait?.cancelOpportunityFire();\n            const existingMoveTask = tasks.find((task: any) => task.constructor === MoveTask && !task.isCancelling());\n            if (existingMoveTask) {\n                existingMoveTask.setForceMove(this.forceMove);\n                existingMoveTask.updateTarget(this.target.tile, !!this.target.getBridge());\n                if (existingMoveTask.children.length &&\n                    existingMoveTask.children[0] instanceof AttackTask) {\n                    existingMoveTask.children[0].cancel();\n                }\n                tasks.splice(tasks.indexOf(existingMoveTask) + 1);\n                this.sourceObject.unitOrderTrait.clearOrders();\n                return false;\n            }\n            if (this.sourceObject.isUnit() &&\n                this.sourceObject.rules.movementZone === MovementZone.Fly) {\n                const attackTask = tasks.find((task: any) => [AttackTask, AttackMoveTask, AttackMoveTargetTask]\n                    .includes(task.constructor) && !task.isCancelling());\n                if (attackTask && attackTask.forceCancel(this.sourceObject)) {\n                    tasks.splice(tasks.indexOf(attackTask), 1);\n                }\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/order/OccupyOrder.ts",
    "content": "import { Order } from \"@/game/order/Order\";\nimport { OrderType } from \"@/game/order/OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { GarrisonBuildingTask } from \"@/game/gameobject/task/GarrisonBuildingTask\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { OrderFeedbackType } from \"@/game/order/OrderFeedbackType\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\nimport { EnterRecyclerTask } from \"@/game/gameobject/task/EnterRecyclerTask\";\nimport { InfiltrateBuildingTask } from \"@/game/gameobject/task/InfiltrateBuildingTask\";\nimport { EnterHospitalTask } from \"@/game/gameobject/task/EnterHospitalTask\";\nexport class OccupyOrder extends Order {\n    private game: any;\n    constructor(game: any) {\n        super(OrderType.Occupy);\n        this.game = game;\n        this.targetOptional = false;\n        this.terminal = true;\n        this.feedbackType = OrderFeedbackType.Capture;\n    }\n    getPointerType(mini: boolean): PointerType {\n        return mini\n            ? this.isAllowed()\n                ? PointerType.OccupyMini\n                : PointerType.NoActionMini\n            : this.isAllowed()\n                ? PointerType.Occupy\n                : PointerType.NoOccupy;\n    }\n    isValid(): boolean {\n        if (!(this.target.obj?.isSpawned &&\n            this.target.obj?.isBuilding() &&\n            this.sourceObject.isUnit())) {\n            return false;\n        }\n        if (this.isUnitRecycle(this.sourceObject, this.target.obj)) {\n            return true;\n        }\n        if (!this.sourceObject.isInfantry()) {\n            return false;\n        }\n        if (this.target.obj.isBuilding() && this.target.obj.hospitalTrait) {\n            return this.game.areFriendly(this.sourceObject, this.target.obj) &&\n                this.sourceObject.isInfantry();\n        }\n        if (this.target.obj.garrisonTrait) {\n            return this.target.obj.garrisonTrait.canBeOccupied() &&\n                this.sourceObject.rules.occupier &&\n                !(this.target.obj.garrisonTrait.units.length &&\n                    this.target.obj.garrisonTrait.units[0].owner !== this.sourceObject.owner) &&\n                !this.sourceObject.mindControllableTrait?.isActive() &&\n                !this.sourceObject.mindControllerTrait?.isActive();\n        }\n        return !!(this.target.obj.rules.spyable &&\n            this.sourceObject.rules.infiltrate &&\n            !this.game.areFriendly(this.sourceObject, this.target.obj));\n    }\n    private isUnitRecycle(unit: any, building: any): boolean {\n        return unit.owner === building.owner &&\n            ((unit.isInfantry() && building.rules.cloning) || building.rules.grinding) &&\n            !unit.rules.engineer;\n    }\n    isAllowed(): boolean {\n        const building = this.target.obj;\n        const unit = this.sourceObject;\n        if (this.isUnitRecycle(unit, building)) {\n            return unit.rules.movementZone !== MovementZone.Fly &&\n                unit.rules.locomotor !== LocomotorType.Chrono &&\n                this.game.sellTrait.computeRefundValue(unit) > 0;\n        }\n        if (building.hospitalTrait) {\n            return unit.healthTrait.health < 100 &&\n                unit.rules.movementZone !== MovementZone.Fly;\n        }\n        if (building.garrisonTrait) {\n            return building.garrisonTrait.units.length < building.rules.maxNumberOccupants;\n        }\n        return true;\n    }\n    process(): any[] {\n        const building = this.target.obj;\n        const unit = this.sourceObject;\n        if (this.isUnitRecycle(unit, building)) {\n            return [new EnterRecyclerTask(this.game, building)];\n        }\n        if (building.hospitalTrait) {\n            return [new EnterHospitalTask(this.game, building)];\n        }\n        if (building.garrisonTrait) {\n            return [new GarrisonBuildingTask(this.game, building)];\n        }\n        return [new InfiltrateBuildingTask(this.game, building)];\n    }\n    onAdd(tasks: any[], replace: boolean): boolean {\n        if (!replace) {\n            const existingTask = tasks.find(task => task instanceof GarrisonBuildingTask ||\n                task instanceof InfiltrateBuildingTask);\n            if (this.isValid() &&\n                this.isAllowed() &&\n                existingTask &&\n                !existingTask.isCancelling() &&\n                existingTask.target === this.target.obj) {\n                if (new RangeHelper(this.game.map.tileOccupation).isInTileRange(this.sourceObject, this.target.obj, 0, Math.SQRT2)) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/order/Order.ts",
    "content": "import { PointerType } from \"@/engine/type/PointerType\";\nimport { OrderFeedbackType } from \"./OrderFeedbackType\";\nexport abstract class Order {\n    public orderType: any;\n    public targetOptional: boolean = true;\n    public minimapAllowed: boolean = true;\n    public singleSelectionRequired: boolean = false;\n    public terminal: boolean = false;\n    public feedbackType: OrderFeedbackType = OrderFeedbackType.None;\n    public sourceObject: any;\n    public target: any;\n    constructor(orderType: any) {\n        this.orderType = orderType;\n    }\n    getPointerType(isMini: boolean, target?: any): PointerType {\n        return isMini ? PointerType.Mini : PointerType.Default;\n    }\n    set(sourceObject: any, target: any): Order {\n        this.sourceObject = sourceObject;\n        this.target = target;\n        return this;\n    }\n    isValid(): boolean {\n        return true;\n    }\n    isAllowed(): boolean {\n        return true;\n    }\n    onAdd(tasks: any[], isQueued: boolean): boolean {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/order/OrderFactory.ts",
    "content": "import { OrderType } from \"./OrderType\";\nimport { DeployOrder } from \"./DeployOrder\";\nimport { MoveOrder } from \"./MoveOrder\";\nimport { OccupyOrder } from \"./OccupyOrder\";\nimport { AttackOrder } from \"./AttackOrder\";\nimport { StopOrder } from \"./StopOrder\";\nimport { CheerOrder } from \"./CheerOrder\";\nimport { DockOrder } from \"./DockOrder\";\nimport { GatherOrder } from \"./GatherOrder\";\nimport { AttackMoveOrder } from \"./AttackMoveOrder\";\nimport { RepairOrder } from \"./RepairOrder\";\nimport { GuardAreaOrder } from \"./GuardAreaOrder\";\nimport { ScatterOrder } from \"./ScatterOrder\";\nimport { EnterTransportOrder } from \"./EnterTransportOrder\";\nimport { CaptureOrder } from \"./CaptureOrder\";\nexport class OrderFactory {\n    private game: any;\n    private map: any;\n    constructor(game: any, map: any) {\n        this.game = game;\n        this.map = map;\n    }\n    create(orderType: OrderType, options?: any) {\n        switch (orderType) {\n            case OrderType.Deploy:\n                return new DeployOrder(this.game, true);\n            case OrderType.DeploySelected:\n                return new DeployOrder(this.game, false);\n            case OrderType.ForceMove:\n                return new MoveOrder(this.game, this.map, options, true);\n            case OrderType.Move:\n                return new MoveOrder(this.game, this.map, options);\n            case OrderType.ForceAttack:\n                return new AttackOrder(this.game, { forceAttack: true });\n            case OrderType.Attack:\n                return new AttackOrder(this.game, { noIvanBomb: true });\n            case OrderType.PlaceBomb:\n                return new AttackOrder(this.game);\n            case OrderType.AttackMove:\n                return new AttackMoveOrder(this.game, this.map);\n            case OrderType.Capture:\n                return new CaptureOrder(this.game);\n            case OrderType.Occupy:\n                return new OccupyOrder(this.game);\n            case OrderType.Stop:\n                return new StopOrder(this.game);\n            case OrderType.Cheer:\n                return new CheerOrder();\n            case OrderType.Dock:\n                return new DockOrder(this.game);\n            case OrderType.Gather:\n                return new GatherOrder(this.game);\n            case OrderType.Repair:\n                return new RepairOrder(this.game);\n            case OrderType.Guard:\n                return new GuardAreaOrder(this.game, false);\n            case OrderType.GuardArea:\n                return new GuardAreaOrder(this.game, true);\n            case OrderType.Scatter:\n                return new ScatterOrder(this.game);\n            case OrderType.EnterTransport:\n                return new EnterTransportOrder(this.game);\n            default:\n                throw new Error(`Unhandled order type ${OrderType[orderType]}`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/order/OrderFeedbackType.ts",
    "content": "export enum OrderFeedbackType {\n    None = 0,\n    Move = 1,\n    Attack = 2,\n    Enter = 3,\n    Capture = 4,\n    SpecialAttack = 5\n}\n"
  },
  {
    "path": "src/game/order/OrderType.ts",
    "content": "export enum OrderType {\n    Move = 0,\n    ForceMove = 1,\n    Attack = 2,\n    ForceAttack = 3,\n    AttackMove = 4,\n    Guard = 5,\n    GuardArea = 6,\n    Capture = 7,\n    Occupy = 8,\n    Deploy = 9,\n    DeploySelected = 10,\n    Stop = 11,\n    Cheer = 12,\n    Dock = 13,\n    Gather = 14,\n    Repair = 15,\n    Scatter = 16,\n    EnterTransport = 17,\n    PlaceBomb = 18\n}\n"
  },
  {
    "path": "src/game/order/RepairOrder.ts",
    "content": "import { Order } from \"./Order\";\nimport { OrderType } from \"./OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { RangeHelper } from \"../gameobject/unit/RangeHelper\";\nimport { RepairBuildingTask } from \"../gameobject/task/RepairBuildingTask\";\nimport { OrderFeedbackType } from \"./OrderFeedbackType\";\nexport class RepairOrder extends Order {\n    private game: any;\n    public targetOptional: boolean = false;\n    public terminal: boolean = true;\n    public feedbackType: OrderFeedbackType = OrderFeedbackType.Capture;\n    constructor(game: any) {\n        super(OrderType.Repair);\n        this.game = game;\n    }\n    getPointerType(isMini: boolean): PointerType {\n        if (isMini) {\n            return this.isAllowed()\n                ? PointerType.OccupyMini\n                : PointerType.NoActionMini;\n        }\n        return this.isAllowed()\n            ? PointerType.RepairMove\n            : PointerType.NoRepair;\n    }\n    isValid(): boolean {\n        return (!!this.target.obj?.isBuilding() &&\n            !this.target.obj.isDestroyed &&\n            this.sourceObject.isInfantry() &&\n            this.sourceObject.rules.engineer &&\n            ((!this.target.obj.owner.isCombatant() &&\n                (!!this.target.obj.garrisonTrait ||\n                    !!this.target.obj.cabHutTrait)) ||\n                this.game.areFriendly(this.target.obj, this.sourceObject)));\n    }\n    isAllowed(): boolean {\n        const target = this.target.obj;\n        if (target.cabHutTrait) {\n            return target.cabHutTrait.canRepairBridge();\n        }\n        return !!(target.rules.repairable && target.healthTrait.health < 100);\n    }\n    process() {\n        const target = this.target.obj;\n        return [new RepairBuildingTask(this.game, target)];\n    }\n    onAdd(tasks: any[], isQueued: boolean): boolean {\n        if (!isQueued) {\n            const repairTask = tasks.find(task => task instanceof RepairBuildingTask);\n            if (this.isValid() &&\n                this.isAllowed() &&\n                repairTask &&\n                !repairTask.isCancelling() &&\n                repairTask.target === this.target.obj) {\n                if (new RangeHelper(this.game.map.tileOccupation).isInTileRange(this.sourceObject, this.target.obj, 0, Math.SQRT2)) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/order/ScatterOrder.ts",
    "content": "import { Order } from \"./Order\";\nimport { OrderType } from \"./OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { ScatterTask } from \"../gameobject/task/ScatterTask\";\nimport { MovementZone } from \"../type/MovementZone\";\nexport class ScatterOrder extends Order {\n    private game: any;\n    constructor(game: any) {\n        super(OrderType.Scatter);\n        this.game = game;\n    }\n    getPointerType(): PointerType {\n        return PointerType.NoAction;\n    }\n    isValid(): boolean {\n        return ((this.sourceObject.isInfantry() || this.sourceObject.isVehicle()) &&\n            this.sourceObject.rules.movementZone !== MovementZone.Fly &&\n            !this.sourceObject.moveTrait.isDisabled());\n    }\n    isAllowed(): boolean {\n        return true;\n    }\n    process() {\n        if (!this.target) {\n            throw new Error(\"Target should be set for executing a scatter order. See OrderUnitsAction.\");\n        }\n        return [\n            new ScatterTask(this.game, {\n                tile: this.target.tile,\n                toBridge: !!this.target.getBridge(),\n            }, undefined),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/game/order/StopOrder.ts",
    "content": "import { Order } from \"./Order\";\nimport { OrderType } from \"./OrderType\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { LocomotorType } from \"../type/LocomotorType\";\nimport { CallbackTask } from \"../gameobject/task/system/CallbackTask\";\nexport class StopOrder extends Order {\n    private game: any;\n    constructor(game: any) {\n        super(OrderType.Stop);\n        this.game = game;\n    }\n    getPointerType(): PointerType {\n        return PointerType.NoAction;\n    }\n    isValid(): boolean {\n        return this.sourceObject.isTechno();\n    }\n    isAllowed(): boolean {\n        return true;\n    }\n    process() {\n        return [\n            new CallbackTask((unit) => {\n                if (!unit.isUnit())\n                    return;\n                if (unit.rules.locomotor !== LocomotorType.Vehicle &&\n                    unit.rules.locomotor !== LocomotorType.Ship)\n                    return;\n                unit.moveTrait.speedPenalty = 0;\n            })\n        ];\n    }\n    onAdd(tasks: any[], isQueued: boolean): boolean {\n        const source = this.sourceObject;\n        if (!isQueued && tasks.length > 0 && source.isUnit()) {\n            if (source.rules.locomotor === LocomotorType.Vehicle ||\n                source.rules.locomotor === LocomotorType.Ship) {\n                source.moveTrait.speedPenalty = 0.5;\n            }\n        }\n        if (source.isBuilding() && source.rallyTrait?.getRallyPoint()) {\n            source.unitRepairTrait?.resetRallyPoint(source, this.game);\n            source.factoryTrait?.resetRallyPoint(source, this.game);\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/order/orderPriorities.ts",
    "content": "import { OrderType } from \"./OrderType\";\nexport const orderPriorities = [\n    OrderType.Occupy,\n    OrderType.Dock,\n    OrderType.Attack,\n    OrderType.Capture,\n    OrderType.Repair,\n    OrderType.EnterTransport,\n    OrderType.PlaceBomb,\n    OrderType.Deploy,\n    OrderType.Gather,\n];\n"
  },
  {
    "path": "src/game/player/PlayerFactory.ts",
    "content": "import { Player } from '@/game/Player';\nimport { Country } from '@/game/Country';\nimport { PowerTrait } from './trait/PowerTrait';\nimport { RadarTrait } from './trait/RadarTrait';\nimport { Production } from './production/Production';\nimport { SideType } from '../SideType';\nimport { SuperWeaponsTrait } from './trait/SuperWeaponsTrait';\nimport { SharedDetectDisguiseTrait } from './trait/SharedDetectDisguiseTrait';\nexport class PlayerFactory {\n    private rules: any;\n    private gameOpts: any;\n    private allAvailableObjects: any;\n    constructor(rules: any, gameOpts: any, allAvailableObjects: any) {\n        this.rules = rules;\n        this.gameOpts = gameOpts;\n        this.allAvailableObjects = allAvailableObjects;\n    }\n    createCombatant(id: any, country: any, team: any, color: any, isAi: boolean, aiDifficulty: any, customBotId?: string): Player {\n        let player = new Player(id, country, team, color);\n        player.isAi = isAi;\n        player.aiDifficulty = aiDifficulty;\n        player.customBotId = customBotId;\n        player.powerTrait = new PowerTrait(player);\n        player.traits.add(player.powerTrait);\n        player.radarTrait = new RadarTrait();\n        player.traits.add(player.radarTrait);\n        player.superWeaponsTrait = new SuperWeaponsTrait();\n        player.traits.add(player.superWeaponsTrait);\n        player.production = Production.factory(player, this.rules, this.gameOpts, this.allAvailableObjects);\n        player.sharedDetectDisguiseTrait = new SharedDetectDisguiseTrait();\n        return player;\n    }\n    createObserver(id: any, rules: any): Player {\n        let player = new Player(id, undefined, undefined, rules.colors.get(\"LightGrey\"));\n        player.radarTrait = new RadarTrait();\n        player.traits.add(player.radarTrait);\n        player.radarTrait.setDisabled(false);\n        return player;\n    }\n    createNeutral(rules: any, id: any): Player {\n        let neutralCountryRule = [...rules.countryRules.values()].find((country) => country.side === SideType.Civilian);\n        if (!neutralCountryRule) {\n            throw new Error(\"Missing neutral country. No country found in rules with Civilian side\");\n        }\n        let country = new Country(neutralCountryRule);\n        let player = new Player(id, country, undefined, rules.colors.get(\"LightGrey\"));\n        player.powerTrait = new PowerTrait(player);\n        player.traits.add(player.powerTrait);\n        return player;\n    }\n}\n"
  },
  {
    "path": "src/game/player/production/Production.ts",
    "content": "import { QueueType, ProductionQueue } from './ProductionQueue';\nimport { BuildCat, FactoryType } from '../../rules/TechnoRules';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { EventDispatcher } from '@/util/event';\nimport { PrereqCategory } from '@/game/rules/GeneralRules';\nimport { SideType } from '@/game/SideType';\nconst PREREQ_MAP = new Map()\n    .set(\"POWER\", PrereqCategory.Power)\n    .set(\"FACTORY\", PrereqCategory.Factory)\n    .set(\"BARRACKS\", PrereqCategory.Barracks)\n    .set(\"RADAR\", PrereqCategory.Radar)\n    .set(\"TECH\", PrereqCategory.Tech)\n    .set(\"PROC\", PrereqCategory.Proc);\nexport class Production {\n    private player: any;\n    private maxTechLevel: number;\n    private gameOpts: any;\n    private rules: any;\n    private allAvailableObjects: any[];\n    private buildSpeedModifier: number;\n    private queues: Map<QueueType, ProductionQueue>;\n    private _onQueueUpdate: EventDispatcher<any>;\n    private primaryFactories: Map<any, any>;\n    private factoryCounts: Map<any, number>;\n    private veteranTypes: Set<any>;\n    private stolenTech: Set<SideType>;\n    static factory(player: any, rules: any, gameOpts: any, availableObjects: any[]): Production {\n        const production = new Production(player, rules.mpDialogSettings.techLevel, gameOpts, rules, availableObjects);\n        const maxQueueSize = rules.general.maximumQueuedObjects + 1;\n        production.addQueue(QueueType.Structures, new ProductionQueue(QueueType.Structures, 1, 1));\n        production.addQueue(QueueType.Armory, new ProductionQueue(QueueType.Armory, 1, 1));\n        production.addQueue(QueueType.Infantry, new ProductionQueue(QueueType.Infantry, maxQueueSize, maxQueueSize));\n        production.addQueue(QueueType.Vehicles, new ProductionQueue(QueueType.Vehicles, maxQueueSize, maxQueueSize));\n        production.addQueue(QueueType.Ships, new ProductionQueue(QueueType.Ships, maxQueueSize, maxQueueSize));\n        production.addQueue(QueueType.Aircrafts, new ProductionQueue(QueueType.Aircrafts, 0, maxQueueSize));\n        return production;\n    }\n    constructor(player: any, techLevel: number, gameOpts: any, rules: any, availableObjects: any[]) {\n        this.player = player;\n        this.maxTechLevel = techLevel;\n        this.gameOpts = gameOpts;\n        this.rules = rules;\n        this.allAvailableObjects = availableObjects;\n        this.buildSpeedModifier = 1;\n        this.queues = new Map();\n        this._onQueueUpdate = new EventDispatcher();\n        this.primaryFactories = new Map();\n        this.factoryCounts = new Map();\n        this.veteranTypes = new Set();\n        this.stolenTech = new Set();\n    }\n    get onQueueUpdate() {\n        return this._onQueueUpdate.asEvent();\n    }\n    addQueue(type: QueueType, queue: ProductionQueue) {\n        this.queues.set(type, queue);\n        queue.onUpdate.subscribe(() => this._onQueueUpdate.dispatch(this, queue));\n    }\n    getQueue(type: QueueType): ProductionQueue {\n        const queue = this.queues.get(type);\n        if (!queue) {\n            throw new Error(\"No queue found with type \" + QueueType[type]);\n        }\n        return queue;\n    }\n    getAllQueues(): ProductionQueue[] {\n        return [...this.queues.values()];\n    }\n    getQueueTypeForObject(object: any): QueueType {\n        if (object.type === ObjectType.Building) {\n            return object.buildCat === BuildCat.Combat\n                ? QueueType.Armory\n                : QueueType.Structures;\n        }\n        if (object.type === ObjectType.Infantry) {\n            return QueueType.Infantry;\n        }\n        if (object.type === ObjectType.Vehicle) {\n            return object.naval ? QueueType.Ships : QueueType.Vehicles;\n        }\n        if (object.type === ObjectType.Aircraft) {\n            return QueueType.Aircrafts;\n        }\n        throw new Error(\"Unsupported object type \" + ObjectType[object.type]);\n    }\n    getQueueForObject(object: any): ProductionQueue {\n        return this.getQueue(this.getQueueTypeForObject(object));\n    }\n    getQueueTypeForFactory(type: FactoryType): QueueType {\n        if (type === FactoryType.InfantryType)\n            return QueueType.Infantry;\n        if (type === FactoryType.UnitType)\n            return QueueType.Vehicles;\n        if (type === FactoryType.AircraftType)\n            return QueueType.Aircrafts;\n        if (type === FactoryType.NavalUnitType)\n            return QueueType.Ships;\n        throw new Error(\"Unsupported factory type \" + FactoryType[type]);\n    }\n    getFactoryTypeForQueueType(type: QueueType): FactoryType {\n        if (type === QueueType.Structures || type === QueueType.Armory) {\n            return FactoryType.BuildingType;\n        }\n        if (type === QueueType.Infantry)\n            return FactoryType.InfantryType;\n        if (type === QueueType.Vehicles)\n            return FactoryType.UnitType;\n        if (type === QueueType.Aircrafts)\n            return FactoryType.AircraftType;\n        if (type === QueueType.Ships)\n            return FactoryType.NavalUnitType;\n        throw new Error(\"Unsupported queue type \" + QueueType[type]);\n    }\n    getQueueForFactory(type: FactoryType): ProductionQueue {\n        return this.getQueue(this.getQueueTypeForFactory(type));\n    }\n    isAvailableForProduction(object: any): boolean {\n        return (object.isAvailableTo(this.player.country) &&\n            object.techLevel !== -1 &&\n            object.techLevel <= this.maxTechLevel &&\n            !(object.buildLimit === 0 && !this.player.isAi) &&\n            !(object.superWeapon &&\n                this.rules.getSuperWeapon(object.superWeapon).disableableFromShell &&\n                !this.gameOpts.superWeapons) &&\n            this.hasFactoryFor(object) &&\n            this.meetsPrerequisites(object) &&\n            this.meetsStolenTech(object));\n    }\n    getAvailableObjects(): any[] {\n        return this.allAvailableObjects.filter(obj => this.isAvailableForProduction(obj));\n    }\n    hasFactoryFor(object: any): boolean {\n        if (object.owner.length) {\n            const factoryType = this.getFactoryTypeFor(object);\n            return !!Array.from(this.player.buildings).find((building: any) => building.factoryTrait?.type === factoryType &&\n                (factoryType !== FactoryType.UnitType || building.rules.naval === object.naval) &&\n                !!building.rules.owner.find((owner: string) => object.owner.includes(owner)));\n        }\n        return true;\n    }\n    meetsStolenTech(object: any): boolean {\n        return object.requiresStolenAlliedTech\n            ? this.stolenTech.has(SideType.GDI)\n            : !object.requiresStolenSovietTech || this.stolenTech.has(SideType.Nod);\n    }\n    getFactoryTypeFor(object: any): FactoryType {\n        if (object.type === ObjectType.Building)\n            return FactoryType.BuildingType;\n        if (object.type === ObjectType.Infantry)\n            return FactoryType.InfantryType;\n        if (object.type === ObjectType.Aircraft)\n            return FactoryType.AircraftType;\n        return object.naval ? FactoryType.NavalUnitType : FactoryType.UnitType;\n    }\n    meetsPrerequisites(object: any): boolean {\n        const buildingNames = Array.from(this.player.buildings).map((b: any) => b.name);\n        for (const prereq of object.prerequisite) {\n            const upperPrereq = prereq.toUpperCase();\n            if (PREREQ_MAP.has(upperPrereq)) {\n                const category = PREREQ_MAP.get(upperPrereq);\n                if (category === undefined) {\n                    throw new Error(\"Unknown prereqName \" + upperPrereq);\n                }\n                const prereqBuildings = this.rules.general.prereqCategories.get(category);\n                if (prereqBuildings === undefined) {\n                    throw new Error(`Missing prerequisite category ${category} in rules`);\n                }\n                let hasPrereq = false;\n                for (const building of prereqBuildings) {\n                    if (buildingNames.indexOf(building) !== -1) {\n                        hasPrereq = true;\n                        break;\n                    }\n                }\n                if (!hasPrereq)\n                    return false;\n            }\n            else if (buildingNames.indexOf(upperPrereq) === -1) {\n                return false;\n            }\n        }\n        return true;\n    }\n    getPrimaryFactory(type: FactoryType): any {\n        return this.primaryFactories.get(type);\n    }\n    setPrimaryFactory(building: any) {\n        if (building.rules.factory) {\n            this.primaryFactories.set(building.rules.factory, building);\n        }\n    }\n    isPrimaryFactory(building: any): boolean {\n        return this.getPrimaryFactory(building.rules.factory) === building;\n    }\n    incrementFactoryCount(type: FactoryType) {\n        this.factoryCounts.set(type, (this.factoryCounts.get(type) ?? 0) + 1);\n    }\n    decrementFactoryCount(type: FactoryType) {\n        if (!this.factoryCounts.get(type)) {\n            throw new Error(`Can't decrement factory count ${FactoryType[type]}. Already 0`);\n        }\n        this.factoryCounts.set(type, this.factoryCounts.get(type)! - 1);\n    }\n    getFactoryCount(type: FactoryType): number {\n        return this.factoryCounts.get(type) ?? 0;\n    }\n    crownPrimaryFactoryHeir(type: FactoryType) {\n        const heir = Array.from(this.player.buildings).find((building: any) => building.rules.factory === type);\n        if (heir) {\n            this.primaryFactories.set(type, heir);\n        }\n        else {\n            this.primaryFactories.delete(type);\n        }\n    }\n    hasAnyFactory(): boolean {\n        return this.primaryFactories.size > 0;\n    }\n    addVeteranType(type: any) {\n        this.veteranTypes.add(type);\n    }\n    hasVeteranType(type: any): boolean {\n        return this.veteranTypes.has(type);\n    }\n    addStolenTech(type: SideType) {\n        this.stolenTech.add(type);\n    }\n    dispose() {\n        this.queues.clear();\n        this.player = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/player/production/ProductionQueue.ts",
    "content": "import { EventDispatcher } from '@/util/event';\nexport enum QueueType {\n    Structures = 0,\n    Armory = 1,\n    Infantry = 2,\n    Vehicles = 3,\n    Aircrafts = 4,\n    Ships = 5\n}\nexport enum QueueStatus {\n    Idle = 0,\n    Active = 1,\n    OnHold = 2,\n    Ready = 3\n}\ninterface QueueItem {\n    rules: any;\n    quantity: number;\n    creditsEach: number;\n    creditsSpent: number;\n    creditsSpentLeftover: number;\n    progress: number;\n}\nexport class ProductionQueue {\n    public readonly type: QueueType;\n    private _maxSize: number;\n    private maxItemQuantity: number;\n    private items: QueueItem[];\n    private size: number;\n    private _status: QueueStatus;\n    private _onUpdate: EventDispatcher<ProductionQueue>;\n    get onUpdate() {\n        return this._onUpdate.asEvent();\n    }\n    constructor(type: QueueType, maxSize: number, maxItemQuantity: number) {\n        this.type = type;\n        this._maxSize = maxSize;\n        this.maxItemQuantity = maxItemQuantity;\n        this.items = [];\n        this.size = 0;\n        this._status = QueueStatus.Idle;\n        this._onUpdate = new EventDispatcher();\n    }\n    get status(): QueueStatus {\n        return this._status;\n    }\n    set status(value: QueueStatus) {\n        const oldStatus = this._status;\n        this._status = value;\n        if (value !== oldStatus) {\n            this._onUpdate.dispatch(this);\n        }\n    }\n    get maxSize(): number {\n        return this._maxSize;\n    }\n    set maxSize(value: number) {\n        const oldSize = this.size;\n        this.size = Math.min(value, this.size);\n        let totalQuantity = 0;\n        let itemIndex = 0;\n        while (totalQuantity <= this.size && itemIndex < this.items.length) {\n            const item = this.items[itemIndex];\n            totalQuantity += item.quantity;\n            if (totalQuantity > this.size) {\n                item.quantity -= totalQuantity - this.size;\n            }\n            if (item.quantity > 0) {\n                itemIndex++;\n            }\n        }\n        this._maxSize = value;\n        if (this.items[itemIndex]) {\n            this.items.splice(itemIndex);\n        }\n        if (oldSize !== this.size) {\n            if (!this.size) {\n                this._status = QueueStatus.Idle;\n            }\n            this._onUpdate.dispatch(this);\n        }\n    }\n    get currentSize(): number {\n        return this.size;\n    }\n    find(rules: any): QueueItem[] {\n        return this.items.filter(item => item.rules === rules);\n    }\n    getFirst(): QueueItem | undefined {\n        return this.items[0];\n    }\n    getAll(): QueueItem[] {\n        return [...this.items];\n    }\n    push(rules: any, quantity: number, creditsEach: number) {\n        quantity = Math.min(this.maxSize - this.size, quantity);\n        const existingQuantity = this.find(rules).reduce((sum, item) => sum + item.quantity, 0);\n        quantity = Math.min(this.maxItemQuantity - existingQuantity, quantity);\n        const lastItem = this.items[this.items.length - 1];\n        if (lastItem?.rules === rules) {\n            lastItem.quantity += quantity;\n        }\n        else {\n            this.items.push({\n                rules,\n                quantity,\n                creditsEach,\n                creditsSpent: 0,\n                creditsSpentLeftover: 0,\n                progress: 0\n            });\n        }\n        this.size += quantity;\n        if (quantity) {\n            if (this._status === QueueStatus.Idle) {\n                this._status = QueueStatus.Active;\n            }\n            this._onUpdate.dispatch(this);\n        }\n    }\n    insertAfterFirst(rules: any, quantity: number, creditsEach: number) {\n        quantity = Math.min(this.maxSize - this.size, quantity);\n        const existingQuantity = this.find(rules).reduce((sum, item) => sum + item.quantity, 0);\n        quantity = Math.min(this.maxItemQuantity - existingQuantity, quantity);\n        if (!quantity) {\n            return;\n        }\n        if (!this.items.length) {\n            this.push(rules, quantity, creditsEach);\n            return;\n        }\n        const first = this.items[0];\n        const firstRemainingQuantity = Math.max(0, first.quantity - 1);\n        first.quantity = 1;\n        const items = [first];\n        const tail = this.items.slice(1);\n        items.push({\n            rules,\n            quantity,\n            creditsEach,\n            creditsSpent: 0,\n            creditsSpentLeftover: 0,\n            progress: 0,\n        });\n        if (firstRemainingQuantity > 0) {\n            items.push({\n                rules: first.rules,\n                quantity: firstRemainingQuantity,\n                creditsEach: first.creditsEach,\n                creditsSpent: 0,\n                creditsSpentLeftover: 0,\n                progress: 0,\n            });\n        }\n        items.push(...tail);\n        this.items = items;\n        this.size += quantity;\n        if (this._status === QueueStatus.Idle) {\n            this._status = QueueStatus.Active;\n        }\n        this._onUpdate.dispatch(this);\n    }\n    pop(rules: any, quantity: number) {\n        this.remove(rules, quantity, false);\n    }\n    shift(rules: any, quantity: number) {\n        this.remove(rules, quantity, true);\n    }\n    private remove(rules: any, quantity: number, fromStart: boolean) {\n        const matchingItems = this.find(rules);\n        if (!matchingItems.length) {\n            throw new Error(`Can't remove non-existent item ${rules.name} from queue ${QueueType[this.type]}`);\n        }\n        const totalQuantity = matchingItems.reduce((sum, item) => sum + item.quantity, 0);\n        if (totalQuantity < quantity) {\n            throw new Error(`Attempted to remove a quantity larger than the one in queue (${rules.name})`);\n        }\n        let remainingQuantity = quantity;\n        while (remainingQuantity > 0) {\n            const item = fromStart ? matchingItems.shift() : matchingItems.pop();\n            if (item!.quantity <= remainingQuantity) {\n                const wasFirst = this.getFirst() === item;\n                this.items.splice(this.items.indexOf(item!), 1);\n                if (wasFirst) {\n                    this._status = QueueStatus.Active;\n                }\n                remainingQuantity -= item!.quantity;\n            }\n            else {\n                item!.quantity -= remainingQuantity;\n                remainingQuantity = 0;\n            }\n        }\n        this.size -= quantity;\n        if (quantity) {\n            if (!this.size) {\n                this._status = QueueStatus.Idle;\n            }\n            this._onUpdate.dispatch(this);\n        }\n    }\n    notifyUpdated() {\n        this._onUpdate.dispatch(this);\n    }\n}\n"
  },
  {
    "path": "src/game/player/trait/PowerTrait.ts",
    "content": "import { PowerLowEvent } from '../../event/PowerLowEvent';\nimport { PowerRestoreEvent } from '../../event/PowerRestoreEvent';\nimport { PowerChangeEvent } from '../../event/PowerChangeEvent';\nimport { NotifyPower } from '../../trait/interface/NotifyPower';\nimport { fnv32a } from '@/util/math';\nexport enum PowerLevel {\n    Low = 0,\n    Normal = 1\n}\nexport class PowerTrait {\n    private player: any;\n    private power: number;\n    private drain: number;\n    private level: PowerLevel;\n    private blackoutFrames: number;\n    private powerByObject: Map<any, number>;\n    constructor(player: any) {\n        this.player = player;\n        this.power = 0;\n        this.drain = 0;\n        this.level = PowerLevel.Normal;\n        this.blackoutFrames = 0;\n        this.powerByObject = new Map();\n    }\n    isLowPower(): boolean {\n        return this.level === PowerLevel.Low;\n    }\n    setBlackoutFor(frames: number, world: any) {\n        const wasBlackedOut = this.blackoutFrames > 0;\n        this.blackoutFrames = frames;\n        if (!wasBlackedOut) {\n            this.updateLevel(world);\n        }\n    }\n    updateBlackout(world: any) {\n        if (this.blackoutFrames > 0) {\n            this.blackoutFrames--;\n            if (this.blackoutFrames <= 0) {\n                this.updateLevel(world);\n            }\n        }\n    }\n    getBlackoutDuration(): number {\n        return this.blackoutFrames;\n    }\n    updateFrom(object: any, action: 'add' | 'update' | 'remove', world: any) {\n        const power = object.rules.power;\n        if (!power)\n            return;\n        if (power < 0) {\n            if (action === 'add' || action === 'remove') {\n                this.drain += action === 'add' ? -power : power;\n            }\n        }\n        else {\n            let powerDelta = 0;\n            if (action === 'add') {\n                const powerValue = Math.ceil((power * object.healthTrait.health) / 100);\n                this.powerByObject.set(object, powerValue);\n                powerDelta = powerValue;\n            }\n            else if (action === 'update' || action === 'remove') {\n                const oldPowerValue = this.powerByObject.get(object);\n                if (oldPowerValue === undefined) {\n                    throw new Error(\"Cannot update power before add.\");\n                }\n                if (action === 'update') {\n                    const newPowerValue = Math.ceil((power * object.healthTrait.health) / 100);\n                    this.powerByObject.set(object, newPowerValue);\n                    powerDelta = newPowerValue - oldPowerValue;\n                }\n                else {\n                    this.powerByObject.delete(object);\n                    powerDelta = -oldPowerValue;\n                }\n            }\n            this.power += powerDelta;\n        }\n        this.updateLevel(world);\n        world.traits.filter(NotifyPower).forEach((trait: any) => {\n            trait[NotifyPower.onPowerChange](this.player, world);\n        });\n        world.events.dispatch(new PowerChangeEvent(this.player, this.power, this.drain));\n    }\n    private updateLevel(world: any) {\n        const oldLevel = this.level;\n        this.level = this.power >= this.drain && !this.blackoutFrames\n            ? PowerLevel.Normal\n            : PowerLevel.Low;\n        if (this.level !== oldLevel) {\n            if (oldLevel === PowerLevel.Normal && this.level === PowerLevel.Low) {\n                world.traits.filter(NotifyPower).forEach((trait: any) => {\n                    trait[NotifyPower.onPowerLow](this.player, world);\n                });\n                world.events.dispatch(new PowerLowEvent(this.player));\n            }\n            if (oldLevel === PowerLevel.Low && this.level === PowerLevel.Normal) {\n                world.traits.filter(NotifyPower).forEach((trait: any) => {\n                    trait[NotifyPower.onPowerRestore](this.player, world);\n                });\n                world.events.dispatch(new PowerRestoreEvent(this.player));\n            }\n        }\n    }\n    getHash(): number {\n        return fnv32a([this.power, this.drain]);\n    }\n    debugGetState() {\n        return { power: this.power, drain: this.drain };\n    }\n    dispose() {\n        this.player = undefined;\n        this.powerByObject.clear();\n    }\n}\n"
  },
  {
    "path": "src/game/player/trait/RadarTrait.ts",
    "content": "export class RadarTrait {\n    private disabled: boolean;\n    private activeEvents: any[];\n    constructor() {\n        this.disabled = true;\n        this.activeEvents = [];\n    }\n    isDisabled(): boolean {\n        return this.disabled;\n    }\n    setDisabled(value: boolean): void {\n        this.disabled = value;\n    }\n}\n"
  },
  {
    "path": "src/game/player/trait/SharedDetectDisguiseTrait.ts",
    "content": "export class SharedDetectDisguiseTrait {\n    private objects: Set<any>;\n    constructor() {\n        this.objects = new Set();\n    }\n    add(object: any): void {\n        this.objects.add(object);\n    }\n    delete(object: any): void {\n        this.objects.delete(object);\n    }\n    has(object: any): boolean {\n        return this.objects.has(object);\n    }\n    dispose(): void {\n        this.objects.clear();\n    }\n}\n"
  },
  {
    "path": "src/game/player/trait/SuperWeaponsTrait.ts",
    "content": "export class SuperWeaponsTrait {\n    private superWeapons: Map<string, any>;\n    constructor() {\n        this.superWeapons = new Map();\n    }\n    getAll(): any[] {\n        return [...this.superWeapons.values()];\n    }\n    add(superWeapon: any): void {\n        this.superWeapons.set(superWeapon.name, superWeapon);\n    }\n    has(name: string): boolean {\n        return this.superWeapons.has(name);\n    }\n    get(name: string): any | undefined {\n        return this.superWeapons.get(name);\n    }\n    remove(name: string): void {\n        this.superWeapons.delete(name);\n    }\n}\n"
  },
  {
    "path": "src/game/rules/AiRules.ts",
    "content": "export class AiRules {\n    private buildPower: string[] = [];\n    private buildRefinery: string[] = [];\n    private buildTech: string[] = [];\n    private tiberiumFarScan: number = 50;\n    private tiberiumNearScan: number = 5;\n    readIni(ini: any): AiRules {\n        this.buildPower = ini.getArray(\"BuildPower\");\n        this.buildRefinery = ini.getArray(\"BuildRefinery\");\n        this.buildTech = ini.getArray(\"BuildTech\");\n        this.tiberiumFarScan = ini.getNumber(\"TiberiumFarScan\", 50);\n        this.tiberiumNearScan = ini.getNumber(\"TiberiumNearScan\", 5);\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/AudioVisualRules.ts",
    "content": "export class AudioVisualRules {\n    private ini: any;\n    private ambientChangeRate: number = 0;\n    private ambientChangeStep: number = 0;\n    private behind: string = '';\n    private bridgeExplosions: string[] = [];\n    private chronoBeamColor: number[] = [];\n    private chronoBlast: string = '';\n    private chronoBlastDest: string = '';\n    private chronoPlacement: string = '';\n    private chronoSparkle1: string = '';\n    private conditionRed: number = 0;\n    private conditionYellow: number = 0;\n    private creditTicks: string[] = [];\n    private extraAircraftLight: number = 0;\n    private extraInfantryLight: number = 0;\n    private extraUnitLight: number = 0;\n    private fireNames: string[] = [];\n    private flyerHelper: string = '';\n    private gravity: number = 0;\n    private idleActionFrequency: number = 0;\n    private impactLandSound?: string;\n    private impactWaterSound?: string;\n    private infantryExplode: string = '';\n    private flamingInfantry: string = '';\n    private infantryHeadPop: string = '';\n    private infantryNuked: string = '';\n    private ironCurtainInvokeAnim: string = '';\n    private messageDuration: number = 10;\n    private metallicDebris: string[] = [];\n    private nukeTakeOff: string = '';\n    private deadBodies: string[] = [];\n    private wake: string = '';\n    private parachute: string = '';\n    private moveFlash: string = '';\n    private warpOut: string = '';\n    private warpAway: string = '';\n    private weaponNullifyAnim: string = '';\n    private weatherConClouds: string[] = [];\n    private weatherConBoltExplosion: string = '';\n    private weatherConBolts: string[] = [];\n    readIni(ini: any): AudioVisualRules {\n        this.ini = ini;\n        this.ambientChangeRate = ini.getNumber(\"AmbientChangeRate\");\n        this.ambientChangeStep = ini.getNumber(\"AmbientChangeStep\");\n        this.behind = ini.getString(\"Behind\");\n        this.bridgeExplosions = ini.getArray(\"BridgeExplosions\");\n        this.chronoBeamColor = ini.getNumberArray(\"ChronoBeamColor\");\n        this.chronoBlast = ini.getString(\"ChronoBlast\");\n        this.chronoBlastDest = ini.getString(\"ChronoBlastDest\");\n        this.chronoPlacement = ini.getString(\"ChronoPlacement\");\n        this.chronoSparkle1 = ini.getString(\"ChronoSparkle1\");\n        this.conditionRed = ini.getNumber(\"ConditionRed\");\n        this.conditionYellow = ini.getNumber(\"ConditionYellow\");\n        this.creditTicks = ini.getArray(\"CreditTicks\");\n        this.extraAircraftLight = ini.getNumber(\"ExtraAircraftLight\");\n        this.extraInfantryLight = ini.getNumber(\"ExtraInfantryLight\");\n        this.extraUnitLight = ini.getNumber(\"ExtraUnitLight\");\n        let damageFireTypes = ini.getString(\"DamageFireTypes\");\n        damageFireTypes = damageFireTypes || \"FIRE01,FIRE02,FIRE03\";\n        this.fireNames = damageFireTypes.split(/\\.|,/).filter((e) => e !== \"\");\n        this.flyerHelper = ini.getString(\"FlyerHelper\");\n        this.gravity = ini.getNumber(\"Gravity\");\n        this.idleActionFrequency = 60 * ini.getNumber(\"IdleActionFrequency\");\n        this.impactLandSound = ini.getString(\"ImpactLandSound\") || undefined;\n        this.impactWaterSound = ini.getString(\"ImpactWaterSound\") || undefined;\n        this.infantryExplode = ini.getString(\"InfantryExplode\");\n        this.flamingInfantry = ini.getString(\"FlamingInfantry\");\n        this.infantryHeadPop = ini.getString(\"InfantryHeadPop\");\n        this.infantryNuked = ini.getString(\"InfantryNuked\");\n        this.ironCurtainInvokeAnim = ini.getString(\"IronCurtainInvokeAnim\");\n        this.messageDuration = ini.getNumber(\"MessageDuration\", 10);\n        this.metallicDebris = ini.getArray(\"MetallicDebris\");\n        this.nukeTakeOff = ini.getString(\"NukeTakeOff\");\n        this.deadBodies = ini.getArray(\"DeadBodies\");\n        this.wake = ini.getString(\"Wake\");\n        this.parachute = ini.getString(\"Parachute\");\n        this.moveFlash = ini.getString(\"MoveFlash\");\n        this.warpOut = ini.getString(\"WarpOut\");\n        this.warpAway = ini.getString(\"WarpAway\");\n        this.weaponNullifyAnim = ini.getString(\"WeaponNullifyAnim\");\n        this.weatherConClouds = ini.getArray(\"WeatherConClouds\");\n        this.weatherConBoltExplosion = ini.getString(\"WeatherConBoltExplosion\");\n        this.weatherConBolts = ini.getArray(\"WeatherConBolts\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/CombatDamageRules.ts",
    "content": "export class CombatDamageRules {\n    private ballisticScatter: number = 0;\n    private bridgeStrength: number = 0;\n    private c4Delay: number = 0;\n    private c4Warhead: string = '';\n    private deathWeapon: string = '';\n    private dMislEliteWarhead: string = '';\n    private dMislWarhead: string = '';\n    private flameDamage: string = '';\n    private ironCurtainDuration: number = 0;\n    private ivanDamage: number = 0;\n    private ivanIconFlickerRate: number = 0;\n    private ivanTimedDelay: number = 0;\n    private ivanWarhead: string = '';\n    private splashList: string[] = [];\n    private v3EliteWarhead: string = '';\n    private v3Warhead: string = '';\n    readIni(ini: any): CombatDamageRules {\n        this.ballisticScatter = ini.getNumber(\"BallisticScatter\");\n        this.bridgeStrength = ini.getNumber(\"BridgeStrength\");\n        this.c4Delay = ini.getNumber(\"C4Delay\");\n        this.c4Warhead = ini.getString(\"C4Warhead\");\n        this.deathWeapon = ini.getString(\"DeathWeapon\");\n        this.dMislEliteWarhead = ini.getString(\"DMislEliteWarhead\");\n        this.dMislWarhead = ini.getString(\"DMislWarhead\");\n        this.flameDamage = ini.getString(\"FlameDamage\");\n        this.ironCurtainDuration = ini.getNumber(\"IronCurtainDuration\");\n        this.ivanDamage = ini.getNumber(\"IvanDamage\");\n        this.ivanIconFlickerRate = ini.getNumber(\"IvanIconFlickerRate\");\n        this.ivanTimedDelay = ini.getNumber(\"IvanTimedDelay\");\n        this.ivanWarhead = ini.getString(\"IvanWarhead\");\n        this.splashList = ini.getArray(\"SplashList\");\n        this.v3EliteWarhead = ini.getString(\"V3EliteWarhead\");\n        this.v3Warhead = ini.getString(\"V3Warhead\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/CountryRules.ts",
    "content": "import { SideType } from \"@/game/SideType\";\nconst sideMap = new Map<string, SideType>()\n    .set(\"GDI\", SideType.GDI)\n    .set(\"Nod\", SideType.Nod)\n    .set(\"Civilian\", SideType.Civilian)\n    .set(\"Mutant\", SideType.Mutant);\nconst tooltipMap = new Map<string, string>([\n    [\"Americans\", \"STT:PlayerSideAmerica\"],\n    [\"Alliance\", \"STT:PlayerSideKorea\"],\n    [\"French\", \"STT:PlayerSideFrance\"],\n    [\"Germans\", \"STT:PlayerSideGermany\"],\n    [\"British\", \"STT:PlayerSideBritain\"],\n    [\"Africans\", \"STT:PlayerSideLibya\"],\n    [\"Arabs\", \"STT:PlayerSideIraq\"],\n    [\"Confederation\", \"STT:PlayerSideCuba\"],\n    [\"Russians\", \"STT:PlayerSideRussia\"],\n]);\nexport class CountryRules {\n    private id: string;\n    public name!: string;\n    public uiName!: string;\n    private uiTooltip: string;\n    private side: SideType;\n    public multiplay: boolean;\n    private multiplayPassive: boolean;\n    private veteranAircraft: string[];\n    private veteranInfantry: string[];\n    private veteranUnits: string[];\n    constructor(id: string) {\n        this.id = id;\n    }\n    readIni(ini: any): CountryRules {\n        this.name = ini.name;\n        this.uiName = ini.getString(\"UIName\");\n        this.uiTooltip = ini.getString(\"UITooltip\") || tooltipMap.get(this.name);\n        const sideStr = ini.getString(\"Side\");\n        if (!sideStr) {\n            throw new Error(`Missing Side for country \"${this.name}\"`);\n        }\n        const side = sideMap.get(sideStr);\n        if (side === undefined) {\n            throw new Error(`Unknown side \"${sideStr}\" for country \"${this.name}\"`);\n        }\n        this.side = side;\n        this.multiplay = ini.getBool(\"Multiplay\");\n        this.multiplayPassive = ini.getBool(\"MultiplayPassive\");\n        this.veteranAircraft = ini.getArray(\"VeteranAircraft\");\n        this.veteranInfantry = ini.getArray(\"VeteranInfantry\");\n        this.veteranUnits = ini.getArray(\"VeteranUnits\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/CrateRules.ts",
    "content": "export class CrateRules {\n    private crateMaximum: number = 0;\n    private crateMinimum: number = 0;\n    private crateRadius: number = 0;\n    private crateRegen: number = 0;\n    private unitCrateType?: string;\n    private healCrateSound: string = '';\n    public crateImg: string = '';\n    public waterCrateImg: string = '';\n    private freeMCV: boolean = false;\n    readIni(ini: any): CrateRules {\n        this.crateMaximum = ini.getNumber(\"CrateMaximum\");\n        this.crateMinimum = ini.getNumber(\"CrateMinimum\");\n        this.crateRadius = ini.getNumber(\"CrateRadius\");\n        this.crateRegen = ini.getNumber(\"CrateRegen\");\n        const unitCrateType = ini.getString(\"UnitCrateType\");\n        this.unitCrateType = unitCrateType.toLowerCase() !== \"none\" ? unitCrateType : undefined;\n        this.healCrateSound = ini.getString(\"HealCrateSound\");\n        this.crateImg = ini.getString(\"CrateImg\");\n        this.waterCrateImg = ini.getString(\"WaterCrateImg\");\n        this.freeMCV = ini.getBool(\"FreeMCV\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/DebrisRules.ts",
    "content": "import { clamp } from \"@/util/math\";\nimport { ObjectRules } from \"./ObjectRules\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nexport class DebrisRules extends ObjectRules {\n    private damage: number = 0;\n    private damageRadius: number = 0;\n    private duration: number = 0;\n    private elasticity: number = 0.75;\n    private expireAnim?: string;\n    private minAngularVelocity: number = 0;\n    private maxAngularVelocity: number = 0;\n    private maxXYVel: number = 0;\n    private minZVel: number = 0;\n    private maxZVel: number = 0;\n    private shareTurretData: boolean = false;\n    private shareBodyData: boolean = false;\n    private shareBarrelData: boolean = false;\n    private shareSource?: string;\n    private trailerAnim?: string;\n    private trailerSeparation: number = 0;\n    private warhead?: string;\n    constructor(type: ObjectType, ini: any, index: number = -1, generalRules?: any) {\n        super(type, ini, index, generalRules);\n        this.parse();\n    }\n    protected parse(): void {\n        super.parse();\n        this.damage = this.ini.getNumber(\"Damage\");\n        this.damageRadius = this.ini.getNumber(\"DamageRadius\");\n        this.duration = this.ini.getNumber(\"Duration\");\n        this.elasticity = clamp(this.ini.getNumber(\"Elasticity\", 0.75), 0, 1);\n        this.expireAnim = this.ini.getString(\"ExpireAnim\") || undefined;\n        this.minAngularVelocity = this.ini.getNumber(\"MinAngularVelocity\");\n        this.maxAngularVelocity = this.ini.getNumber(\"MaxAngularVelocity\");\n        this.maxXYVel = this.ini.getNumber(\"MaxXYVel\");\n        this.minZVel = this.ini.getNumber(\"MinZVel\");\n        this.maxZVel = this.ini.getNumber(\"MaxZVel\");\n        this.shareTurretData = this.ini.getBool(\"ShareTurretData\");\n        this.shareBodyData = this.ini.getBool(\"ShareBodyData\");\n        this.shareBarrelData = this.ini.getBool(\"ShareBarrelData\");\n        this.shareSource = this.ini.getString(\"ShareSource\") || undefined;\n        this.trailerAnim = this.ini.getString(\"TrailerAnim\") || undefined;\n        this.trailerSeparation = this.ini.getNumber(\"TrailerSeperation\");\n        this.warhead = this.ini.getString(\"Warhead\") || undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/ElevationModelRules.ts",
    "content": "export class ElevationModelRules {\n    private increment: number = 0;\n    private incrementBonus: number = 1;\n    private bonusCap: number = 0;\n    readIni(ini: any): ElevationModelRules {\n        this.increment = ini.getNumber(\"ElevationIncrement\");\n        this.incrementBonus = ini.getNumber(\"ElevationIncrementBonus\", 1);\n        this.bonusCap = ini.getNumber(\"ElevationBonusCap\");\n        return this;\n    }\n    getBonus(elevation: number, targetElevation: number): number {\n        if (elevation <= targetElevation) {\n            return 0;\n        }\n        return Math.min(this.bonusCap, Math.floor((elevation - targetElevation) / this.increment)) * this.incrementBonus;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/GeneralRules.ts",
    "content": "import { RadarRules } from './general/RadarRules';\nimport { RepairRules } from './general/RepairRules';\nimport { VeteranRules } from './general/VeteranRules';\nimport { CrewRules } from './general/CrewRules';\nimport { PrismRules } from './general/PrismRules';\nimport { ThreatRules } from './general/ThreatRules';\nimport { ParadropRules } from './general/ParadropRules';\nimport { LightningStormRules } from './general/LightningStormRules';\nimport { V3RocketRules } from './general/V3RocketRules';\nimport { DMislRules } from './general/DMislRules';\nimport { HoverRules } from './general/HoverRules';\nimport { clamp } from '@/util/math';\nexport enum PrereqCategory {\n    Power = 0,\n    Factory = 1,\n    Barracks = 2,\n    Radar = 3,\n    Tech = 4,\n    Proc = 5\n}\ninterface IniReader {\n    getNumber(key: string, defaultValue?: number): number;\n    getString(key: string): string;\n    getArray(key: string): string[];\n    getFixed(key: string, defaultValue?: number): number;\n    getBool(key: string, defaultValue?: boolean): boolean;\n    has(key: string): boolean;\n}\ninterface MissileRules {\n    type: string;\n}\nconst prereqCategoryMap = new Map<PrereqCategory, string>([\n    [PrereqCategory.Power, 'PrerequisitePower'],\n    [PrereqCategory.Factory, 'PrerequisiteFactory'],\n    [PrereqCategory.Barracks, 'PrerequisiteBarracks'],\n    [PrereqCategory.Radar, 'PrerequisiteRadar'],\n    [PrereqCategory.Tech, 'PrerequisiteTech'],\n    [PrereqCategory.Proc, 'PrerequisiteProc']\n]);\nexport class GeneralRules {\n    public prereqCategories = new Map<PrereqCategory, string[]>();\n    public aircraftFogReveal!: number;\n    public flightLevel!: number;\n    public alliedDisguise!: string;\n    public sovietDisguise!: string;\n    public defaultMirageDisguises!: string[];\n    public cloakDelay!: number;\n    public infantryBlinkDisguiseTime!: number;\n    public baseUnit!: string[];\n    public buildSpeed!: number;\n    public buildupTime!: number;\n    public wallBuildSpeedCoefficient!: number;\n    public multipleFactory!: number;\n    public maximumQueuedObjects!: number;\n    public lowPowerPenaltyModifier!: number;\n    public minLowPowerProductionSpeed!: number;\n    public maxLowPowerProductionSpeed!: number;\n    public chronoDelay!: number;\n    public chronoDistanceFactor!: number;\n    public chronoHarvTooFarDistance!: number;\n    public chronoMinimumDelay!: number;\n    public chronoRangeMinimum!: number;\n    public chronoTrigger!: boolean;\n    public bridgeVoxelMax!: number;\n    public cliffBackImpassability!: number;\n    public closeEnough!: number;\n    public maxWaypointPathLength!: number;\n    public engineer!: string;\n    public engineerCaptureLevel!: number;\n    public engineerDamage!: number;\n    public engineerAlwaysCaptureTech!: boolean;\n    public technician!: string;\n    public harvesterTooFarDistance!: number;\n    public harvesterUnit!: string[];\n    public guardAreaTargetingDelay!: number;\n    public normalTargetingDelay!: number;\n    public revealTriggerRadius!: number;\n    public padAircraft!: string[];\n    public parachuteMaxFallRate!: number;\n    public dropPodWeapon!: string;\n    public refundPercent!: number;\n    public returnStructures!: boolean;\n    public unitsUnsellable!: boolean;\n    public purifierBonus!: number;\n    public maximumCheerRate!: number;\n    public spyMoneyStealPercent!: number;\n    public spyPowerBlackout!: number;\n    public shipSinkingWeight!: number;\n    public treeStrength!: number;\n    public crew!: CrewRules;\n    public dMisl!: DMislRules;\n    public hover!: HoverRules;\n    public lightningStorm!: LightningStormRules;\n    public paradrop!: ParadropRules;\n    public prism!: PrismRules;\n    public radar!: RadarRules;\n    public repair!: RepairRules;\n    public threat!: ThreatRules;\n    public v3Rocket!: V3RocketRules;\n    public veteran!: VeteranRules;\n    public readIni(ini: IniReader): void {\n        this.aircraftFogReveal = ini.getNumber('AircraftFogReveal');\n        this.alliedDisguise = ini.getString('AlliedDisguise');\n        this.baseUnit = ini.getArray('BaseUnit');\n        this.bridgeVoxelMax = ini.getNumber('BridgeVoxelMax');\n        this.buildSpeed = ini.getFixed('BuildSpeed');\n        this.buildupTime = ini.getNumber('BuildupTime');\n        this.chronoDelay = ini.getNumber('ChronoDelay');\n        this.chronoDistanceFactor = ini.getNumber('ChronoDistanceFactor', 32);\n        this.chronoHarvTooFarDistance = ini.getNumber('ChronoHarvTooFarDistance');\n        this.chronoMinimumDelay = ini.getNumber('ChronoMinimumDelay');\n        this.chronoRangeMinimum = ini.getNumber('ChronoRangeMinimum');\n        this.chronoTrigger = ini.getBool('ChronoTrigger', true);\n        this.cliffBackImpassability = ini.getNumber('CliffBackImpassability', 2);\n        this.cloakDelay = ini.getNumber('CloakDelay');\n        this.closeEnough = ini.getNumber('CloseEnough');\n        this.crew = new CrewRules().readIni(ini);\n        this.defaultMirageDisguises = ini.getArray('DefaultMirageDisguises');\n        this.dMisl = new DMislRules().readIni(ini);\n        this.dropPodWeapon = ini.getString('DropPodWeapon');\n        this.engineer = ini.getString('Engineer');\n        this.engineerCaptureLevel = ini.getFixed('EngineerCaptureLevel', 0.25);\n        this.engineerDamage = ini.getFixed('EngineerDamage', 0.437);\n        this.engineerAlwaysCaptureTech = ini.getBool('EngineerAlwaysCaptureTech', true);\n        this.flightLevel = ini.getNumber('FlightLevel');\n        this.guardAreaTargetingDelay = ini.getNumber('GuardAreaTargetingDelay');\n        this.harvesterTooFarDistance = ini.getNumber('HarvesterTooFarDistance');\n        this.harvesterUnit = ini.getArray('HarvesterUnit');\n        this.hover = new HoverRules().readIni(ini);\n        this.infantryBlinkDisguiseTime = ini.getNumber('InfantryBlinkDisguiseTime');\n        this.lightningStorm = new LightningStormRules().readIni(ini);\n        this.lowPowerPenaltyModifier = ini.getNumber('LowPowerPenaltyModifier', 1);\n        this.minLowPowerProductionSpeed = ini.getFixed('MinLowPowerProductionSpeed', 0.5);\n        this.maxLowPowerProductionSpeed = ini.getFixed('MaxLowPowerProductionSpeed', 1);\n        this.maximumCheerRate = ini.getNumber('MaximumCheerRate');\n        this.maximumQueuedObjects = ini.getNumber('MaximumQueuedObjects');\n        this.maxWaypointPathLength = ini.getNumber('MaxWaypointPathLength');\n        this.multipleFactory = ini.getFixed('MultipleFactory', 1);\n        this.normalTargetingDelay = ini.getNumber('NormalTargetingDelay');\n        this.padAircraft = ini.getArray('PadAircraft');\n        this.parachuteMaxFallRate = ini.getNumber('ParachuteMaxFallRate');\n        this.paradrop = new ParadropRules().readIni(ini);\n        this.prism = new PrismRules().readIni(ini);\n        this.purifierBonus = ini.getNumber('PurifierBonus');\n        this.radar = new RadarRules().readIni(ini);\n        this.refundPercent = clamp(ini.getNumber('RefundPercent'), 0, 1);\n        this.repair = new RepairRules().readIni(ini);\n        this.returnStructures = ini.getBool('ReturnStructures');\n        this.revealTriggerRadius = Math.min(10, ini.getNumber('RevealTriggerRadius'));\n        this.shipSinkingWeight = ini.getNumber('ShipSinkingWeight');\n        this.sovietDisguise = ini.getString('SovietDisguise');\n        this.spyMoneyStealPercent = ini.getNumber('SpyMoneyStealPercent');\n        this.spyPowerBlackout = ini.getNumber('SpyPowerBlackout');\n        this.technician = ini.getString('Technician');\n        this.threat = new ThreatRules().readIni(ini);\n        this.treeStrength = ini.getNumber('TreeStrength');\n        this.unitsUnsellable = ini.getBool('UnitsUnsellable');\n        this.v3Rocket = new V3RocketRules().readIni(ini);\n        this.veteran = new VeteranRules().readIni(ini);\n        this.wallBuildSpeedCoefficient = ini.getFixed('WallBuildSpeedCoefficient');\n        this.readPrereqCategories(ini);\n    }\n    private readPrereqCategories(ini: IniReader): void {\n        for (const [category, key] of prereqCategoryMap) {\n            if (!ini.has(key)) {\n                throw new Error(`Missing prerequisite category ${key} in [General] section`);\n            }\n            this.prereqCategories.set(category, ini.getArray(key));\n        }\n    }\n    public getMissileRules(type: string): MissileRules {\n        switch (type) {\n            case this.v3Rocket.type:\n                return this.v3Rocket;\n            case this.dMisl.type:\n                return this.dMisl;\n            default:\n                throw new Error(`Unsupported missile type \"${type}\"`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/rules/LandRules.ts",
    "content": "import { SpeedType } from \"@/game/type/SpeedType\";\nexport class LandRules {\n    private speedModifiers: Map<SpeedType, number>;\n    private buildable: boolean;\n    constructor() {\n        this.speedModifiers = new Map();\n    }\n    readIni(ini: any): this {\n        this.buildable = ini.getBool(\"Buildable\", false);\n        [...ini.entries.keys()].forEach((key) => {\n            if (SpeedType[key] !== undefined) {\n                this.speedModifiers.set(SpeedType[key as keyof typeof SpeedType], ini.getNumber(key));\n            }\n        });\n        return this;\n    }\n    getSpeedModifier(speedType: SpeedType): number {\n        if (speedType === SpeedType.Foot &&\n            this.speedModifiers.get(SpeedType.Track) === 0) {\n            return 0;\n        }\n        let modifier = this.speedModifiers.get(speedType);\n        if (modifier === undefined) {\n            modifier = 1;\n        }\n        if (speedType !== SpeedType.Track &&\n            speedType !== SpeedType.Wheel &&\n            modifier > 0) {\n            modifier = 1;\n        }\n        return modifier;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/MpDialogSettings.ts",
    "content": "import type { IniSection } from '../../data/IniSection';\nexport class MpDialogSettings {\n    public minMoney?: number;\n    public money?: number;\n    public maxMoney?: number;\n    public moneyIncrement?: number;\n    public minUnitCount?: number;\n    public unitCount?: number;\n    public maxUnitCount?: number;\n    public crates?: boolean;\n    public gameSpeed?: number;\n    public mcvRedeploys?: boolean;\n    public shortGame?: boolean;\n    public superWeapons?: boolean;\n    public techLevel?: number;\n    public alliesAllowed?: boolean;\n    public allyChangeAllowed?: boolean;\n    public mustAlly?: boolean;\n    public bridgeDestruction?: boolean;\n    public multiEngineer?: boolean;\n    private readOptionalNumber(section: IniSection, key: string): number | undefined {\n        return section.has(key) ? section.getNumber(key) : undefined;\n    }\n    private readOptionalBool(section: IniSection, key: string, invalidDefault: boolean = false): boolean | undefined {\n        return section.has(key) ? section.getBool(key, invalidDefault) : undefined;\n    }\n    readIni(section: IniSection): this {\n        this.minMoney = this.readOptionalNumber(section, \"MinMoney\");\n        this.money = this.readOptionalNumber(section, \"Money\");\n        this.maxMoney = this.readOptionalNumber(section, \"MaxMoney\");\n        this.moneyIncrement = this.readOptionalNumber(section, \"MoneyIncrement\");\n        this.minUnitCount = this.readOptionalNumber(section, \"MinUnitCount\");\n        this.unitCount = this.readOptionalNumber(section, \"UnitCount\");\n        this.maxUnitCount = this.readOptionalNumber(section, \"MaxUnitCount\");\n        this.crates = this.readOptionalBool(section, \"Crates\");\n        this.gameSpeed = this.readOptionalNumber(section, \"GameSpeed\");\n        this.mcvRedeploys = this.readOptionalBool(section, \"MCVRedeploys\");\n        this.shortGame = this.readOptionalBool(section, \"ShortGame\");\n        this.superWeapons = this.readOptionalBool(section, \"SuperWeapons\");\n        this.techLevel = this.readOptionalNumber(section, \"TechLevel\");\n        this.alliesAllowed = this.readOptionalBool(section, \"AlliesAllowed\", true);\n        this.allyChangeAllowed = this.readOptionalBool(section, \"AllyChangeAllowed\", true);\n        this.mustAlly = this.readOptionalBool(section, \"MustAlly\");\n        this.bridgeDestruction = this.readOptionalBool(section, \"BridgeDestruction\", true);\n        this.multiEngineer = this.readOptionalBool(section, \"MultiEngineer\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/ObjectRules.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nexport class ObjectRules {\n    static readonly IMAGE_NONE = \"none\";\n    public type: ObjectType;\n    protected ini: any;\n    public index: number;\n    protected generalRules: any;\n    private alphaImage?: string;\n    private alternateArcticArt: boolean = false;\n    private crushable: boolean = false;\n    private crushSound?: string;\n    private dontScore: boolean = false;\n    private insignificant: boolean = false;\n    private legalTarget: boolean = true;\n    private noShadow: boolean = false;\n    private uiName: string = \"\";\n    static iniSpeedToLeptonsPerTick(speed: number, frameRate: number): number {\n        return Math.min(256, (256 * speed) / frameRate);\n    }\n    static iniRotToDegsPerTick(rotation: number): number {\n        return (rotation / 256) * 360;\n    }\n    constructor(type: ObjectType, ini: any, index: number = -1, generalRules?: any) {\n        this.type = type;\n        this.ini = ini;\n        this.index = index;\n        this.generalRules = generalRules || {};\n        this.parse();\n    }\n    protected parse(): void {\n        this.alphaImage = this.ini.getString(\"AlphaImage\") || undefined;\n        this.alternateArcticArt = this.ini.getBool(\"AlternateArcticArt\");\n        this.crushable = this.ini.getBool(\"Crushable\", this.type === ObjectType.Infantry);\n        this.crushSound = this.ini.getString(\"CrushSound\") || undefined;\n        this.dontScore = this.ini.getBool(\"DontScore\");\n        this.insignificant = this.ini.getBool(\"Insignificant\");\n        this.legalTarget = this.ini.getBool(\"LegalTarget\", true);\n        this.noShadow = this.ini.getBool(\"NoShadow\");\n        this.uiName = this.ini.getString(\"UIName\");\n    }\n    get name(): string {\n        return this.ini.name;\n    }\n    get imageName(): string {\n        let image = this.ini.getString(\"Image\");\n        return (image && image !== \"null\") ? image : this.name;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/ObjectRulesFactory.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { ObjectRules } from './ObjectRules';\nimport { TechnoRules } from './TechnoRules';\nimport { OverlayRules } from './OverlayRules';\nimport { TerrainRules } from './TerrainRules';\nimport { SmudgeRules } from './SmudgeRules';\nimport { DebrisRules } from './DebrisRules';\nexport class ObjectRulesFactory {\n    create(type: ObjectType, ini: any, generalRules: any, index: number = -1) {\n        switch (type) {\n            case ObjectType.Aircraft:\n            case ObjectType.Building:\n            case ObjectType.Infantry:\n            case ObjectType.Vehicle:\n                return new TechnoRules(type, ini, index, generalRules);\n            case ObjectType.Overlay:\n                return new OverlayRules(type, ini, index, generalRules);\n            case ObjectType.Terrain:\n                return new TerrainRules(type, ini, index, generalRules);\n            case ObjectType.Smudge:\n                return new SmudgeRules(type, ini, index, generalRules);\n            case ObjectType.VoxelAnim:\n                return new DebrisRules(type, ini, index, generalRules);\n            default:\n                return new ObjectRules(type, ini, index, generalRules);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/rules/OverlayRules.ts",
    "content": "import { LandType } from '@/game/type/LandType';\nimport { ObjectRules } from '@/game/rules/ObjectRules';\nimport { ArmorType } from '@/game/type/ArmorType';\nimport { ObjectType } from '@/engine/type/ObjectType';\nexport class OverlayRules extends ObjectRules {\n    public armor!: ArmorType;\n    public crate!: boolean;\n    public isARock!: boolean;\n    public isRubble!: boolean;\n    public isVeinholeMonster!: boolean;\n    public isVeins!: boolean;\n    public land!: LandType;\n    public noUseTileLandType!: boolean;\n    public strength!: number;\n    public tiberium!: boolean;\n    public wall!: boolean;\n    public radarInvisible!: boolean;\n    constructor(type: ObjectType, ini: any, index: number = -1, generalRules?: any) {\n        super(type, ini, index, generalRules);\n        this.parse();\n    }\n    protected parse(): void {\n        super.parse();\n        this.armor = this.ini.getEnum(\"Armor\", ArmorType, ArmorType.None, true);\n        this.crate = this.ini.getBool(\"Crate\");\n        const isARock = this.ini.getBool(\"IsARock\");\n        this.isARock = isARock;\n        this.isRubble = this.ini.getBool(\"IsRubble\");\n        this.isVeinholeMonster = this.ini.getBool(\"IsVeinholeMonster\");\n        this.isVeins = this.ini.getBool(\"IsVeins\");\n        this.land = this.ini.getEnum(\"Land\", LandType, LandType.Clear);\n        this.noUseTileLandType = !!this.ini.getString(\"NoUseTileLandType\");\n        this.strength = this.ini.getNumber(\"Strength\");\n        this.tiberium = this.ini.getBool(\"Tiberium\");\n        const isWall = this.ini.getBool(\"Wall\");\n        this.wall = isWall;\n        this.radarInvisible = this.ini.getBool(\"RadarInvisible\", !isWall && !isARock);\n    }\n}\n"
  },
  {
    "path": "src/game/rules/PowerupsRules.ts",
    "content": "import { UNSUPPORTED_POWERUP_TYPES } from '../trait/CrateGeneratorTrait';\nimport { PowerupType } from '../type/PowerupType';\ninterface PowerupEntry {\n    type: PowerupType;\n    probShares: number;\n    animName?: string;\n    waterAllowed: boolean;\n    data?: string;\n}\nexport class PowerupsRules {\n    private powerups: PowerupEntry[] = [];\n    readIni(entries: Map<string, string>): this {\n        for (const [key, value] of entries) {\n            const [probShares, animName, waterAllowed, data] = value.split(',');\n            const shares = Number(probShares);\n            const type = PowerupType[key as keyof typeof PowerupType];\n            if (type !== undefined) {\n                if (!UNSUPPORTED_POWERUP_TYPES.includes(type)) {\n                    this.powerups.push({\n                        type,\n                        probShares: shares,\n                        animName: animName.toLowerCase() !== '<none>' ? animName : undefined,\n                        waterAllowed: waterAllowed === 'yes',\n                        data\n                    });\n                }\n            }\n            else {\n                console.warn(`Unknown powerup \"${key}\". Skipping.`);\n            }\n        }\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/ProjectileRules.ts",
    "content": "import { ObjectRules } from './ObjectRules';\nimport { ObjectType } from '@/engine/type/ObjectType';\nexport class ProjectileRules extends ObjectRules {\n    public acceleration!: number;\n    public arcing!: boolean;\n    public courseLockDuration!: number;\n    public detonationAltitude!: number;\n    public firersPalette!: boolean;\n    public flakScatter!: boolean;\n    public inaccurate!: boolean;\n    public inviso!: boolean;\n    public isAntiAir!: boolean;\n    public isAntiGround!: boolean;\n    public level!: boolean;\n    public rot!: number;\n    public iniRot!: number;\n    public shadow!: boolean;\n    public shrapnelWeapon?: string;\n    public shrapnelCount!: number;\n    public subjectToCliffs!: boolean;\n    public subjectToElevation!: boolean;\n    public subjectToWalls!: boolean;\n    public vertical!: boolean;\n    constructor(type: ObjectType, ini: any, index: number = -1, generalRules?: any) {\n        super(type, ini, index, generalRules);\n        this.parse();\n    }\n    protected parse(): void {\n        super.parse();\n        const rot = this.ini.getNumber(\"ROT\", 0);\n        let acceleration = this.ini.getNumber(\"Acceleration\");\n        if (rot === 1 && !acceleration) {\n            acceleration = Number.POSITIVE_INFINITY;\n        }\n        acceleration = acceleration || 3;\n        this.acceleration = acceleration;\n        this.arcing = this.ini.getBool(\"Arcing\");\n        this.courseLockDuration = this.ini.getNumber(\"CourseLockDuration\");\n        this.detonationAltitude = this.ini.getNumber(\"DetonationAltitude\");\n        this.firersPalette = this.ini.getBool(\"FirersPalette\");\n        this.flakScatter = this.ini.getBool(\"FlakScatter\");\n        this.inaccurate = this.ini.getBool(\"Inaccurate\");\n        this.inviso = this.ini.getBool(\"Inviso\");\n        this.isAntiAir = this.ini.getBool(\"AA\");\n        this.isAntiGround = this.ini.getBool(\"AG\", true);\n        this.level = this.ini.getBool(\"Level\");\n        this.rot = ObjectRules.iniRotToDegsPerTick(rot);\n        this.iniRot = rot;\n        this.shadow = this.ini.getBool(\"Shadow\", true);\n        this.shrapnelWeapon = this.ini.getString(\"ShrapnelWeapon\") || undefined;\n        this.shrapnelCount = this.ini.getNumber(\"ShrapnelCount\");\n        this.subjectToCliffs = this.ini.getBool(\"SubjectToCliffs\");\n        this.subjectToElevation = this.ini.getBool(\"SubjectToElevation\");\n        this.subjectToWalls = this.ini.getBool(\"SubjectToWalls\");\n        this.vertical = this.ini.getBool(\"Vertical\");\n    }\n}\n"
  },
  {
    "path": "src/game/rules/RadiationRules.ts",
    "content": "export class RadiationRules {\n    public radDurationMultiple!: number;\n    public radApplicationDelay!: number;\n    public radLevelMax!: number;\n    public radLevelDelay!: number;\n    public radLightDelay!: number;\n    public radLevelFactor!: number;\n    public radLightFactor!: number;\n    public radTintFactor!: number;\n    public radColor!: number[];\n    public radSiteWarhead!: string;\n    readIni(ini: any): this {\n        this.radDurationMultiple = ini.getNumber(\"RadDurationMultiple\");\n        this.radApplicationDelay = ini.getNumber(\"RadApplicationDelay\");\n        this.radLevelMax = ini.getNumber(\"RadLevelMax\");\n        this.radLevelDelay = ini.getNumber(\"RadLevelDelay\");\n        this.radLightDelay = ini.getNumber(\"RadLightDelay\");\n        this.radLevelFactor = ini.getNumber(\"RadLevelFactor\");\n        this.radLightFactor = ini.getNumber(\"RadLightFactor\");\n        this.radTintFactor = ini.getNumber(\"RadTintFactor\");\n        this.radColor = ini.getNumberArray(\"RadColor\");\n        this.radSiteWarhead = ini.getString(\"RadSiteWarhead\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/Rules.ts",
    "content": "import { Color } from \"@/util/Color\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { CountryRules } from \"@/game/rules/CountryRules\";\nimport { WeaponRules } from \"@/game/rules/WeaponRules\";\nimport { AudioVisualRules } from \"@/game/rules/AudioVisualRules\";\nimport { GeneralRules } from \"@/game/rules/GeneralRules\";\nimport { MpDialogSettings } from \"@/game/rules/MpDialogSettings\";\nimport { LandType } from \"@/game/type/LandType\";\nimport { LandRules } from \"@/game/rules/LandRules\";\nimport { WarheadRules } from \"@/game/rules/WarheadRules\";\nimport { ProjectileRules } from \"@/game/rules/ProjectileRules\";\nimport { ObjectRulesFactory } from \"@/game/rules/ObjectRulesFactory\";\nimport { CombatDamageRules } from \"@/game/rules/CombatDamageRules\";\nimport { TiberiumRules } from \"@/game/rules/TiberiumRules\";\nimport { AiRules } from \"@/game/rules/AiRules\";\nimport { ElevationModelRules } from \"@/game/rules/ElevationModelRules\";\nimport { RadiationRules } from \"@/game/rules/RadiationRules\";\nimport { SuperWeaponRules } from \"@/game/rules/SuperWeaponRules\";\nimport { CrateRules } from \"@/game/rules/CrateRules\";\nimport { PowerupsRules } from \"@/game/rules/PowerupsRules\";\nimport { mpAllowedColors } from \"@/game/rules/mpAllowedColors\";\nimport { isNotNullOrUndefined } from \"@/util/typeGuard\";\nimport { Weapon } from \"@/game/Weapon\";\ninterface IniFile {\n    getSection(name: string): IniSection | undefined;\n    getOrCreateSection(name: string): IniSection;\n}\ninterface IniSection {\n    entries: Map<string, any>;\n}\ninterface Logger {\n    debug(message: string): void;\n}\ninterface ObjectRules {\n    deathWeapon?: string;\n    primary?: string;\n    secondary?: string;\n    elitePrimary?: string;\n    eliteSecondary?: string;\n    occupyWeapon?: string;\n    eliteOccupyWeapon?: string;\n    weaponCount?: number;\n    getWeaponAtIndex?(index: number): string;\n    getEliteWeaponAtIndex?(index: number): string;\n}\ninterface SpecialFlags {\n    initialVeteran?: boolean;\n}\nexport class Rules {\n    private ini: IniFile;\n    private logger?: Logger;\n    private buildingTypes = new Map<number, string>();\n    private vehicleTypes = new Map<number, string>();\n    private infantryTypes = new Map<number, string>();\n    private aircraftTypes = new Map<number, string>();\n    private terrainTypes = new Map<number, string>();\n    private overlayTypes = new Map<number, string>();\n    private overlayIdsByType = new Map<string, number>();\n    private animationTypes = new Map<number, string>();\n    private animationNames = new Set<string>();\n    private voxelAnimTypes = new Map<number, string>();\n    private smudgeTypes = new Map<number, string>();\n    private warheadTypes = new Map<number, string>();\n    private tiberiumTypes = new Map<number, string>();\n    private superWeaponTypes = new Map<number, string>();\n    private countryTypes = new Map<number, string>();\n    readonly weaponTypes = new Map<number, string>();\n    private allObjectRules = new Map<ObjectType, Map<string, ObjectRules>>();\n    readonly buildingRules = new Map<string, ObjectRules>();\n    readonly infantryRules = new Map<string, ObjectRules>();\n    readonly vehicleRules = new Map<string, ObjectRules>();\n    readonly aircraftRules = new Map<string, ObjectRules>();\n    readonly terrainRules = new Map<string, ObjectRules>();\n    readonly overlayRules = new Map<string, ObjectRules>();\n    private smudgeRules = new Map<string, ObjectRules>();\n    private voxelAnimRules = new Map<string, ObjectRules>();\n    private countryRules = new Map<string, CountryRules>();\n    readonly warheadRules = new Map<string, WarheadRules>();\n    public powerups = new PowerupsRules();\n    public colors = new Map<string, Color>();\n    public general = new GeneralRules();\n    public ai = new AiRules();\n    public crateRules = new CrateRules();\n    public elevationModel = new ElevationModelRules();\n    public mpDialogSettings = new MpDialogSettings();\n    public audioVisual = new AudioVisualRules();\n    public combatDamage = new CombatDamageRules();\n    public radiation = new RadiationRules();\n    private landRules = new Map<LandType, LandRules>();\n    private tiberiumRules = new Map<string, TiberiumRules>();\n    private superWeaponRules = new Map<string, SuperWeaponRules>();\n    private cachedWeaponRules = new Map<string, WeaponRules>();\n    private cachedProjectileRules = new Map<string, ProjectileRules>();\n    constructor(ini: IniFile, logger?: Logger) {\n        this.ini = ini;\n        this.logger = logger;\n        this.init();\n    }\n    hasObject(name: string, type: ObjectType): boolean {\n        return this.allObjectRules.get(type)?.has(name) ?? false;\n    }\n    getObject(name: string, type: ObjectType): ObjectRules {\n        const rules = this.allObjectRules.get(type)?.get(name);\n        if (!rules) {\n            throw new Error(`Missing rules for object \"${name}\"`);\n        }\n        return rules;\n    }\n    getTechnoByInternalId(id: number, type: ObjectType): ObjectRules {\n        let typeName: string | undefined;\n        if (type === ObjectType.Building) {\n            typeName = this.buildingTypes.get(id);\n        }\n        else if (type === ObjectType.Infantry) {\n            typeName = this.infantryTypes.get(id);\n        }\n        else if (type === ObjectType.Vehicle) {\n            typeName = this.vehicleTypes.get(id);\n        }\n        else if (type === ObjectType.Aircraft) {\n            typeName = this.aircraftTypes.get(id);\n        }\n        else {\n            throw new Error(`Type ${ObjectType[type]} is not a techno type`);\n        }\n        if (typeName === undefined) {\n            throw new Error(`Object type \"${ObjectType[type]}\" with ID \"${id}\" not found`);\n        }\n        return this.getObject(typeName, type);\n    }\n    getBuilding(name: string): ObjectRules {\n        const rules = this.buildingRules.get(name);\n        if (!rules) {\n            throw new Error(`Missing rules for building \"${name}\"`);\n        }\n        return rules;\n    }\n    getWeapon(name: string): WeaponRules {\n        let rules = this.cachedWeaponRules.get(name);\n        if (!rules) {\n            const section = this.ini.getSection(name);\n            if (!section) {\n                throw new Error(`Weapon ${name} is missing ini section`);\n            }\n            rules = new WeaponRules(section);\n            this.cachedWeaponRules.set(name, rules);\n        }\n        return rules;\n    }\n    getWeaponByInternalId(id: number): WeaponRules {\n        const weaponName = this.weaponTypes.get(id);\n        if (!weaponName) {\n            throw new RangeError(`Weapon with internal ID \"${id}\" not found`);\n        }\n        return this.getWeapon(weaponName);\n    }\n    getWarhead(name: string): WarheadRules {\n        let rules = this.warheadRules.get(name.toLowerCase());\n        if (!rules) {\n            const section = this.ini.getSection(name);\n            if (section) {\n                rules = new WarheadRules(section);\n                this.warheadRules.set(name.toLowerCase(), rules);\n            }\n        }\n        if (!rules) {\n            try {\n                const known = Array.from(this.warheadRules.keys());\n                const sample = known.slice(0, 20).join(\", \");\n                const hasSection = !!this.ini.getSection(name);\n                console.error(`[Diag] Unknown warhead \"${name}\". hasSection=${hasSection}. Known warheads (sample): ${sample} ... total=${known.length}`);\n            }\n            catch (e) {\n                console.error('[Diag] Unknown warhead diagnostics failed:', e);\n            }\n            throw new Error(\"Unknown warhead \" + name);\n        }\n        return rules;\n    }\n    getProjectile(name: string): ProjectileRules {\n        let rules = this.cachedProjectileRules.get(name);\n        if (!rules) {\n            const section = this.ini.getSection(name);\n            if (!section) {\n                throw new Error(`Projectile ${name} is missing ini section`);\n            }\n            rules = new ProjectileRules(ObjectType.Projectile, section);\n            this.cachedProjectileRules.set(name, rules);\n        }\n        return rules;\n    }\n    getOverlayName(id: number): string {\n        const name = this.overlayTypes.get(id);\n        if (!name) {\n            throw new Error(\"Invalid overlay id \" + id);\n        }\n        return name;\n    }\n    hasOverlayId(id: number): boolean {\n        return this.overlayTypes.has(id);\n    }\n    getOverlayId(name: string): number {\n        const id = this.overlayIdsByType.get(name);\n        if (id === undefined) {\n            throw new Error(\"Invalid overlay name \" + name);\n        }\n        return id;\n    }\n    getOverlay(name: string): ObjectRules {\n        const rules = this.overlayRules.get(name);\n        if (!rules) {\n            throw new Error(`Missing rules for overlay \"${name}\"`);\n        }\n        return rules;\n    }\n    getAnimationName(id: number): string | undefined {\n        return this.animationTypes.get(id);\n    }\n    getCountry(name: string): CountryRules {\n        if (!this.countryRules.has(name)) {\n            throw new Error(\"Unknown country \" + name);\n        }\n        return this.countryRules.get(name)!;\n    }\n    getMultiplayerCountries(): CountryRules[] {\n        return [...this.countryRules.values()].filter(country => country.multiplay);\n    }\n    getMultiplayerColors(): Map<string, Color> {\n        const colors = new Map<string, Color>();\n        mpAllowedColors.forEach(colorName => {\n            if (!this.colors.has(colorName)) {\n                throw new Error(`Multiplayer color \"${colorName}\" does not exist in the rules [Colors] section.`);\n            }\n            colors.set(colorName, this.colors.get(colorName)!);\n        });\n        return colors;\n    }\n    getLandRules(landType: LandType): LandRules {\n        let rules = this.landRules.get(landType);\n        if (!rules) {\n            const sectionName = landType === LandType.Cliff ? \"Rock\" : LandType[landType];\n            rules = new LandRules().readIni(this.ini.getOrCreateSection(sectionName));\n            this.landRules.set(landType, rules);\n        }\n        return rules;\n    }\n    getTiberium(id: number): TiberiumRules {\n        const typeName = this.tiberiumTypes.get(id);\n        if (!typeName) {\n            throw new Error(\"Unknown tiberium type \" + id);\n        }\n        return this.tiberiumRules.get(typeName)!;\n    }\n    getSuperWeapon(name: string): SuperWeaponRules {\n        if (!this.superWeaponRules.has(name)) {\n            throw new Error(`Unknown superweapon type \"${name}\"`);\n        }\n        return this.superWeaponRules.get(name)!;\n    }\n    getIni(): IniFile {\n        return this.ini;\n    }\n    applySpecialFlags(flags: SpecialFlags): void {\n        if (flags.initialVeteran) {\n            this.general.veteran.initialVeteran = true;\n        }\n    }\n    private init(): void {\n        this.readAudioVisual();\n        this.readCombatDamage();\n        this.readRadiation();\n        this.readGeneral();\n        this.readAi();\n        this.readCrateRules();\n        this.readElevationModel();\n        this.readMpDialogSettings();\n        this.readObjectTypes(\"BuildingTypes\", this.buildingTypes);\n        this.readObjectTypes(\"InfantryTypes\", this.infantryTypes);\n        this.readObjectTypes(\"VehicleTypes\", this.vehicleTypes);\n        this.readObjectTypes(\"AircraftTypes\", this.aircraftTypes);\n        this.readObjectTypes(\"TerrainTypes\", this.terrainTypes);\n        this.readObjectTypes(\"SmudgeTypes\", this.smudgeTypes);\n        this.readObjectTypes(\"Animations\", this.animationTypes);\n        this.animationNames = new Set(this.animationTypes.values());\n        this.readObjectTypes(\"VoxelAnims\", this.voxelAnimTypes);\n        this.readObjectTypes(\"OverlayTypes\", this.overlayTypes);\n        this.overlayTypes.forEach((name, id) => this.overlayIdsByType.set(name, id));\n        this.readColors();\n        this.readObjectTypes(\"Countries\", this.countryTypes);\n        this.readObjectTypes(\"Warheads\", this.warheadTypes);\n        this.readObjectTypes(\"Tiberiums\", this.tiberiumTypes);\n        this.readObjectTypes(\"SuperWeaponTypes\", this.superWeaponTypes);\n        this.allObjectRules\n            .set(ObjectType.Building, this.buildingRules)\n            .set(ObjectType.Infantry, this.infantryRules)\n            .set(ObjectType.Vehicle, this.vehicleRules)\n            .set(ObjectType.Aircraft, this.aircraftRules)\n            .set(ObjectType.Terrain, this.terrainRules)\n            .set(ObjectType.Overlay, this.overlayRules)\n            .set(ObjectType.Smudge, this.smudgeRules)\n            .set(ObjectType.VoxelAnim, this.voxelAnimRules);\n        this.readObjects(ObjectType.Building, this.buildingTypes, this.buildingRules);\n        this.readObjects(ObjectType.Infantry, this.infantryTypes, this.infantryRules);\n        this.readObjects(ObjectType.Vehicle, this.vehicleTypes, this.vehicleRules);\n        this.readObjects(ObjectType.Aircraft, this.aircraftTypes, this.aircraftRules);\n        this.readObjects(ObjectType.Terrain, this.terrainTypes, this.terrainRules);\n        this.readObjects(ObjectType.Overlay, this.overlayTypes, this.overlayRules);\n        this.readObjects(ObjectType.Smudge, this.smudgeTypes, this.smudgeRules);\n        this.readObjects(ObjectType.VoxelAnim, this.voxelAnimTypes, this.voxelAnimRules);\n        this.readCountries();\n        this.readWarheads();\n        this.readPowerups();\n        this.readTiberiums();\n        this.readSuperWeapons();\n        this.buildWeaponsList();\n    }\n    private readAudioVisual(): void {\n        const section = this.ini.getSection(\"AudioVisual\");\n        if (!section) {\n            throw new Error(\"Missing [AudioVisual] section\");\n        }\n        this.audioVisual.readIni(section);\n    }\n    private readCombatDamage(): void {\n        const section = this.ini.getSection(\"CombatDamage\");\n        if (!section) {\n            throw new Error(\"Missing [CombatDamage] section\");\n        }\n        this.combatDamage.readIni(section);\n    }\n    private readRadiation(): void {\n        const section = this.ini.getSection(\"Radiation\");\n        if (!section) {\n            throw new Error(\"Missing [Radiation] section\");\n        }\n        this.radiation.readIni(section);\n    }\n    private readGeneral(): void {\n        const section = this.ini.getSection(\"General\");\n        if (!section) {\n            throw new Error(\"Missing [General] section\");\n        }\n        this.general.readIni(section as any);\n    }\n    private readAi(): void {\n        const section = this.ini.getSection(\"AI\");\n        if (!section) {\n            throw new Error(\"Missing [AI] section\");\n        }\n        this.ai.readIni(section);\n    }\n    private readCrateRules(): void {\n        const section = this.ini.getSection(\"CrateRules\");\n        if (!section) {\n            throw new Error(\"Missing [CrateRules] section\");\n        }\n        this.crateRules.readIni(section);\n    }\n    private readElevationModel(): void {\n        const section = this.ini.getSection(\"ElevationModel\");\n        if (!section) {\n            throw new Error(\"Missing [ElevationModel] section\");\n        }\n        this.elevationModel.readIni(section);\n    }\n    private readMpDialogSettings(): void {\n        const section = this.ini.getSection(\"MultiplayerDialogSettings\");\n        if (!section) {\n            throw new Error(\"Missing [MultiplayerDialogSettings] section\");\n        }\n        this.mpDialogSettings.readIni(section as any);\n    }\n    private readObjectTypes(sectionName: string, typeMap: Map<number, string>): void {\n        const section = this.ini.getSection(sectionName);\n        if (!section) {\n            throw new Error(`Missing [${sectionName}] section`);\n        }\n        let index = 0;\n        const seenTypes = new Set<string>();\n        section.entries.forEach((value, key) => {\n            if (typeof value === \"string\") {\n                if (Number.isNaN(Number(key))) {\n                    this.logger?.debug(`Non-numeric id \"${key}\" found in rules section [${sectionName}]. Skipping.`);\n                }\n                else if (seenTypes.has(value)) {\n                    this.logger?.debug(`Duplicate type \"${value}\" in rules section [${sectionName}]. Skipping.`);\n                }\n                else {\n                    typeMap.set(index++, value);\n                    seenTypes.add(value);\n                }\n            }\n            else {\n                this.logger?.debug(`Non-string type found in rules section [${sectionName}]. Skipping.`);\n            }\n        });\n    }\n    private readColors(): void {\n        const section = this.ini.getSection(\"Colors\");\n        if (!section) {\n            throw new Error(\"Missing [Colors] section\");\n        }\n        section.entries.forEach((value, name) => {\n            const [h, s, v] = (value as string).split(\",\");\n            const color = Color.fromHsv(parseInt(h, 10), parseInt(s, 10), parseInt(v, 10));\n            this.colors.set(name, color);\n        });\n    }\n    private readObjects(objectType: ObjectType, typeMap: Map<number, string>, rulesMap: Map<string, ObjectRules>): void {\n        typeMap.forEach((typeName, id) => {\n            const section = this.ini.getSection(typeName);\n            if (section) {\n                const rules = new ObjectRulesFactory().create(objectType, section, this.general, id as any);\n                rulesMap.set(typeName, rules as any);\n            }\n            else {\n                this.logger?.debug(`${ObjectType[objectType]} type \"${typeName}\" has no rules section`);\n            }\n        });\n    }\n    private readCountries(): void {\n        this.countryTypes.forEach((name, id) => {\n            const section = this.ini.getSection(name);\n            if (!section) {\n                throw new Error(\"Missing ini section for country \" + name);\n            }\n            const rules = new CountryRules(id as any);\n            rules.readIni(section as any);\n            this.countryRules.set(name, rules);\n        });\n    }\n    private readWarheads(): void {\n        this.warheadTypes.forEach(name => {\n            const section = this.ini.getSection(name);\n            if (section) {\n                const rules = new WarheadRules(section);\n                this.warheadRules.set(name.toLowerCase(), rules);\n            }\n            else {\n                this.logger?.debug(`Warhead \"${name}\" has no rules section`);\n            }\n        });\n    }\n    private readPowerups(): void {\n        const section = this.ini.getSection(\"Powerups\");\n        if (!section) {\n            throw new Error(\"Missing [Powerups] section\");\n        }\n        this.powerups.readIni(section.entries);\n    }\n    private readTiberiums(): void {\n        this.tiberiumTypes.forEach(name => {\n            const section = this.ini.getSection(name);\n            if (!section) {\n                throw new Error(\"Missing rules section for tiberium type \" + name);\n            }\n            this.tiberiumRules.set(name, new TiberiumRules().readIni(section));\n        });\n    }\n    private readSuperWeapons(): void {\n        this.superWeaponTypes.forEach((name, id) => {\n            const section = this.ini.getSection(name);\n            if (!section) {\n                throw new Error(\"Missing rules section for superweapon type \" + name);\n            }\n            this.superWeaponRules.set(name, new SuperWeaponRules(id).readIni(section));\n        });\n    }\n    private buildWeaponsList(): void {\n        const weaponNames = new Set<string>();\n        weaponNames.add(this.general.dropPodWeapon);\n        for (const superWeapon of this.superWeaponRules.values()) {\n            if (superWeapon.weaponType) {\n                weaponNames.add(superWeapon.weaponType);\n            }\n        }\n        weaponNames.add(Weapon.NUKE_PAYLOAD_NAME);\n        const allObjectRules = [\n            ...this.buildingRules.values(),\n            ...this.aircraftRules.values(),\n            ...this.vehicleRules.values(),\n            ...this.infantryRules.values(),\n        ];\n        for (const rules of allObjectRules) {\n            const weapons = [\n                rules.deathWeapon,\n                rules.primary,\n                rules.secondary,\n                rules.elitePrimary,\n                rules.eliteSecondary,\n                rules.occupyWeapon,\n                rules.eliteOccupyWeapon,\n                ...(rules.weaponCount\n                    ? new Array(rules.weaponCount)\n                        .fill(0)\n                        .map((_, index) => [\n                        rules.getWeaponAtIndex?.(index),\n                        rules.getEliteWeaponAtIndex?.(index),\n                    ])\n                        .flat()\n                    : []),\n            ]\n                .filter(isNotNullOrUndefined)\n                .filter(weapon => weapon !== \"\");\n            for (const weapon of weapons) {\n                weaponNames.add(weapon);\n            }\n        }\n        let weaponIndex = 0;\n        for (const weaponName of weaponNames) {\n            this.weaponTypes.set(weaponIndex++, weaponName);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/rules/SmudgeRules.ts",
    "content": "import { ObjectRules } from './ObjectRules';\nimport { ObjectType } from '@/engine/type/ObjectType';\nexport class SmudgeRules extends ObjectRules {\n    public burn!: boolean;\n    public crater!: boolean;\n    public width!: number;\n    public height!: number;\n    constructor(type: ObjectType, ini: any, index: number = -1, generalRules?: any) {\n        super(type, ini, index, generalRules);\n        this.parse();\n    }\n    protected parse(): void {\n        super.parse();\n        this.burn = this.ini.getBool(\"Burn\");\n        this.crater = this.ini.getBool(\"Crater\");\n        this.width = this.ini.getNumber(\"Width\", 1);\n        this.height = this.ini.getNumber(\"Height\", 1);\n    }\n}\n"
  },
  {
    "path": "src/game/rules/SuperWeaponRules.ts",
    "content": "import { SuperWeaponType } from '../type/SuperWeaponType';\nexport class SuperWeaponRules {\n    public index: number;\n    public disableableFromShell: boolean;\n    public isPowered: boolean;\n    public name: string;\n    public preClick: boolean;\n    public preDependent?: SuperWeaponType;\n    public postClick: boolean;\n    public rechargeTime: number;\n    public showTimer: boolean;\n    public sidebarImage: string;\n    public type?: SuperWeaponType;\n    public uiName: string;\n    public weaponType?: string;\n    constructor(index: number) {\n        this.index = index;\n    }\n    readIni(ini: any): this {\n        this.disableableFromShell = ini.getBool(\"DisableableFromShell\");\n        this.isPowered = ini.getBool(\"IsPowered\", true);\n        this.name = ini.name;\n        this.preClick = ini.getBool(\"PreClick\");\n        this.preDependent = ini.getEnum(\"PreDependent\", SuperWeaponType, undefined);\n        this.postClick = ini.getBool(\"PostClick\");\n        this.rechargeTime = ini.getNumber(\"RechargeTime\", 5);\n        this.showTimer = ini.getBool(\"ShowTimer\");\n        this.sidebarImage = ini.getString(\"SidebarImage\").toLowerCase();\n        this.type = ini.getEnum(\"Type\", SuperWeaponType, undefined);\n        this.uiName = ini.getString(\"UIName\");\n        this.weaponType = ini.getString(\"WeaponType\") || undefined;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/TechnoRules.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { SideType } from \"@/game/SideType\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { PipColor } from \"@/game/type/PipColor\";\nimport { LocomotorType } from \"@/game/type/LocomotorType\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { ArmorType } from \"@/game/type/ArmorType\";\nimport { LandTargeting } from \"@/game/type/LandTargeting\";\nimport { NavalTargeting } from \"@/game/type/NavalTargeting\";\nimport { ObjectRules } from \"@/game/rules/ObjectRules\";\nimport { WeaponType } from \"@/game/WeaponType\";\nimport { VeteranAbility } from \"@/game/gameobject/unit/VeteranAbility\";\nimport { VhpScan } from \"@/game/type/VhpScan\";\nimport { Vector3 } from \"@/game/math/Vector3\";\ninterface House {\n    name: string;\n}\nexport enum BuildCat {\n    Combat = 0,\n    Tech = 1,\n    Resource = 2,\n    Power = 3\n}\nexport enum FactoryType {\n    None = 0,\n    BuildingType = 1,\n    InfantryType = 2,\n    UnitType = 3,\n    NavalUnitType = 4,\n    AircraftType = 5\n}\nexport class TechnoRules extends ObjectRules {\n    static readonly MAX_SIGHT = 11;\n    declare owner: string[];\n    declare aiBasePlanningSide?: number;\n    declare requiredHouses: string[];\n    declare forbiddenHouses: string[];\n    declare requiresStolenAlliedTech: boolean;\n    declare requiresStolenSovietTech: boolean;\n    declare techLevel: number;\n    declare cost: number;\n    declare points: number;\n    declare power: number;\n    declare powered: boolean;\n    declare prerequisite: string[];\n    declare soylent: number;\n    declare crateGoodie: boolean;\n    declare buildCat: BuildCat;\n    declare adjacent: number;\n    declare baseNormal: boolean;\n    declare buildLimit: number;\n    declare airRangeBonus: number;\n    declare guardRange: number;\n    declare defaultToGuardArea: boolean;\n    declare eligibileForAllyBuilding: boolean;\n    declare numberImpassableRows: number;\n    declare bridgeRepairHut: boolean;\n    declare constructionYard: boolean;\n    declare refinery: boolean;\n    declare unitRepair: boolean;\n    declare unitReload: boolean;\n    declare unitSell: boolean;\n    declare isBaseDefense: boolean;\n    declare superWeapon?: string;\n    declare chargedAnimTime: number;\n    declare naval: boolean;\n    declare underwater: boolean;\n    declare waterBound: boolean;\n    declare orePurifier: boolean;\n    declare cloning: boolean;\n    declare grinding: boolean;\n    declare nukeSilo: boolean;\n    declare repairable: boolean;\n    declare clickRepairable: boolean;\n    declare unsellable: boolean;\n    declare returnable: boolean;\n    declare gdiBarracks: boolean;\n    declare nodBarracks: boolean;\n    declare numberOfDocks: number;\n    declare factory: FactoryType;\n    declare weaponsFactory: boolean;\n    declare helipad: boolean;\n    declare hospital: boolean;\n    declare landTargeting: LandTargeting;\n    declare navalTargeting: NavalTargeting;\n    declare tooBigToFitUnderBridge: boolean;\n    declare canBeOccupied: boolean;\n    declare maxNumberOccupants: number;\n    declare leaveRubble: boolean;\n    declare undeploysInto: string;\n    declare deploysInto: string;\n    declare deployTime: number;\n    declare capturable: boolean;\n    declare spyable: boolean;\n    declare needsEngineer: boolean;\n    declare c4: boolean;\n    declare canC4: boolean;\n    declare eligibleForDelayKill: boolean;\n    declare produceCashStartup: number;\n    declare produceCashAmount: number;\n    declare produceCashDelay: number;\n    declare explosion: string[];\n    declare explodes: boolean;\n    declare ifvMode: number;\n    declare turretIndexesByIfvMode: Map<number, number>;\n    declare turret: boolean;\n    declare turretCount: number;\n    declare turretAnim: string;\n    declare turretAnimIsVoxel: boolean;\n    declare turretAnimX: number;\n    declare turretAnimY: number;\n    declare turretAnimZAdjust: number;\n    declare isChargeTurret: boolean;\n    declare overpowerable: boolean;\n    declare freeUnit: string;\n    declare primary?: string;\n    declare secondary?: string;\n    declare elitePrimary?: string;\n    declare eliteSecondary?: string;\n    declare weaponCount: number;\n    declare deathWeapon?: string;\n    declare deathWeaponDamageModifier: number;\n    declare occupyWeapon?: string;\n    declare eliteOccupyWeapon?: string;\n    declare veteranAbilities: Set<VeteranAbility>;\n    declare eliteAbilities: Set<VeteranAbility>;\n    declare selfHealing: boolean;\n    declare wall: boolean;\n    declare gate: boolean;\n    declare armor: ArmorType;\n    declare strength: number;\n    declare immune: boolean;\n    declare immuneToRadiation: boolean;\n    declare immuneToPsionics: boolean;\n    declare typeImmune: boolean;\n    declare warpable: boolean;\n    declare isTilter: boolean;\n    declare walkRate: number;\n    declare idleRate: number;\n    declare noSpawnAlt: boolean;\n    declare crusher: boolean;\n    declare consideredAircraft: boolean;\n    declare crashable: boolean;\n    declare landable: boolean;\n    declare airportBound: boolean;\n    declare balloonHover: boolean;\n    declare hoverAttack: boolean;\n    declare omniFire: boolean;\n    declare fighter: boolean;\n    declare flightLevel?: number;\n    declare locomotor: LocomotorType;\n    declare speedType?: SpeedType;\n    declare speed: number;\n    declare movementZone: MovementZone;\n    declare fearless: boolean;\n    declare deployer: boolean;\n    declare deployFire: boolean;\n    declare deployFireWeapon: number;\n    declare undeployDelay: number;\n    declare fraidycat: boolean;\n    declare isHuman: boolean;\n    declare organic: boolean;\n    declare occupier: boolean;\n    declare engineer: boolean;\n    declare ivan: boolean;\n    declare civilian: boolean;\n    declare agent: boolean;\n    declare infiltrate: boolean;\n    declare threatPosed: number;\n    declare specialThreatValue: number;\n    declare canPassiveAquire: boolean;\n    declare canRetaliate: boolean;\n    declare preventAttackMove: boolean;\n    declare opportunityFire: boolean;\n    declare distributedFire: boolean;\n    declare radialFireSegments: number;\n    declare attackCursorOnFriendlies: boolean;\n    declare bombable: boolean;\n    declare trainable: boolean;\n    declare crewed: boolean;\n    declare parasiteable: boolean;\n    declare suppressionThreshold: number;\n    declare reselectIfLimboed: boolean;\n    declare rejoinTeamIfLimboed: boolean;\n    declare weight: number;\n    declare accelerates: boolean;\n    declare accelerationFactor: number;\n    declare teleporter: boolean;\n    declare canDisguise: boolean;\n    declare disguiseWhenStill: boolean;\n    declare permaDisguise: boolean;\n    declare detectDisguise: boolean;\n    declare detectDisguiseRange: number;\n    declare cloakable: boolean;\n    declare sensors: boolean;\n    declare sensorArray: boolean;\n    declare sensorsSight: number;\n    declare burstDelay: (number | undefined)[];\n    declare vhpScan: VhpScan;\n    declare pip: PipColor;\n    declare passengers: number;\n    declare gunner: boolean;\n    declare ammo: number;\n    declare initialAmmo: number;\n    declare manualReload: boolean;\n    declare storage: number;\n    declare spawned: boolean;\n    declare spawns: string;\n    declare spawnsNumber: number;\n    declare spawnRegenRate: number;\n    declare spawnReloadRate: number;\n    declare missileSpawn: boolean;\n    declare size: number;\n    declare sizeLimit: number;\n    declare sight: number;\n    declare spySat: boolean;\n    declare gapGenerator: boolean;\n    declare gapRadiusInCells: number;\n    declare psychicDetectionRadius: number;\n    declare hasRadialIndicator: boolean;\n    declare harvester: boolean;\n    declare unloadingClass: string;\n    declare dock: string[];\n    declare radar: boolean;\n    declare radarInvisible: boolean;\n    declare revealToAll: boolean;\n    declare selectable: boolean;\n    declare isSelectableCombatant: boolean;\n    declare invisibleInGame: boolean;\n    declare moveToShroud: boolean;\n    declare leadershipRating: number;\n    declare unnatural: boolean;\n    declare natural: boolean;\n    declare buildTimeMultiplier: number;\n    declare allowedToStartInMultiplayer: boolean;\n    declare rot: number;\n    declare jumpjetAccel: number;\n    declare jumpjetClimb: number;\n    declare jumpjetCrash: number;\n    declare jumpjetDeviation: number;\n    declare jumpjetHeight: number;\n    declare jumpjetNoWobbles: boolean;\n    declare jumpjetSpeed: number;\n    declare jumpjetTurnRate: number;\n    declare jumpjetWobbles: number;\n    declare pitchSpeed: number;\n    declare pitchAngle: number;\n    declare damageParticleSystems: string[];\n    declare damageSmokeOffset: Vector3;\n    declare minDebris: number;\n    declare maxDebris: number;\n    declare debrisTypes: string[];\n    declare debrisAnims: string[];\n    declare isLightpost: boolean;\n    declare lightVisibility: number;\n    declare lightIntensity: number;\n    declare lightRedTint: number;\n    declare lightGreenTint: number;\n    declare lightBlueTint: number;\n    declare ambientSound?: string;\n    declare createSound?: string;\n    declare deploySound?: string;\n    declare undeploySound?: string;\n    declare voiceSelect?: string;\n    declare voiceMove?: string;\n    declare voiceAttack?: string;\n    declare voiceFeedback?: string;\n    declare voiceSpecialAttack?: string;\n    declare voiceEnter?: string;\n    declare voiceCapture?: string;\n    declare voiceCrashing?: string;\n    declare crashingSound?: string;\n    declare impactLandSound?: string;\n    declare auxSound1?: string;\n    declare auxSound2?: string;\n    declare dieSound?: string;\n    declare moveSound?: string;\n    declare enterWaterSound?: string;\n    declare leaveWaterSound?: string;\n    declare turretRotateSound?: string;\n    declare workingSound?: string;\n    declare notWorkingSound?: string;\n    declare chronoInSound?: string;\n    declare chronoOutSound?: string;\n    declare enterTransportSound?: string;\n    declare leaveTransportSound?: string;\n    constructor(e: any, t: any, i: any, r: any) {\n        super(e, t, i, r);\n    }\n    parse(): void {\n        super.parse();\n        this.owner = this.ini.getArray(\"Owner\");\n        const aiBasePlanningValue = this.ini.getNumber(\"AIBasePlanningSide\");\n        this.aiBasePlanningSide = (-1 !== aiBasePlanningValue && void 0 !== SideType[aiBasePlanningValue]) ? aiBasePlanningValue : void 0;\n        this.requiredHouses = this.ini.getArray(\"RequiredHouses\");\n        this.forbiddenHouses = this.ini.getArray(\"ForbiddenHouses\");\n        this.requiresStolenAlliedTech = this.ini.getBool(\"RequiresStolenAlliedTech\");\n        this.requiresStolenSovietTech = this.ini.getBool(\"RequiresStolenSovietTech\");\n        this.techLevel = this.ini.getNumber(\"TechLevel\", -1);\n        this.cost = this.ini.getNumber(\"Cost\");\n        this.points = this.ini.getNumber(\"Points\");\n        this.power = this.ini.getNumber(\"Power\");\n        this.powered = this.ini.getBool(\"Powered\");\n        this.prerequisite = this.ini.getArray(\"Prerequisite\");\n        this.soylent = this.ini.getNumber(\"Soylent\");\n        this.crateGoodie = this.ini.getBool(\"CrateGoodie\");\n        this.buildCat = this.ini.getEnum(\"BuildCat\", BuildCat, BuildCat.Combat);\n        this.adjacent = this.ini.getNumber(\"Adjacent\", 1);\n        this.baseNormal = this.ini.getBool(\"BaseNormal\", true);\n        this.buildLimit = this.ini.getNumber(\"BuildLimit\", Number.POSITIVE_INFINITY);\n        this.airRangeBonus = this.ini.getNumber(\"AirRangeBonus\");\n        this.guardRange = this.ini.getNumber(\"GuardRange\");\n        this.defaultToGuardArea = this.ini.getBool(\"DefaultToGuardArea\");\n        this.eligibileForAllyBuilding = this.ini.getBool(\"EligibileForAllyBuilding\");\n        this.numberImpassableRows = this.ini.getNumber(\"NumberImpassableRows\");\n        this.bridgeRepairHut = this.ini.getBool(\"BridgeRepairHut\");\n        this.constructionYard = this.ini.getBool(\"ConstructionYard\");\n        this.refinery = this.ini.getBool(\"Refinery\");\n        this.unitRepair = this.ini.getBool(\"UnitRepair\");\n        this.unitReload = this.ini.getBool(\"UnitReload\");\n        this.unitSell = this.ini.getBool(\"UnitSell\");\n        this.isBaseDefense = this.ini.getBool(\"IsBaseDefense\");\n        this.superWeapon = this.parseWeaponName(this.ini.getString(\"SuperWeapon\"));\n        this.chargedAnimTime = this.ini.getNumber(\"ChargedAnimTime\");\n        const naval = this.ini.getBool(\"Naval\");\n        this.naval = naval;\n        this.underwater = this.ini.getBool(\"Underwater\");\n        this.waterBound = this.ini.getBool(\"WaterBound\");\n        this.orePurifier = this.ini.getBool(\"OrePurifier\");\n        this.cloning = this.ini.getBool(\"Cloning\");\n        this.grinding = this.ini.getBool(\"Grinding\");\n        this.nukeSilo = this.ini.getBool(\"NukeSilo\");\n        this.repairable = this.ini.getBool(\"Repairable\", this.type === ObjectType.Building);\n        this.clickRepairable = this.ini.getBool(\"ClickRepairable\", this.type === ObjectType.Building);\n        this.unsellable = this.ini.getBool(\"Unsellable\", this.type !== ObjectType.Building && this.generalRules.unitsUnsellable);\n        this.returnable = this.ini.getBool(\"Returnable\", this.generalRules.returnStructures);\n        this.gdiBarracks = this.ini.getBool(\"GDIBarracks\");\n        this.nodBarracks = this.ini.getBool(\"NODBarracks\");\n        this.numberOfDocks = this.ini.getNumber(\"NumberOfDocks\");\n        if (this.unitRepair && !this.numberOfDocks) {\n            this.numberOfDocks = 1;\n        }\n        this.factory = this.ini.getEnum(\"Factory\", FactoryType, FactoryType.None);\n        if (this.factory === FactoryType.UnitType && naval) {\n            this.factory = FactoryType.NavalUnitType;\n        }\n        this.weaponsFactory = this.ini.getBool(\"WeaponsFactory\");\n        this.helipad = this.ini.getBool(\"Helipad\");\n        this.hospital = this.ini.getBool(\"Hospital\");\n        this.landTargeting = this.ini.getEnumNumeric(\"LandTargeting\", LandTargeting, LandTargeting.LandOk);\n        this.navalTargeting = this.ini.getEnumNumeric(\"NavalTargeting\", NavalTargeting, NavalTargeting.UnderwaterNever);\n        this.tooBigToFitUnderBridge = this.ini.getBool(\"TooBigToFitUnderBridge\", this.type === ObjectType.Building);\n        this.canBeOccupied = this.ini.getBool(\"CanBeOccupied\");\n        this.maxNumberOccupants = this.ini.getNumber(\"MaxNumberOccupants\");\n        this.leaveRubble = this.ini.getBool(\"LeaveRubble\");\n        this.undeploysInto = this.ini.getString(\"UndeploysInto\");\n        this.deploysInto = this.ini.getString(\"DeploysInto\");\n        this.deployTime = this.ini.getNumber(\"DeployTime\");\n        this.capturable = this.ini.getBool(\"Capturable\");\n        this.spyable = this.ini.getBool(\"Spyable\");\n        this.needsEngineer = this.ini.getBool(\"NeedsEngineer\");\n        this.c4 = this.ini.getBool(\"C4\");\n        this.canC4 = this.ini.getBool(\"CanC4\", true);\n        this.eligibleForDelayKill = this.ini.getBool(\"EligibleForDelayKill\");\n        this.produceCashStartup = this.ini.getNumber(\"ProduceCashStartup\");\n        this.produceCashAmount = this.ini.getNumber(\"ProduceCashAmount\");\n        this.produceCashDelay = this.ini.getNumber(\"ProduceCashDelay\");\n        this.explosion = this.ini.getArray(\"Explosion\");\n        this.explodes = this.ini.getBool(\"Explodes\");\n        this.ifvMode = this.ini.getNumber(\"IFVMode\");\n        this.turretIndexesByIfvMode = this.parseTurretIndexes();\n        this.turret = this.ini.getBool(\"Turret\");\n        this.turretCount = this.ini.getNumber(\"TurretCount\", this.turret ? 1 : 0);\n        this.turretAnim = this.ini.getString(\"TurretAnim\");\n        this.turretAnimIsVoxel = this.ini.getBool(\"TurretAnimIsVoxel\");\n        this.turretAnimX = this.ini.getNumber(\"TurretAnimX\");\n        this.turretAnimY = this.ini.getNumber(\"TurretAnimY\");\n        this.turretAnimZAdjust = this.ini.getNumber(\"TurretAnimZAdjust\");\n        this.isChargeTurret = this.ini.getBool(\"IsChargeTurret\");\n        this.overpowerable = this.ini.getBool(\"Overpowerable\");\n        this.freeUnit = this.ini.getString(\"FreeUnit\");\n        this.primary = this.parseWeaponName(this.ini.getString(\"Primary\"));\n        this.secondary = this.parseWeaponName(this.ini.getString(\"Secondary\"));\n        this.elitePrimary = this.parseWeaponName(this.ini.getString(\"ElitePrimary\"));\n        this.eliteSecondary = this.parseWeaponName(this.ini.getString(\"EliteSecondary\"));\n        this.weaponCount = this.ini.getNumber(\"WeaponCount\");\n        this.deathWeapon = this.parseWeaponName(this.ini.getString(\"DeathWeapon\"));\n        this.deathWeaponDamageModifier = this.ini.getNumber(\"DeathWeaponDamageModifier\", 1);\n        this.occupyWeapon = this.parseWeaponName(this.ini.getString(\"OccupyWeapon\"));\n        this.eliteOccupyWeapon = this.parseWeaponName(this.ini.getString(\"EliteOccupyWeapon\"));\n        this.veteranAbilities = new Set(this.ini.getEnumArray(\"VeteranAbilities\", VeteranAbility));\n        this.eliteAbilities = new Set([\n            ...this.veteranAbilities,\n            ...this.ini.getEnumArray(\"EliteAbilities\", VeteranAbility)\n        ]);\n        this.selfHealing = this.ini.getBool(\"SelfHealing\");\n        this.wall = this.ini.getBool(\"Wall\");\n        this.gate = this.ini.getBool(\"Gate\");\n        this.armor = this.ini.getEnum(\"Armor\", ArmorType, ArmorType.None, true);\n        this.strength = Math.floor(this.ini.getNumber(\"Strength\"));\n        this.immune = this.ini.getBool(\"Immune\");\n        this.immuneToRadiation = this.ini.getBool(\"ImmuneToRadiation\");\n        this.immuneToPsionics = this.ini.getBool(\"ImmuneToPsionics\");\n        this.typeImmune = this.ini.getBool(\"TypeImmune\");\n        this.warpable = this.ini.getBool(\"Warpable\", true);\n        this.isTilter = this.ini.getBool(\"IsTilter\", true);\n        this.walkRate = this.ini.getNumber(\"WalkRate\", 1);\n        this.idleRate = this.ini.getNumber(\"IdleRate\", 0);\n        this.noSpawnAlt = this.ini.getBool(\"NoSpawnAlt\");\n        this.crusher = this.ini.getBool(\"Crusher\");\n        this.consideredAircraft = this.ini.getBool(\"ConsideredAircraft\");\n        this.crashable = this.ini.getBool(\"Crashable\");\n        const landable = this.ini.getBool(\"Landable\");\n        this.landable = landable;\n        this.airportBound = this.ini.getBool(\"AirportBound\");\n        this.balloonHover = this.ini.getBool(\"BalloonHover\");\n        this.hoverAttack = this.ini.getBool(\"HoverAttack\");\n        this.omniFire = this.ini.getBool(\"OmniFire\");\n        this.fighter = this.ini.getBool(\"Fighter\");\n        this.flightLevel = this.ini.getNumber(\"FlightLevel\") || void 0;\n        const locomotorString = this.ini.getString(\"Locomotor\");\n        let defaultLocomotor = this.type === ObjectType.Building ? LocomotorType.Statue : LocomotorType.Chrono;\n        if (locomotorString) {\n            const locomotorType = (LocomotorType as any).locomotorTypesByClsId?.get(locomotorString);\n            if (locomotorType) {\n                this.locomotor = locomotorType;\n            }\n            else {\n                console.warn(`Object rules \"${this.name}\" has invalid Locomotor \"${locomotorString}\"`);\n                this.locomotor = defaultLocomotor;\n            }\n        }\n        else {\n            this.locomotor = defaultLocomotor;\n        }\n        if (this.locomotor !== LocomotorType.Statue) {\n            let defaultSpeed = (LocomotorType as any).defaultSpeedsByLocomotor?.get(this.locomotor);\n            if (void 0 === defaultSpeed) {\n                if (this.type === ObjectType.Aircraft || this.consideredAircraft) {\n                    defaultSpeed = SpeedType.Winged;\n                }\n                else if (this.type === ObjectType.Vehicle) {\n                    defaultSpeed = this.crusher ? SpeedType.Track : SpeedType.Wheel;\n                }\n                else if (this.type === ObjectType.Infantry) {\n                    defaultSpeed = SpeedType.Foot;\n                }\n            }\n            this.speedType = this.ini.getEnum(\"SpeedType\", SpeedType, defaultSpeed, true);\n        }\n        const speedMultiplier = [\n            LocomotorType.Ship,\n            LocomotorType.Vehicle,\n            LocomotorType.Chrono\n        ].includes(this.locomotor) ? 65 : 100;\n        this.speed = ObjectRules.iniSpeedToLeptonsPerTick(this.ini.getNumber(\"Speed\"), speedMultiplier);\n        this.movementZone = this.ini.getEnum(\"MovementZone\", MovementZone, MovementZone.Normal);\n        this.fearless = this.ini.getBool(\"Fearless\");\n        this.deployer = this.ini.getBool(\"Deployer\");\n        this.deployFire = this.ini.getBool(\"DeployFire\");\n        this.deployFireWeapon = this.ini.getNumber(\"DeployFireWeapon\", WeaponType.Secondary);\n        this.undeployDelay = this.ini.getNumber(\"UndeployDelay\");\n        this.fraidycat = this.ini.getBool(\"Fraidycat\", false);\n        this.isHuman = !this.ini.getBool(\"NotHuman\");\n        this.organic = this.type === ObjectType.Infantry || this.ini.getBool(\"Organic\");\n        this.occupier = this.ini.getBool(\"Occupier\");\n        this.engineer = this.ini.getBool(\"Engineer\");\n        this.ivan = this.ini.getBool(\"Ivan\");\n        this.civilian = this.ini.getBool(\"Civilian\");\n        this.agent = this.ini.getBool(\"Agent\");\n        this.infiltrate = this.ini.getBool(\"Infiltrate\");\n        this.threatPosed = this.ini.getNumber(\"ThreatPosed\");\n        this.specialThreatValue = this.ini.getNumber(\"SpecialThreatValue\");\n        this.canPassiveAquire = this.ini.getBool(\"CanPassiveAquire\", true);\n        this.canRetaliate = this.ini.getBool(\"CanRetaliate\", true);\n        this.preventAttackMove = this.ini.getBool(\"PreventAttackMove\");\n        this.opportunityFire = this.ini.getBool(\"OpportunityFire\");\n        this.distributedFire = this.ini.getBool(\"DistributedFire\");\n        this.radialFireSegments = this.ini.getNumber(\"RadialFireSegments\");\n        this.attackCursorOnFriendlies = this.ini.getBool(\"AttackCursorOnFriendlies\");\n        this.bombable = this.ini.getBool(\"Bombable\", true);\n        this.trainable = this.ini.getBool(\"Trainable\", this.type !== ObjectType.Building);\n        this.crewed = this.ini.getBool(\"Crewed\");\n        this.parasiteable = this.ini.getBool(\"Parasiteable\", this.type !== ObjectType.Building);\n        this.suppressionThreshold = this.ini.getNumber(\"SuppressionThreshold\");\n        this.reselectIfLimboed = this.ini.getBool(\"ReselectIfLimboed\");\n        this.rejoinTeamIfLimboed = this.ini.getBool(\"RejoinTeamIfLimboed\");\n        this.weight = this.ini.getNumber(\"Weight\");\n        this.accelerates = this.ini.getBool(\"Accelerates\", true);\n        this.accelerationFactor = this.ini.getNumber(\"AccelerationFactor\", 0.03);\n        this.teleporter = this.ini.getBool(\"Teleporter\");\n        this.canDisguise = this.ini.getBool(\"CanDisguise\");\n        this.disguiseWhenStill = this.ini.getBool(\"DisguiseWhenStill\");\n        this.permaDisguise = this.ini.getBool(\"PermaDisguise\");\n        this.detectDisguise = this.ini.getBool(\"DetectDisguise\");\n        this.detectDisguiseRange = this.ini.getNumber(\"DetectDisguiseRange\");\n        this.cloakable = this.ini.getBool(\"Cloakable\");\n        this.sensors = this.ini.getBool(\"Sensors\");\n        this.sensorArray = this.ini.getBool(\"SensorArray\");\n        this.sensorsSight = this.ini.getNumber(\"SensorsSight\");\n        this.burstDelay = this.parseBurstDelay();\n        this.vhpScan = this.ini.getEnum(\"VHPScan\", VhpScan, VhpScan.None, true);\n        this.pip = this.ini.getEnum(\"Pip\", PipColor, PipColor.Green, true);\n        this.passengers = this.ini.getNumber(\"Passengers\");\n        this.gunner = this.ini.getBool(\"Gunner\");\n        this.ammo = this.ini.getNumber(\"Ammo\", -1);\n        this.initialAmmo = this.ini.getNumber(\"InitialAmmo\", -1);\n        this.manualReload = this.ini.getBool(\"ManualReload\", this.type === ObjectType.Aircraft);\n        this.storage = this.ini.getNumber(\"Storage\");\n        this.spawned = this.ini.getBool(\"Spawned\");\n        this.spawns = this.ini.getString(\"Spawns\");\n        this.spawnsNumber = this.ini.getNumber(\"SpawnsNumber\");\n        this.spawnRegenRate = this.ini.getNumber(\"SpawnRegenRate\");\n        this.spawnReloadRate = this.ini.getNumber(\"SpawnReloadRate\");\n        this.missileSpawn = this.ini.getBool(\"MissileSpawn\");\n        this.size = this.ini.getNumber(\"Size\", 1);\n        this.sizeLimit = this.ini.getNumber(\"SizeLimit\");\n        this.sight = Math.min(TechnoRules.MAX_SIGHT, this.needsEngineer ? 6 : this.ini.getNumber(\"Sight\", 1));\n        this.spySat = this.ini.getBool(\"SpySat\");\n        this.gapGenerator = this.ini.getBool(\"GapGenerator\");\n        this.gapRadiusInCells = this.ini.getNumber(\"GapRadiusInCells\");\n        this.psychicDetectionRadius = this.ini.getNumber(\"PsychicDetectionRadius\");\n        this.hasRadialIndicator = this.ini.getBool(\"HasRadialIndicator\");\n        this.harvester = this.ini.getBool(\"Harvester\");\n        this.unloadingClass = this.ini.getString(\"UnloadingClass\");\n        this.dock = this.ini.getArray(\"Dock\");\n        this.radar = this.ini.getBool(\"Radar\");\n        this.radarInvisible = this.ini.getBool(\"RadarInvisible\");\n        this.revealToAll = this.ini.getBool(\"RevealToAll\");\n        this.selectable = !(this.type === ObjectType.Aircraft && !landable) && this.ini.getBool(\"Selectable\", true);\n        this.isSelectableCombatant = this.ini.getBool(\"IsSelectableCombatant\");\n        this.invisibleInGame = this.ini.getBool(\"InvisibleInGame\");\n        this.moveToShroud = this.ini.getBool(\"MoveToShroud\", this.type !== ObjectType.Aircraft);\n        this.leadershipRating = this.ini.getNumber(\"LeadershipRating\", 5);\n        this.unnatural = this.ini.getBool(\"Unnatural\");\n        this.natural = this.ini.getBool(\"Natural\");\n        this.buildTimeMultiplier = this.ini.getFixed(\"BuildTimeMultiplier\", 1);\n        this.allowedToStartInMultiplayer = this.ini.getBool(\"AllowedToStartInMultiplayer\", true);\n        this.rot = ObjectRules.iniRotToDegsPerTick(this.ini.getNumber(\"ROT\", 0));\n        this.jumpjetAccel = this.ini.getNumber(\"JumpJetAccel\", 2);\n        this.jumpjetClimb = this.ini.getNumber(\"JumpjetClimb\", 5);\n        this.jumpjetCrash = this.ini.getNumber(\"JumpjetCrash\", 5);\n        this.jumpjetDeviation = this.ini.getNumber(\"JumpjetDeviation\", 40);\n        this.jumpjetHeight = this.ini.getNumber(\"JumpjetHeight\", 500);\n        this.jumpjetNoWobbles = this.ini.getBool(\"JumpjetNoWobbles\");\n        this.jumpjetSpeed = this.ini.getNumber(\"JumpjetSpeed\", 14);\n        this.jumpjetTurnRate = ObjectRules.iniRotToDegsPerTick(this.ini.getNumber(\"JumpJetTurnRate\", 4));\n        this.jumpjetWobbles = this.ini.getNumber(\"JumpjetWobbles\", 0.15);\n        this.pitchSpeed = this.ini.getNumber(\"PitchSpeed\", 0.25);\n        this.pitchAngle = this.pitchSpeed >= 1 ? 0 : 20;\n        this.damageParticleSystems = this.ini.getArray(\"DamageParticleSystems\");\n        const damageSmokeOffsetArray = this.ini.getNumberArray(\"DamageSmokeOffset\", undefined, [0, 0, 0]);\n        this.damageSmokeOffset = new Vector3(damageSmokeOffsetArray[0], damageSmokeOffsetArray[2] / Math.SQRT2, damageSmokeOffsetArray[1]);\n        this.minDebris = this.ini.getNumber(\"MinDebris\");\n        this.maxDebris = this.ini.getNumber(\"MaxDebris\");\n        this.debrisTypes = this.ini.getArray(\"DebrisTypes\");\n        this.debrisAnims = this.ini.getArray(\"DebrisAnims\");\n        this.isLightpost = this.imageName === \"GALITE\";\n        this.lightVisibility = this.ini.getNumber(\"LightVisibility\", 5000);\n        this.lightIntensity = this.ini.getNumber(\"LightIntensity\");\n        this.lightRedTint = this.ini.getNumber(\"LightRedTint\", 1);\n        this.lightGreenTint = this.ini.getNumber(\"LightGreenTint\", 1);\n        this.lightBlueTint = this.ini.getNumber(\"LightBlueTint\", 1);\n        this.ambientSound = this.ini.getString(\"AmbientSound\") || undefined;\n        this.createSound = this.ini.getString(\"CreateSound\") || undefined;\n        this.deploySound = this.ini.getString(\"DeploySound\") || undefined;\n        this.undeploySound = this.ini.getString(\"UndeploySound\") || undefined;\n        this.voiceSelect = this.ini.getString(\"VoiceSelect\") || undefined;\n        this.voiceMove = this.ini.getString(\"VoiceMove\") || undefined;\n        this.voiceAttack = this.ini.getString(\"VoiceAttack\") || undefined;\n        this.voiceFeedback = this.ini.getString(\"VoiceFeedback\") || undefined;\n        this.voiceSpecialAttack = this.ini.getString(\"VoiceSpecialAttack\") || undefined;\n        this.voiceEnter = this.ini.getString(\"VoiceEnter\") || undefined;\n        this.voiceCapture = this.ini.getString(\"VoiceCapture\") || undefined;\n        this.voiceCrashing = this.ini.getString(\"VoiceCrashing\") || undefined;\n        this.crashingSound = this.ini.getString(\"CrashingSound\") || undefined;\n        this.impactLandSound = this.ini.getString(\"ImpactLandSound\") || undefined;\n        this.auxSound1 = this.ini.getString(\"AuxSound1\") || undefined;\n        this.auxSound2 = this.ini.getString(\"AuxSound2\") || undefined;\n        this.dieSound = this.ini.getString(\"DieSound\") || undefined;\n        this.moveSound = this.ini.getString(\"MoveSound\") || undefined;\n        this.enterWaterSound = this.ini.getString(\"EnterWaterSound\") || undefined;\n        this.leaveWaterSound = this.ini.getString(\"LeaveWaterSound\") || undefined;\n        this.turretRotateSound = this.ini.getString(\"TurretRotateSound\") || undefined;\n        this.workingSound = this.ini.getString(\"WorkingSound\") || undefined;\n        this.notWorkingSound = this.ini.getString(\"NotWorkingSound\") || undefined;\n        this.chronoInSound = this.ini.getString(\"ChronoInSound\") || undefined;\n        this.chronoOutSound = this.ini.getString(\"ChronoOutSound\") || undefined;\n        this.enterTransportSound = this.ini.getString(\"EnterTransportSound\") || undefined;\n        this.leaveTransportSound = this.ini.getString(\"LeaveTransportSound\") || undefined;\n    }\n    private parseWeaponName(weaponName: string | undefined): string | undefined {\n        return weaponName && weaponName.toLowerCase() !== \"none\" ? weaponName : undefined;\n    }\n    private parseTurretIndexes(): Map<number, number> {\n        const turretIndexMap = new Map<number, number>();\n        if (this.ini.getBool(\"Gunner\")) {\n            this.ini.entries.forEach((value: string, key: string) => {\n                const match = key.match(/^(.*)TurretWeapon$/i);\n                if (match) {\n                    const turretIndexKey = match[1] + \"TurretIndex\";\n                    if (this.ini.has(turretIndexKey)) {\n                        turretIndexMap.set(Number(value), this.ini.getNumber(turretIndexKey));\n                    }\n                }\n            });\n        }\n        return turretIndexMap;\n    }\n    private parseBurstDelay(): (number | undefined)[] {\n        const burstDelays: (number | undefined)[] = [];\n        for (let i = 0; i < 4; i++) {\n            const key = \"BurstDelay\" + i;\n            burstDelays.push(this.ini.has(key) ? this.ini.getNumber(key) : undefined);\n        }\n        return burstDelays;\n    }\n    public hasOwner(house: House): boolean {\n        return this.owner.length > 0 && this.owner.indexOf(house.name) !== -1;\n    }\n    public isAvailableTo(house: House): boolean {\n        const hasRequiredHouse = this.requiredHouses.length === 0 ||\n            this.requiredHouses.indexOf(house.name) !== -1;\n        const isForbidden = this.forbiddenHouses.indexOf(house.name) !== -1;\n        return hasRequiredHouse && !isForbidden;\n    }\n    public getWeaponAtIndex(index: number): string | undefined {\n        return this.parseWeaponName(this.ini.getString(\"Weapon\" + (index + 1)));\n    }\n    public getEliteWeaponAtIndex(index: number): string | undefined {\n        return this.parseWeaponName(this.ini.getString(\"EliteWeapon\" + (index + 1)));\n    }\n}\n"
  },
  {
    "path": "src/game/rules/TerrainRules.ts",
    "content": "import { ObjectRules } from './ObjectRules';\nimport { TheaterType } from '@/engine/TheaterType';\nimport { ObjectType } from '@/engine/type/ObjectType';\nexport enum OccupationBits {\n    All = 7,\n    Right = 1,\n    Left = 2,\n    Bottom = 4\n}\nfunction testOccupationBit(subCell: number, bits: number): boolean {\n    switch (subCell) {\n        case 0:\n        case 1:\n            return true;\n        case 2:\n            return (bits & OccupationBits.Right) !== 0;\n        case 3:\n            return (bits & OccupationBits.Left) !== 0;\n        case 4:\n            return (bits & OccupationBits.Bottom) !== 0;\n        default:\n            throw new Error(`Invalid subCell \"${subCell}\"`);\n    }\n}\nexport class TerrainRules extends ObjectRules {\n    public animationRate!: number;\n    public animationProbability!: number;\n    public gate!: boolean;\n    public immune!: boolean;\n    public isAnimated!: boolean;\n    public snowOccupationBits!: number;\n    public spawnsTiberium!: boolean;\n    public strength!: number;\n    public radarInvisible!: boolean;\n    public temperateOccupationBits!: number;\n    constructor(type: ObjectType, ini: any, index: number = -1, generalRules?: any) {\n        super(type, ini, index, generalRules);\n        this.parse();\n    }\n    protected parse(): void {\n        super.parse();\n        this.animationRate = this.ini.getNumber(\"AnimationRate\");\n        this.animationProbability = this.ini.getNumber(\"AnimationProbability\");\n        this.gate = this.ini.getBool(\"Gate\");\n        this.immune = this.ini.getBool(\"Immune\");\n        this.isAnimated = this.ini.getBool(\"IsAnimated\");\n        this.snowOccupationBits = this.normalizeOccupationBits(this.ini.getNumber(\"SnowOccupationBits\", OccupationBits.All));\n        this.spawnsTiberium = this.ini.getBool(\"SpawnsTiberium\");\n        this.strength = this.ini.getNumber(\"Strength\");\n        this.radarInvisible = this.ini.getBool(\"RadarInvisible\");\n        this.temperateOccupationBits = this.normalizeOccupationBits(this.ini.getNumber(\"TemperateOccupationBits\", OccupationBits.All));\n    }\n    private normalizeOccupationBits(bits: number): number {\n        return (bits + 8 * Math.abs(Math.floor(bits / 8))) % 8;\n    }\n    public getOccupationBits(theaterType: TheaterType): number {\n        return theaterType !== TheaterType.Snow\n            ? this.temperateOccupationBits\n            : this.snowOccupationBits;\n    }\n    public getOccupiedSubCells(theaterType: TheaterType): number[] {\n        const bits = this.getOccupationBits(theaterType);\n        const allSubCells = [0, 1, 2, 3, 4];\n        if (bits === OccupationBits.All) {\n            return allSubCells;\n        }\n        const occupiedSubCells: number[] = [];\n        for (const subCell of allSubCells) {\n            if (testOccupationBit(subCell, bits)) {\n                occupiedSubCells.push(subCell);\n            }\n        }\n        return occupiedSubCells;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/TiberiumRules.ts",
    "content": "export class TiberiumRules {\n    public value: number;\n    readIni(ini: any): this {\n        this.value = ini.getNumber(\"Value\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/WarheadRules.ts",
    "content": "import { InfDeathType } from '../gameobject/infantry/InfDeathType';\nexport class WarheadRules {\n    private rules: any;\n    private verses: Map<number, number>;\n    public affectsAllies!: boolean;\n    public animList!: string[];\n    public bombDisarm!: boolean;\n    public bullets!: boolean;\n    public causesDelayKill!: boolean;\n    public cellSpread!: number;\n    public conventional!: boolean;\n    public culling!: boolean;\n    public delayKillAtMax!: number;\n    public delayKillFrames!: number;\n    public electricAssault!: boolean;\n    public emEffect!: boolean;\n    public infDeath!: InfDeathType;\n    public ivanBomb!: boolean;\n    public makesDisguise!: boolean;\n    public mindControl!: boolean;\n    public nukeMaker!: boolean;\n    public paralyzes!: number;\n    public parasite!: boolean;\n    public percentAtMax!: number;\n    public proneDamage!: number;\n    public psychicDamage!: boolean;\n    public radiation!: boolean;\n    public rocker!: boolean;\n    public sonic!: boolean;\n    public temporal!: boolean;\n    public wallAbsoluteDestroyer!: boolean;\n    public wall!: boolean;\n    public wood!: boolean;\n    constructor(rules: any) {\n        this.rules = rules;\n        this.verses = new Map();\n        this.parse();\n    }\n    get name(): string {\n        return this.rules.name;\n    }\n    private parse(): void {\n        this.affectsAllies = this.rules.getBool(\"AffectsAllies\", true);\n        this.animList = this.rules.getArray(\"AnimList\");\n        this.bombDisarm = this.rules.getBool(\"BombDisarm\");\n        this.bullets = this.rules.getBool(\"Bullets\");\n        this.causesDelayKill = this.rules.getBool(\"CausesDelayKill\");\n        this.cellSpread = this.rules.getNumber(\"CellSpread\");\n        this.conventional = this.rules.getBool(\"Conventional\");\n        this.culling = this.rules.getBool(\"Culling\");\n        this.delayKillAtMax = this.rules.getNumber(\"DelayKillAtMax\");\n        this.delayKillFrames = this.rules.getNumber(\"DelayKillFrames\");\n        this.electricAssault = this.rules.getBool(\"ElectricAssault\");\n        this.emEffect = this.rules.getBool(\"EMEffect\");\n        this.infDeath = this.rules.getEnumNumeric(\"InfDeath\", InfDeathType, InfDeathType.None);\n        this.ivanBomb = this.rules.getBool(\"IvanBomb\");\n        this.makesDisguise = this.rules.getBool(\"MakesDisguise\");\n        this.mindControl = this.rules.getBool(\"MindControl\");\n        this.nukeMaker = this.rules.getBool(\"NukeMaker\");\n        this.paralyzes = this.rules.getNumber(\"Paralyzes\");\n        this.parasite = this.rules.getBool(\"Parasite\");\n        this.percentAtMax = this.rules.getNumber(\"PercentAtMax\", 1);\n        this.proneDamage = this.rules.getFixed(\"ProneDamage\", 1);\n        this.psychicDamage = this.rules.getBool(\"PsychicDamage\");\n        this.radiation = this.rules.getBool(\"Radiation\");\n        this.rocker = this.rules.getBool(\"Rocker\");\n        this.sonic = this.rules.getBool(\"Sonic\");\n        this.temporal = this.rules.getBool(\"Temporal\");\n        const verses = this.rules.getFixedArray(\"Verses\");\n        verses.forEach((value: number, index: number) => this.verses.set(index, value));\n        this.wallAbsoluteDestroyer = this.rules.getBool(\"WallAbsoluteDestroyer\");\n        this.wall = this.rules.getBool(\"Wall\");\n        this.wood = this.rules.getBool(\"Wood\");\n    }\n}\n"
  },
  {
    "path": "src/game/rules/WeaponRules.ts",
    "content": "import { ObjectRules } from './ObjectRules';\nexport class WeaponRules {\n    private rules: any;\n    public ambientDamage!: number;\n    public anim!: string[];\n    public areaFire!: boolean;\n    public burst!: number;\n    public cellRangefinding!: boolean;\n    public damage!: number;\n    public decloakToFire!: boolean;\n    public fireOnce!: boolean;\n    public isAlternateColor!: boolean;\n    public isElectricBolt!: boolean;\n    public isHouseColor!: boolean;\n    public isLaser!: boolean;\n    public isRadBeam!: boolean;\n    public isSonic!: boolean;\n    public laserDuration!: number;\n    public limboLaunch!: boolean;\n    public minimumRange!: number;\n    public name!: string;\n    public neverUse!: boolean;\n    public omniFire!: boolean;\n    public projectile!: string;\n    public radLevel!: number;\n    public range!: number;\n    public report!: string[];\n    public revealOnFire!: boolean;\n    public rof!: number;\n    public sabotageCursor!: boolean;\n    public spawner!: boolean;\n    public iniSpeed!: number;\n    public speed!: number;\n    public suicide!: boolean;\n    public useSparkParticles!: boolean;\n    public warhead!: string;\n    constructor(rules: any) {\n        this.rules = rules;\n        this.parse();\n    }\n    private parse(): void {\n        this.ambientDamage = this.rules.getNumber(\"AmbientDamage\");\n        this.anim = this.rules.getArray(\"Anim\");\n        this.areaFire = this.rules.getBool(\"AreaFire\");\n        this.burst = this.rules.getNumber(\"Burst\", 1);\n        this.cellRangefinding = this.rules.getBool(\"CellRangefinding\");\n        this.damage = this.rules.getNumber(\"Damage\");\n        this.decloakToFire = this.rules.getBool(\"DecloakToFire\", true);\n        this.fireOnce = this.rules.getBool(\"FireOnce\");\n        this.isAlternateColor = this.rules.getBool(\"IsAlternateColor\");\n        this.isElectricBolt = this.rules.getBool(\"IsElectricBolt\");\n        this.isHouseColor = this.rules.getBool(\"IsHouseColor\");\n        this.isLaser = this.rules.getBool(\"IsLaser\");\n        this.isRadBeam = this.rules.getBool(\"IsRadBeam\");\n        this.isSonic = this.rules.getBool(\"IsSonic\");\n        this.laserDuration = this.rules.getNumber(\"LaserDuration\");\n        this.limboLaunch = this.rules.getBool(\"LimboLaunch\");\n        this.minimumRange = this.rules.getNumber(\"MinimumRange\");\n        this.name = this.rules.name;\n        this.neverUse = this.rules.getBool(\"NeverUse\");\n        this.omniFire = this.rules.getBool(\"OmniFire\");\n        this.projectile = this.rules.getString(\"Projectile\");\n        this.radLevel = this.rules.getNumber(\"RadLevel\");\n        this.range = this.rules.getNumber(\"Range\");\n        if (this.range === -2) {\n            this.range = Number.POSITIVE_INFINITY;\n        }\n        this.report = this.rules.getArray(\"Report\");\n        this.revealOnFire = this.rules.getBool(\"RevealOnFire\", true);\n        this.rof = this.rules.getNumber(\"ROF\");\n        this.sabotageCursor = this.rules.getBool(\"SabotageCursor\");\n        this.spawner = this.rules.getBool(\"Spawner\");\n        const speed = this.rules.getNumber(\"Speed\");\n        this.iniSpeed = speed;\n        this.speed = ObjectRules.iniSpeedToLeptonsPerTick(speed, 100);\n        this.suicide = this.rules.getBool(\"Suicide\");\n        this.useSparkParticles = this.rules.getBool(\"UseSparkParticles\");\n        this.warhead = this.rules.getString(\"Warhead\");\n    }\n}\n"
  },
  {
    "path": "src/game/rules/general/CrewRules.ts",
    "content": "export class CrewRules {\n    public alliedCrew: string = '';\n    private alliedSurvivorDivisor: number = 0;\n    private crewEscape: number = 0;\n    public sovietCrew: string = '';\n    private sovietSurvivorDivisor: number = 0;\n    private survivorRate: number = 0;\n    readIni(ini: any): CrewRules {\n        this.alliedCrew = ini.getString(\"AlliedCrew\");\n        this.alliedSurvivorDivisor = ini.getNumber(\"AlliedSurvivorDivisor\");\n        this.crewEscape = ini.getNumber(\"CrewEscape\");\n        this.sovietCrew = ini.getString(\"SovietCrew\");\n        this.sovietSurvivorDivisor = ini.getNumber(\"SovietSurvivorDivisor\");\n        this.survivorRate = ini.getNumber(\"SurvivorRate\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/general/DMislRules.ts",
    "content": "import { MissileRules } from './MissileRules';\nexport class DMislRules extends MissileRules {\n    private pauseFrames: number = 0;\n    private tiltFrames: number = 0;\n    private pitchInitial: number = 0;\n    private pitchFinal: number = 0;\n    private turnRate: number = 0;\n    private acceleration: number = 0;\n    private altitude: number = 0;\n    private damage: number = 0;\n    private eliteDamage: number = 0;\n    private bodyLength: number = 0;\n    private lazyCurve: boolean = false;\n    public type: string = '';\n    readIni(ini: any): DMislRules {\n        this.pauseFrames = ini.getNumber(\"DMislPauseFrames\");\n        this.tiltFrames = ini.getNumber(\"DMislTiltFrames\");\n        this.pitchInitial = ini.getNumber(\"DMislPitchInitial\");\n        this.pitchFinal = ini.getNumber(\"DMislPitchFinal\");\n        this.turnRate = ini.getNumber(\"DMislTurnRate\");\n        this.acceleration = ini.getNumber(\"DMislAcceleration\");\n        this.altitude = ini.getNumber(\"DMislAltitude\");\n        this.damage = ini.getNumber(\"DMislDamage\");\n        this.eliteDamage = ini.getNumber(\"DMislEliteDamage\");\n        this.bodyLength = ini.getNumber(\"DMislBodyLength\");\n        this.lazyCurve = ini.getBool(\"DMislLazyCurve\");\n        this.type = ini.getString(\"DMislType\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/general/HoverRules.ts",
    "content": "export class HoverRules {\n    private height: number = 0;\n    private dampen: number = 0;\n    private bob: number = 0;\n    private boost: number = 0;\n    private acceleration: number = 0;\n    private brake: number = 0;\n    readIni(ini: any): HoverRules {\n        this.height = ini.getNumber(\"HoverHeight\");\n        this.dampen = ini.getNumber(\"HoverDampen\");\n        this.bob = ini.getNumber(\"HoverBob\");\n        this.boost = ini.getNumber(\"HoverBoost\");\n        this.acceleration = ini.getNumber(\"HoverAcceleration\");\n        this.brake = ini.getNumber(\"HoverBrake\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/general/LightningStormRules.ts",
    "content": "export class LightningStormRules {\n    private deferment: number = 0;\n    private damage: number = 0;\n    private duration: number = 0;\n    private warhead: string = '';\n    private hitDelay: number = 0;\n    private scatterDelay: number = 0;\n    private cellSpread: number = 0;\n    private separation: number = 0;\n    readIni(ini: any): LightningStormRules {\n        this.deferment = ini.getNumber(\"LightningDeferment\");\n        this.damage = ini.getNumber(\"LightningDamage\");\n        this.duration = ini.getNumber(\"LightningStormDuration\");\n        this.warhead = ini.getString(\"LightningWarhead\");\n        this.hitDelay = ini.getNumber(\"LightningHitDelay\");\n        this.scatterDelay = ini.getNumber(\"LightningScatterDelay\");\n        this.cellSpread = ini.getNumber(\"LightningCellSpread\");\n        this.separation = ini.getNumber(\"LightningSeparation\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/general/MissileRules.ts",
    "content": "export class MissileRules {\n}\n"
  },
  {
    "path": "src/game/rules/general/ParadropRules.ts",
    "content": "import { SideType } from '../../SideType';\ninterface ParadropSquad {\n    inf: string;\n    num: number;\n}\nexport class ParadropRules {\n    private allyParaDrop: ParadropSquad[] = [];\n    private amerParaDrop: ParadropSquad[] = [];\n    private sovParaDrop: ParadropSquad[] = [];\n    private paradropPlane: string = '';\n    private paradropRadius: number = 0;\n    readIni(ini: any): ParadropRules {\n        this.allyParaDrop = this.readParadropSquad(ini.getArray(\"AllyParaDropInf\"), ini.getNumberArray(\"AllyParaDropNum\"), \"Ally\");\n        this.amerParaDrop = this.readParadropSquad(ini.getArray(\"AmerParaDropInf\"), ini.getNumberArray(\"AmerParaDropNum\"), \"Amer\");\n        this.sovParaDrop = this.readParadropSquad(ini.getArray(\"SovParaDropInf\"), ini.getNumberArray(\"SovParaDropNum\"), \"Sov\");\n        this.paradropPlane = ini.getString(\"ParadropPlane\");\n        if (!this.paradropPlane) {\n            throw new Error(\"Missing rules [General]->ParadropPlane\");\n        }\n        this.paradropRadius = ini.getNumber(\"ParadropRadius\");\n        return this;\n    }\n    private readParadropSquad(infArray: string[], numArray: number[], side: string): ParadropSquad[] {\n        if (infArray.length !== numArray.length) {\n            throw new RangeError(`${side}ParaDropInf/Num size mismatch (${infArray.length}, ${numArray.length})`);\n        }\n        const squads: ParadropSquad[] = [];\n        for (let i = 0; i < infArray.length; ++i) {\n            if (numArray[i] > 0) {\n                squads.push({ inf: infArray[i], num: numArray[i] });\n            }\n        }\n        return squads;\n    }\n    getParadropSquads(side: SideType): ParadropSquad[] {\n        switch (side) {\n            case SideType.GDI:\n                return this.allyParaDrop;\n            case SideType.Nod:\n                return this.sovParaDrop;\n            default:\n                throw new Error(`Unhandled side type \"${side}\"`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/rules/general/PrismRules.ts",
    "content": "export class PrismRules {\n    private type: string = '';\n    private supportHeight: number = 0;\n    private supportMax: number = 0;\n    private supportModifier: number = 1;\n    readIni(ini: any): PrismRules {\n        this.type = ini.getString(\"PrismType\");\n        this.supportHeight = ini.getNumber(\"PrismSupportHeight\");\n        this.supportMax = ini.getNumber(\"PrismSupportMax\");\n        this.supportModifier = ini.getNumber(\"PrismSupportModifier\", 1);\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/general/RadarRules.ts",
    "content": "export enum RadarEventType {\n    GenericCombat = 0,\n    GenericNonCombat = 1,\n    DropZone = 2,\n    BaseUnderAttack = 3,\n    HarvesterUnderAttack = 4,\n    EnemyObjectSensed = 5\n}\nexport class RadarRules {\n    private eventSuppressionDistances: number[] = [];\n    private eventVisibilityDurations: number[] = [];\n    private eventDurations: number[] = [];\n    private flashFrameTime: number = 0;\n    private combatFlashTime: number = 0;\n    private eventMinRadius: number = 0;\n    private eventSpeed: number = 0;\n    private eventRotationSpeed: number = 0;\n    private eventColorSpeed: number = 0;\n    readIni(ini: any): RadarRules {\n        this.eventSuppressionDistances = ini.getNumberArray(\"RadarEventSuppressionDistances\");\n        this.eventVisibilityDurations = ini.getNumberArray(\"RadarEventVisibilityDurations\");\n        this.eventDurations = ini.getNumberArray(\"RadarEventDurations\");\n        this.flashFrameTime = ini.getNumber(\"FlashFrameTime\");\n        this.combatFlashTime = ini.getNumber(\"RadarCombatFlashTime\");\n        this.eventMinRadius = ini.getNumber(\"RadarEventMinRadius\");\n        this.eventSpeed = ini.getNumber(\"RadarEventSpeed\");\n        this.eventRotationSpeed = ini.getNumber(\"RadarEventRotationSpeed\");\n        this.eventColorSpeed = ini.getNumber(\"RadarEventColorSpeed\");\n        return this;\n    }\n    getEventSuppresionDistance(eventType: RadarEventType): number {\n        if (eventType > this.eventSuppressionDistances.length - 1) {\n            throw new RangeError(`No event suppression distance is defined for type ${RadarEventType[eventType]}`);\n        }\n        return this.eventSuppressionDistances[eventType];\n    }\n    getEventVisibilityDuration(eventType: RadarEventType): number {\n        if (eventType > this.eventVisibilityDurations.length - 1) {\n            throw new RangeError(`No event visibility duration is defined for type ${RadarEventType[eventType]}`);\n        }\n        return this.eventVisibilityDurations[eventType];\n    }\n    getEventDuration(eventType: RadarEventType): number {\n        if (eventType > this.eventDurations.length - 1) {\n            throw new RangeError(`No event duration is defined for type ${RadarEventType[eventType]}`);\n        }\n        return this.eventDurations[eventType];\n    }\n}\n"
  },
  {
    "path": "src/game/rules/general/RepairRules.ts",
    "content": "export class RepairRules {\n    private reloadRate: number = 0;\n    private repairPercent: number = 0;\n    private repairRate: number = 0;\n    private repairStep: number = 0;\n    private uRepairRate: number = 0;\n    private iRepairRate: number = 0;\n    private iRepairStep: number = 0;\n    readIni(ini: any): RepairRules {\n        this.reloadRate = ini.getNumber(\"ReloadRate\");\n        this.repairPercent = ini.getNumber(\"RepairPercent\");\n        this.repairRate = ini.getNumber(\"RepairRate\");\n        this.repairStep = ini.getNumber(\"RepairStep\");\n        this.uRepairRate = ini.getNumber(\"URepairRate\");\n        this.iRepairRate = ini.getNumber(\"IRepairRate\");\n        this.iRepairStep = ini.getNumber(\"IRepairStep\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/general/ThreatRules.ts",
    "content": "export class ThreatRules {\n    private myEffectivenessCoefficientDefault: number = 0;\n    private targetEffectivenessCoefficientDefault: number = 0;\n    private targetSpecialThreatCoefficientDefault: number = 0;\n    private targetStrengthCoefficientDefault: number = 0;\n    private targetDistanceCoefficientDefault: number = 0;\n    readIni(ini: any): ThreatRules {\n        this.myEffectivenessCoefficientDefault = ini.getNumber(\"MyEffectivenessCoefficientDefault\");\n        this.targetEffectivenessCoefficientDefault = ini.getNumber(\"TargetEffectivenessCoefficientDefault\");\n        this.targetSpecialThreatCoefficientDefault = ini.getNumber(\"TargetSpecialThreatCoefficientDefault\");\n        this.targetStrengthCoefficientDefault = ini.getNumber(\"TargetStrengthCoefficientDefault\");\n        this.targetDistanceCoefficientDefault = ini.getNumber(\"TargetDistanceCoefficientDefault\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/general/V3RocketRules.ts",
    "content": "import { MissileRules } from './MissileRules';\nexport class V3RocketRules extends MissileRules {\n    private pauseFrames: number = 0;\n    private tiltFrames: number = 0;\n    private pitchInitial: number = 0;\n    private pitchFinal: number = 0;\n    private turnRate: number = 0;\n    private acceleration: number = 0;\n    private altitude: number = 0;\n    private damage: number = 0;\n    private eliteDamage: number = 0;\n    private bodyLength: number = 0;\n    private lazyCurve: boolean = false;\n    public type: string = '';\n    readIni(ini: any): V3RocketRules {\n        this.pauseFrames = ini.getNumber(\"V3RocketPauseFrames\");\n        this.tiltFrames = ini.getNumber(\"V3RocketTiltFrames\");\n        this.pitchInitial = ini.getNumber(\"V3RocketPitchInitial\");\n        this.pitchFinal = ini.getNumber(\"V3RocketPitchFinal\");\n        this.turnRate = ini.getNumber(\"V3RocketTurnRate\");\n        this.acceleration = ini.getNumber(\"V3RocketAcceleration\");\n        this.altitude = ini.getNumber(\"V3RocketAltitude\");\n        this.damage = ini.getNumber(\"V3RocketDamage\");\n        this.eliteDamage = ini.getNumber(\"V3RocketEliteDamage\");\n        this.bodyLength = ini.getNumber(\"V3RocketBodyLength\");\n        this.lazyCurve = ini.getBool(\"V3RocketLazyCurve\");\n        this.type = ini.getString(\"V3RocketType\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/general/VeteranRules.ts",
    "content": "export class VeteranRules {\n    public veteranRatio: number = 3;\n    private veteranCombat: number = 1;\n    private veteranSpeed: number = 1;\n    private veteranSight: number = 1;\n    private veteranArmor: number = 1;\n    private veteranROF: number = 1;\n    private veteranCap: number = 2;\n    public initialVeteran: boolean = false;\n    readIni(ini: any): VeteranRules {\n        this.veteranRatio = ini.getNumber(\"VeteranRatio\", 3);\n        this.veteranCombat = ini.getNumber(\"VeteranCombat\", 1);\n        this.veteranSpeed = ini.getNumber(\"VeteranSpeed\", 1);\n        this.veteranSight = Math.max(1, ini.getNumber(\"VeteranSight\", 1));\n        this.veteranArmor = ini.getNumber(\"VeteranArmor\", 1);\n        this.veteranROF = ini.getNumber(\"VeteranROF\", 1);\n        this.veteranCap = ini.getNumber(\"VeteranCap\", 2);\n        this.initialVeteran = ini.getBool(\"InitialVeteran\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/game/rules/mpAllowedColors.ts",
    "content": "export const mpAllowedColors = [\n    \"Gold\",\n    \"DarkRed\",\n    \"DarkBlue\",\n    \"DarkGreen\",\n    \"Orange\",\n    \"DarkSky\",\n    \"Purple\",\n    \"Magenta\"\n];\n"
  },
  {
    "path": "src/game/superweapon/ChronoSphereEffect.ts",
    "content": "import { DeathType } from \"@/game/gameobject/common/DeathType\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { MovementZone } from \"@/game/type/MovementZone\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { SuperWeaponEffect } from \"@/game/superweapon/SuperWeaponEffect\";\nexport class ChronoSphereEffect extends SuperWeaponEffect {\n    private tile2: any;\n    private objectsToTeleport: Array<{\n        obj: any;\n        destTile: any;\n    }> = [];\n    private delayTicks: number = 0;\n    constructor(e: any, t: any, i: any, r: any) {\n        super(e, t, i);\n        this.tile2 = r;\n        this.objectsToTeleport = [];\n    }\n    onStart(t: any): void {\n        this.delayTicks = t.rules.general.chronoDelay;\n        let i = t.map.tiles;\n        for (let o = -1; o <= 1; o++) {\n            for (let e = -1; e <= 1; e++) {\n                var r = i.getByMapCoords(this.tile.rx + o, this.tile.ry + e);\n                if (r) {\n                    var s: any, a = !!r.onBridgeLandType, n = i.getByMapCoords(this.tile2.rx + o, this.tile2.ry + e);\n                    for (s of t.map.getGroundObjectsOnTile(r)) {\n                        if (!s.isUnit() ||\n                            s.tile !== r ||\n                            s.onBridge !== a ||\n                            (s.isInfantry() &&\n                                s.stance === StanceType.Paradrop &&\n                                2 < s.tileElevation) ||\n                            s.isDisposed ||\n                            s.invulnerableTrait.isActive()) {\n                            continue;\n                        }\n                        if ((s.rules.organic && !s.rules.teleporter) || !n) {\n                            t.destroyObject(s, { player: this.owner });\n                        }\n                        else if (!s.warpedOutTrait.isActive()) {\n                            s.warpedOutTrait.setActive(true, true, t);\n                            this.objectsToTeleport.push({\n                                obj: s,\n                                destTile: n,\n                            });\n                        }\n                    }\n                }\n            }\n        }\n    }\n    onTick(l: any): boolean {\n        if (0 < this.delayTicks) {\n            this.delayTicks--;\n        }\n        if (this.delayTicks) {\n            return false;\n        }\n        for (let { obj: d, destTile: g } of this.objectsToTeleport) {\n            if (d.isSpawned) {\n                let i = false, r = g ? l.map.tileOccupation.getBridgeOnTile(g) : undefined, s = l.map.getGroundObjectsOnTile(g), a = s.find((e: any) => e.isBuilding());\n                var c = s.some((e: any) => l.rules.general.padAircraft.includes(e.name)), h = l.rules.general.padAircraft.includes(d.name) &&\n                    !!a?.helipadTrait &&\n                    !!a.dockTrait?.getAllDockTiles().includes(g) &&\n                    !a.dockTrait.hasReservedDockAt(a.dockTrait.getDockNumberByTile(g)) &&\n                    a.owner === d.owner;\n                let e = false, n = d.rules.speedType, o = d.isInfantry();\n                if (d.rules.movementZone === MovementZone.Fly) {\n                    n = SpeedType.Wheel;\n                }\n                var u = l.map.mapBounds.isWithinBounds(g);\n                if (!(h || (l.map.terrain.getPassableSpeed(g, n, o, !!r) && u))) {\n                    let t = false;\n                    if (!c &&\n                        (0 <\n                            l.map.terrain.getPassableSpeed(g, n, o, !!r, undefined, true) ||\n                            !u)) {\n                        if (a) {\n                            i = true;\n                        }\n                        let e = new RadialTileFinder(l.map.tiles, l.map.mapBounds, g, { width: 1, height: 1 }, 1, 15, (e: any) => 0 <\n                            l.map.terrain.getPassableSpeed(e, n, o, !!e.onBridgeLandType) &&\n                            !l.map.terrain.findObstacles({ tile: e, onBridge: !!e.onBridgeLandType }, d).length);\n                        u = e.getNextTile();\n                        if (u) {\n                            g = u;\n                            r = l.map.tileOccupation.getBridgeOnTile(g);\n                            s = l.map.getGroundObjectsOnTile(g);\n                            t = true;\n                        }\n                    }\n                    if (!t) {\n                        d.moveTrait.teleportUnitToTile(g, r, true, false, l);\n                        d.warpedOutTrait.setActive(false, true, l);\n                        if (l.map.getTileZone(g) === ZoneType.Water) {\n                            d.deathType = DeathType.Sink;\n                        }\n                        l.destroyObject(d, { player: this.owner });\n                        e = true;\n                    }\n                }\n                for (let t of s) {\n                    if (!t.isDisposed &&\n                        t.isUnit() &&\n                        !this.objectsToTeleport.some(({ obj: e }) => e === t) &&\n                        !(t.onBridge !== !!r && t.tile === g) &&\n                        !(2 < Math.abs(t.tileElevation - d.tileElevation))) {\n                        if (t.isInfantry() &&\n                            t.stance !== StanceType.Paradrop) {\n                            t.deathType = DeathType.Crush;\n                        }\n                        l.destroyObject(t, { player: this.owner, obj: d });\n                    }\n                }\n                if (!e) {\n                    d.moveTrait.teleportUnitToTile(g, r, true, false, l);\n                    if (h && a?.dockTrait) {\n                        h = a.dockTrait.getAllDockTiles().indexOf(g);\n                        a.dockTrait.undockUnitAt(h);\n                        if (a.dockTrait.hasReservedDockAt(h)) {\n                            throw new Error(\"Target building dock is already reserved by another unit\");\n                        }\n                        a.dockTrait.dockUnitAt(d, h);\n                    }\n                    if (i) {\n                        d.warpedOutTrait.setTimed(l.rules.general.chronoDelay, false, l);\n                    }\n                    else {\n                        d.warpedOutTrait.setActive(false, true, l);\n                    }\n                }\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/superweapon/IronCurtainEffect.ts",
    "content": "import { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { SuperWeaponEffect } from \"@/game/superweapon/SuperWeaponEffect\";\nimport { Game } from \"@/game/Game\";\nexport class IronCurtainEffect extends SuperWeaponEffect {\n    onStart(game: Game) {\n        const duration = game.rules.combatDamage.ironCurtainDuration;\n        const source = { player: this.owner };\n        const tileFinder = new RadialTileFinder(game.map.tiles, game.map.mapBounds, this.tile, { width: 1, height: 1 }, 0, 1, () => true);\n        let tile;\n        while ((tile = tileFinder.getNextTile())) {\n            for (const object of game.map.getGroundObjectsOnTile(tile)) {\n                if (!object.isTechno() ||\n                    (object.isUnit() && object.tile !== tile) ||\n                    object.rules.missileSpawn) {\n                    continue;\n                }\n                if (object.rules.organic) {\n                    if (!object.isDestroyed) {\n                        game.destroyObject(object, source);\n                    }\n                }\n                else {\n                    object.invulnerableTrait.setActiveFor(duration, game.currentTick);\n                    if ((object.isVehicle() || object.isAircraft()) &&\n                        object.parasiteableTrait?.isInfested()) {\n                        object.parasiteableTrait.destroyParasite(source, game);\n                    }\n                }\n            }\n        }\n    }\n    onTick(game: Game): boolean {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/superweapon/LightningStormEffect.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { LightningStormCloudEvent } from \"@/game/event/LightningStormCloudEvent\";\nimport { LightningStormManifestEvent } from \"@/game/event/LightningStormManifestEvent\";\nimport { CollisionType } from \"@/game/gameobject/unit/CollisionType\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nimport { RandomTileFinder } from \"@/game/map/tileFinder/RandomTileFinder\";\nimport { Warhead } from \"@/game/Warhead\";\nimport { SuperWeaponEffect, TileCoord } from \"@/game/superweapon/SuperWeaponEffect\";\nimport { Game } from \"@/game/Game\";\nimport { Vector3 } from \"@/game/math/Vector3\";\nenum LightningStormState {\n    Approaching,\n    Manifesting\n}\ninterface Cloud {\n    tile: TileCoord;\n    durationTicks: number;\n    ticksLeft: number;\n}\nexport class LightningStormEffect extends SuperWeaponEffect {\n    private state: LightningStormState = LightningStormState.Approaching;\n    private clouds: Cloud[] = [];\n    private manifestStartTimer: number = 0;\n    private manifestEndTimer: number = 0;\n    private nextDirectHitTimer: number = 0;\n    private nextRandomHitTimer: number = 0;\n    onStart(game: Game): void {\n        const lightningStorm = game.rules.general.lightningStorm;\n        this.manifestStartTimer = lightningStorm.deferment;\n        this.manifestEndTimer = lightningStorm.duration;\n        this.nextDirectHitTimer = 0;\n        this.nextRandomHitTimer = 0;\n    }\n    onTick(game: Game): boolean {\n        if (this.state === LightningStormState.Approaching) {\n            if (this.manifestStartTimer > 0) {\n                this.manifestStartTimer--;\n            }\n            else {\n                this.state = LightningStormState.Manifesting;\n                game.events.dispatch(new LightningStormManifestEvent(this.tile));\n            }\n        }\n        if (this.state === LightningStormState.Manifesting) {\n            const lightningStorm = game.rules.general.lightningStorm;\n            if (this.manifestEndTimer > 0) {\n                this.manifestEndTimer--;\n                if (this.nextDirectHitTimer > 0) {\n                    this.nextDirectHitTimer--;\n                }\n                if (this.nextDirectHitTimer <= 0) {\n                    this.nextDirectHitTimer = lightningStorm.hitDelay;\n                    this.spawnCloudAt(this.tile, game);\n                }\n                if (this.nextRandomHitTimer > 0) {\n                    this.nextRandomHitTimer--;\n                }\n                if (this.nextRandomHitTimer <= 0) {\n                    this.nextRandomHitTimer = lightningStorm.scatterDelay;\n                    const radius = Math.floor(lightningStorm.cellSpread / 2);\n                    const separation = lightningStorm.separation;\n                    const rangeHelper = new RangeHelper(game.map.tileOccupation);\n                    const tileFinder = new RandomTileFinder(game.map.tiles, game.map.mapBounds, this.tile, radius, game, (tile) => !this.clouds.some(cloud => rangeHelper.tileDistance(tile, cloud.tile) < separation), false);\n                    const randomTile = tileFinder.getNextTile();\n                    if (randomTile) {\n                        this.spawnCloudAt(randomTile, game);\n                    }\n                }\n            }\n            for (const cloud of this.clouds.slice()) {\n                if (cloud.ticksLeft > 0) {\n                    cloud.ticksLeft--;\n                    if (cloud.ticksLeft === Math.floor(cloud.durationTicks / 2)) {\n                        const warheadName = lightningStorm.warhead;\n                        const warhead = new Warhead(game.rules.getWarhead(warheadName));\n                        const tile = cloud.tile;\n                        const bridge = game.map.tileOccupation.getBridgeOnTile(tile);\n                        const elevation = bridge?.tileElevation ?? 0;\n                        const zone = game.map.getTileZone(tile);\n                        warhead.detonate(game as any, lightningStorm.damage, tile, elevation, Coords.tile3dToWorld(tile.rx + 0.5, tile.ry + 0.5, tile.z + elevation), zone, bridge ? CollisionType.OnBridge : CollisionType.None, game.createTarget(bridge, tile), { player: this.owner, weapon: undefined } as any, false, undefined, undefined, true);\n                    }\n                }\n                else {\n                    this.clouds.splice(this.clouds.indexOf(cloud), 1);\n                }\n            }\n            if (!this.clouds.length && this.manifestEndTimer <= 0) {\n                return true;\n            }\n        }\n        return false;\n    }\n    private spawnCloudAt(tile: TileCoord, game: Game): void {\n        const clouds = game.rules.audioVisual.weatherConClouds;\n        const cloudIndex = game.generateRandomInt(0, clouds.length - 1);\n        const animation = game.art.getAnimation(clouds[cloudIndex]);\n        const rate = animation.art.getNumber(\"Rate\", 60 * GameSpeed.BASE_TICKS_PER_SECOND) / 60;\n        const durationTicks = Math.floor((GameSpeed.BASE_TICKS_PER_SECOND / rate) * 60);\n        this.clouds.push({\n            tile,\n            durationTicks,\n            ticksLeft: durationTicks\n        });\n        const elevation = (game.map.tileOccupation.getBridgeOnTile(tile)?.tileElevation ?? 0) +\n            Coords.worldToTileHeight(game.rules.general.flightLevel);\n        const position = Coords.tile3dToWorld(tile.rx + 0.5, tile.ry + 0.5, tile.z + elevation);\n        game.events.dispatch(new LightningStormCloudEvent(position));\n    }\n}\n"
  },
  {
    "path": "src/game/superweapon/NukeEffect.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { Weapon } from \"@/game/Weapon\";\nimport { WeaponType } from \"@/game/WeaponType\";\nimport { SuperWeaponEffect, TileCoord } from \"@/game/superweapon/SuperWeaponEffect\";\nimport { Game } from \"@/game/Game\";\nimport { Target } from \"@/game/Target\";\nimport { Player } from \"../Player\";\nexport class NukeEffect extends SuperWeaponEffect {\n    private weaponType: string;\n    constructor(type: any, owner: Player, tile: TileCoord, weaponType: string) {\n        super(type, owner, tile);\n        this.weaponType = weaponType;\n    }\n    onStart(game: Game): void {\n        const weapon = game.rules.getWeapon(this.weaponType);\n        const target = game.createTarget(undefined, this.tile);\n        const silo = this.owner\n            .getOwnedObjectsByType(ObjectType.Building)\n            .find(building => (building as any).rules.nukeSilo);\n        if (silo) {\n            const weaponInstance = Weapon.factory(weapon.name, WeaponType.Primary, silo as any, game.rules);\n            weaponInstance.fire(target, game);\n        }\n        else {\n            this.fireLooseNuke(weapon, target, game);\n        }\n    }\n    private fireLooseNuke(weapon: Weapon, target: Target, game: Game): void {\n        const position = new Vector2(this.tile.rx + 0.5, this.tile.ry + 0.5).multiplyScalar(Coords.LEPTONS_PER_TILE);\n        if (game.map.isWithinHardBounds(position)) {\n            const projectile = game.createLooseProjectile(weapon.name, this.owner, target);\n            projectile.position.moveToLeptons(position);\n            projectile.position.tileElevation = Coords.worldToTileHeight(projectile.rules.detonationAltitude);\n            game.spawnObject(projectile, projectile.position.tile);\n        }\n    }\n    onTick(game: Game): boolean {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/superweapon/ParadropEffect.ts",
    "content": "import { Coords } from \"@/game/Coords\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { Infantry } from \"@/game/gameobject/Infantry\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { MoveTask } from \"@/game/gameobject/task/move/MoveTask\";\nimport { ParadropTask } from \"@/game/gameobject/task/ParadropTask\";\nimport { UnlandableTrait } from \"@/game/gameobject/trait/UnlandableTrait\";\nimport { FacingUtil } from \"@/game/gameobject/unit/FacingUtil\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { VeteranLevel } from \"@/game/gameobject/unit/VeteranLevel\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { bresenham } from \"@/util/bresenham\";\nimport { SuperWeaponEffect } from \"@/game/superweapon/SuperWeaponEffect\";\nenum ParadropState {\n    Spawning = 0,\n    EnRoute = 1,\n    Dropping = 2,\n    TurningAround = 3\n}\nconst SPAWN_DELAY_MULTIPLIER = 5 * GameSpeed.BASE_TICKS_PER_SECOND;\nexport class ParadropEffect extends SuperWeaponEffect {\n    private paradropSquad: any;\n    private state: ParadropState;\n    private failedAttempts: number;\n    private spawnDelay: number;\n    private passengerRules: any;\n    private passengerCount: number;\n    private targetTile: any;\n    private pdPlane: any;\n    constructor(e: any, t: any, i: any, r: any, s: number) {\n        super(e, t, i);\n        this.paradropSquad = r;\n        this.state = ParadropState.Spawning;\n        this.failedAttempts = 0;\n        this.spawnDelay = s * SPAWN_DELAY_MULTIPLIER;\n    }\n    onStart(e: any): void {\n        this.passengerRules = e.rules.getObject(this.paradropSquad.inf, ObjectType.Infantry);\n        this.passengerCount = this.paradropSquad.num;\n    }\n    computeFlightPath(e: Vector2, t: Vector2, i: any): {\n        fromTile: any;\n        toTile: any;\n    } {\n        if (t.equals(e))\n            throw new Error(\"Source and destination must be different\");\n        let r = e.clone().sub(t);\n        const radiusInTiles = i.rules.general.paradrop.paradropRadius / Coords.LEPTONS_PER_TILE;\n        const endPoint = t\n            .clone()\n            .add(r.clone().setLength(r.length() + 2 * radiusInTiles))\n            .floor();\n        let pathTiles = bresenham(t.x, t.y, endPoint.x, endPoint.y)\n            .map((e: any) => i.map.tiles.getByMapCoords(e.x, e.y) ??\n            i.map.tiles.getPlaceholderTile(e.x, e.y));\n        while (pathTiles.length) {\n            const firstTile = pathTiles[0];\n            const worldCoords = Coords.tileToWorld(firstTile.rx + 0.5, firstTile.ry + 0.5);\n            if (i.map.isWithinHardBounds(new Vector2(worldCoords.x, worldCoords.y)))\n                break;\n            pathTiles.shift();\n        }\n        if (!pathTiles.length)\n            throw new Error(\"No valid paradrop path found\");\n        return { fromTile: pathTiles[0], toTile: pathTiles[pathTiles.length - 1] };\n    }\n    onTick(gameState: any): boolean {\n        if (this.state === ParadropState.Spawning) {\n            if (this.spawnDelay > 0) {\n                this.spawnDelay--;\n                return false;\n            }\n            const mapSize = gameState.map.tiles.getMapSize();\n            const spawnPoints = [\n                new Vector2(0, 0),\n                new Vector2(Math.floor(mapSize.width / 2), 0),\n                new Vector2(0, Math.floor(mapSize.height / 2)),\n            ];\n            const spawnPoint = spawnPoints[gameState.generateRandomInt(0, 2)];\n            const speedType = this.passengerRules.speedType;\n            const tileFinder = new RadialTileFinder(gameState.map.tiles, gameState.map.mapBounds, this.tile, { width: 1, height: 1 }, 0, 50, (tile: any) => gameState.map.terrain.getPassableSpeed(tile, speedType, true, !!tile.onBridgeLandType) > 0);\n            const targetTile = (this.targetTile = tileFinder.getNextTile());\n            if (!targetTile)\n                return true;\n            const targetCoords = new Vector2(targetTile.rx, targetTile.ry);\n            const { fromTile, toTile } = this.computeFlightPath(targetCoords, spawnPoint, gameState);\n            const planeType = gameState.rules.general.paradrop.paradropPlane;\n            const planeRules = gameState.rules.getObject(planeType, ObjectType.Aircraft);\n            const plane = (this.pdPlane = gameState.createUnitForPlayer(planeRules, this.owner));\n            gameState.spawnObject(plane, fromTile);\n            plane.direction = FacingUtil.fromMapCoords(targetCoords.clone().sub(new Vector2(fromTile.rx, fromTile.ry)));\n            plane.position.tileElevation = Coords.worldToTileHeight(plane.rules.flightLevel ?? gameState.rules.general.flightLevel);\n            plane.zone = ZoneType.Air;\n            plane.onBridge = false;\n            plane.unitOrderTrait.addTask(new MoveTask(gameState, toTile, false, { allowOutOfBoundsTarget: true }));\n            plane.traits.get(UnlandableTrait).setEnabled(false);\n            this.state = ParadropState.EnRoute;\n        }\n        if (!this.pdPlane ||\n            this.pdPlane.isDestroyed ||\n            this.pdPlane.isCrashing) {\n            return true;\n        }\n        const targetTile = this.targetTile;\n        if (!this.pdPlane.unitOrderTrait.hasTasks()) {\n            this.state = ParadropState.TurningAround;\n            this.pdPlane.unitOrderTrait.addTask(new MoveTask(gameState, targetTile, false, { allowOutOfBoundsTarget: true }));\n            return false;\n        }\n        const paradropRadius = gameState.rules.general.paradrop.paradropRadius / Coords.LEPTONS_PER_TILE;\n        const rangeHelper = new RangeHelper(gameState.map.tileOccupation);\n        const inRange = rangeHelper.isInTileRange(this.pdPlane.tile, targetTile, 0, paradropRadius);\n        if (this.state === ParadropState.EnRoute && inRange) {\n            this.state = ParadropState.Dropping;\n        }\n        if (this.state === ParadropState.Dropping) {\n            if (inRange && this.passengerCount > 0) {\n                const currentTile = this.pdPlane.tile;\n                const onBridge = !!currentTile.onBridgeLandType;\n                if (this.failedAttempts > 5 &&\n                    gameState.map.mapBounds.isWithinBounds(currentTile)) {\n                    this.passengerCount = 0;\n                    return false;\n                }\n                if (!gameState.map.terrain.getPassableSpeed(currentTile, this.passengerRules.speedType, true, onBridge)) {\n                    return false;\n                }\n                const groundObjects = gameState.map.getGroundObjectsOnTile(currentTile);\n                if (groundObjects.some((obj: any) => (obj.isVehicle() && obj.onBridge === onBridge) ||\n                    (obj.isBuilding() && !obj.isDestroyed) ||\n                    (obj.isInfantry() && obj.stance === StanceType.Paradrop))) {\n                    return false;\n                }\n                const freeSubCell = this.findFreeSubCell(gameState, currentTile);\n                if (!freeSubCell)\n                    return false;\n                this.passengerCount--;\n                const infantry = gameState.createUnitForPlayer(this.passengerRules, this.owner);\n                infantry.stance = StanceType.Paradrop;\n                infantry.position.tileElevation = this.pdPlane.tileElevation;\n                infantry.position.subCell = freeSubCell;\n                infantry.onBridge = onBridge;\n                if (infantry.rules.trainable &&\n                    this.owner.canProduceVeteran(infantry.rules)) {\n                    infantry.veteranTrait?.setVeteranLevel(VeteranLevel.Veteran);\n                }\n                gameState.spawnObject(infantry, currentTile);\n                infantry.unitOrderTrait.addTask(new ParadropTask(gameState).setCancellable(false));\n            }\n            else {\n                if (this.passengerCount <= 0) {\n                    this.pdPlane.unitOrderTrait\n                        .getCurrentTask()\n                        .forceCancel(this.pdPlane);\n                    this.pdPlane.traits\n                        .get(UnlandableTrait)\n                        .setEnabled(true);\n                    return true;\n                }\n                this.failedAttempts++;\n                this.state = ParadropState.TurningAround;\n                this.pdPlane.unitOrderTrait\n                    .getCurrentTask()\n                    .updateTarget(targetTile, !!targetTile.onBridgeLandType);\n            }\n        }\n        if (this.state === ParadropState.TurningAround && inRange) {\n            const exitTile = this.computeFlightPath(new Vector2(targetTile.rx, targetTile.ry), new Vector2(this.pdPlane.tile.rx, this.pdPlane.tile.ry), gameState).toTile;\n            this.pdPlane.unitOrderTrait\n                .getCurrentTask()\n                .updateTarget(exitTile, false);\n            this.state = ParadropState.EnRoute;\n        }\n        return false;\n    }\n    findFreeSubCell(gameState: any, tile: any): any {\n        const occupiedSubCells = gameState.map\n            .getGroundObjectsOnTile(tile)\n            .filter((obj: any) => obj.isTerrain())\n            .map((obj: any) => obj.rules.getOccupiedSubCells(gameState.map.getTheaterType()))\n            .flat();\n        const availableSubCells = Infantry.SUB_CELLS.filter((subCell: any) => occupiedSubCells.indexOf(subCell) === -1);\n        if (availableSubCells.length) {\n            return availableSubCells.length > 1\n                ? availableSubCells[gameState.generateRandomInt(0, availableSubCells.length - 1)]\n                : availableSubCells[0];\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/game/superweapon/SuperWeaponEffect.ts",
    "content": "import { Game } from \"@/game/Game\";\nimport { Player } from \"@/game/Player\";\nexport type TileCoord = any;\nexport enum EffectStatus {\n    NotStarted = 0,\n    Running = 1,\n    Finished = 2\n}\nexport abstract class SuperWeaponEffect {\n    public type: any;\n    public owner: Player;\n    public tile: any;\n    public status: EffectStatus;\n    constructor(type: any, owner: Player, tile: any) {\n        this.type = type;\n        this.owner = owner;\n        this.tile = tile;\n        this.status = EffectStatus.NotStarted;\n    }\n    abstract onStart(game: Game): void;\n    onTick(game: Game): boolean {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/theater/AutoLat.ts",
    "content": "import { TileCollection, TileDirection } from '../map/TileCollection';\nexport class AutoLat {\n    static calculate(tiles: TileCollection, tileData: any) {\n        const tileSetMap = new Map();\n        tiles.forEach((tile) => {\n            let setNum = tileData.getSetNum(tile.tileNum);\n            tileSetMap.set(tile, setNum);\n            if (tileData.isCLAT(setNum)) {\n                setNum = tileData.getLAT(setNum);\n                tileSetMap.set(tile, setNum);\n                tile.tileNum = tileData.getTileNumFromSet(setNum);\n            }\n        });\n        tiles.forEach((tile) => {\n            const setNum = tileSetMap.get(tile);\n            if (tileData.isLAT(setNum)) {\n                let connectionFlags = 0;\n                const topRight = tiles.getNeighbourTile(tile, TileDirection.TopRight);\n                const bottomRight = tiles.getNeighbourTile(tile, TileDirection.BottomRight);\n                const bottomLeft = tiles.getNeighbourTile(tile, TileDirection.BottomLeft);\n                const topLeft = tiles.getNeighbourTile(tile, TileDirection.TopLeft);\n                if (topRight && tileData.canConnectTiles(setNum, tileSetMap.get(topRight)))\n                    connectionFlags += 1;\n                if (bottomRight && tileData.canConnectTiles(setNum, tileSetMap.get(bottomRight)))\n                    connectionFlags += 2;\n                if (bottomLeft && tileData.canConnectTiles(setNum, tileSetMap.get(bottomLeft)))\n                    connectionFlags += 4;\n                if (topLeft && tileData.canConnectTiles(setNum, tileSetMap.get(topLeft)))\n                    connectionFlags += 8;\n                if (connectionFlags > 0) {\n                    const clatSet = tileData.getCLATSet(setNum);\n                    tile.tileNum = tileData.getTileNumFromSet(clatSet, connectionFlags);\n                }\n            }\n            else if (setNum === tileData.getGeneralValue(\"RampBase\") &&\n                tile.rampType >= 1 && tile.rampType <= 4) {\n                let smoothFlags = -1;\n                const topRight = tiles.getNeighbourTile(tile, TileDirection.TopRight);\n                const bottomRight = tiles.getNeighbourTile(tile, TileDirection.BottomRight);\n                const bottomLeft = tiles.getNeighbourTile(tile, TileDirection.BottomLeft);\n                const topLeft = tiles.getNeighbourTile(tile, TileDirection.TopLeft);\n                switch (tile.rampType) {\n                    case 1:\n                        if (topLeft && topLeft.rampType === 0)\n                            smoothFlags++;\n                        if (bottomRight && bottomRight.rampType === 0)\n                            smoothFlags += 2;\n                        break;\n                    case 2:\n                        if (topRight && topRight.rampType === 0)\n                            smoothFlags++;\n                        if (bottomLeft && bottomLeft.rampType === 0)\n                            smoothFlags += 2;\n                        break;\n                    case 3:\n                        if (bottomRight && bottomRight.rampType === 0)\n                            smoothFlags++;\n                        if (topLeft && topLeft.rampType === 0)\n                            smoothFlags += 2;\n                        break;\n                    case 4:\n                        if (bottomLeft && bottomLeft.rampType === 0)\n                            smoothFlags++;\n                        if (topRight && topRight.rampType === 0)\n                            smoothFlags += 2;\n                        break;\n                }\n                if (smoothFlags !== -1) {\n                    tile.tileNum = tileData.getTileNumFromSet(tileData.getGeneralValue(\"RampSmooth\"), 3 * (tile.rampType - 1) + smoothFlags);\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/game/theater/TileSet.ts",
    "content": "import type { TileSetEntry } from './TileSetEntry';\nexport class TileSet {\n    public fileName: string;\n    public setName: string;\n    public tilesInSet: number;\n    public entries: TileSetEntry[] = [];\n    constructor(fileName: string, setName: string, tilesInSet: number) {\n        this.fileName = fileName;\n        this.setName = setName;\n        this.tilesInSet = tilesInSet;\n    }\n    getEntry(indexInSet: number): TileSetEntry | undefined {\n        return this.entries[indexInSet];\n    }\n}\n"
  },
  {
    "path": "src/game/theater/TileSetAnim.ts",
    "content": "export class TileSetAnim {\n    public name: string;\n    public subTile: number;\n    public offsetX: number;\n    public offsetY: number;\n    constructor(name: string, subTile: number, offsetX: number, offsetY: number) {\n        this.name = name;\n        this.subTile = subTile;\n        this.offsetX = offsetX;\n        this.offsetY = offsetY;\n    }\n}\n"
  },
  {
    "path": "src/game/theater/TileSetEntry.ts",
    "content": "import type { TileSet } from \"./TileSet\";\nimport type { TileSetAnim } from \"./TileSetAnim\";\nimport type { TmpFile } from \"../../data/TmpFile\";\nexport class TileSetEntry {\n    public owner: TileSet;\n    public index: number;\n    public files: TmpFile[] = [];\n    public animation?: TileSetAnim;\n    constructor(owner: TileSet, indexInSet: number) {\n        this.owner = owner;\n        this.index = indexInSet;\n    }\n    addFile(file: TmpFile): void {\n        this.files.push(file);\n    }\n    setAnimation(animation: TileSetAnim): void {\n        this.animation = animation;\n    }\n    getAnimation(): TileSetAnim | undefined {\n        return this.animation;\n    }\n    getTmpFile(subTileIndex: number, randomIndexSelector: (min: number, max: number) => number, preferNonDamaged: boolean = false): TmpFile | undefined {\n        if (this.files.length > 0) {\n            const selectedFileIndex = randomIndexSelector(0, this.files.length - 1);\n            let fileToReturn = this.files[selectedFileIndex];\n            if (preferNonDamaged &&\n                fileToReturn &&\n                subTileIndex < fileToReturn.images.length &&\n                (fileToReturn.images[subTileIndex] as any).hasDamagedData) {\n                const fallbackIndex = Math.min(preferNonDamaged ? 1 : 0, this.files.length - 1);\n                return this.files[fallbackIndex];\n            }\n            return fileToReturn;\n        }\n        return undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/theater/TileSets.ts",
    "content": "import * as stringUtils from '../../util/string';\nimport { TileSetEntry } from './TileSetEntry';\nimport { TileSet } from './TileSet';\nimport { TileSetAnim } from './TileSetAnim';\nimport type { IniFile } from '../../data/IniFile';\nimport type { TmpFile } from '../../data/TmpFile';\nexport enum HighBridgeHeadType {\n    TopLeft = 0,\n    BottomRight = 1,\n    TopRight = 2,\n    BottomLeft = 3,\n    MiddleTlBr = 4,\n    MiddleTrBl = 5\n}\nconst highBridgeHeadMapping = new Map<HighBridgeHeadType, string[]>([\n    [HighBridgeHeadType.TopLeft, ['BridgeTopLeft1', 'BridgeTopLeft2']],\n    [HighBridgeHeadType.BottomRight, ['BridgeBottomRight1', 'BridgeBottomRight2']],\n    [HighBridgeHeadType.TopRight, ['BridgeTopRight1', 'BridgeTopRight2']],\n    [HighBridgeHeadType.BottomLeft, ['BridgeBottomLeft1', 'BridgeBottomLeft2']],\n    [HighBridgeHeadType.MiddleTlBr, ['BridgeMiddle1']],\n    [HighBridgeHeadType.MiddleTrBl, ['BridgeMiddle2']],\n]);\nexport class TileSets {\n    private theaterIni: IniFile;\n    private tileSets: TileSet[] = [];\n    private orderedEntries: TileSetEntry[] = [];\n    private highBridgeSetNums: number[];\n    private cliffSetNums: number[];\n    constructor(theaterIni: IniFile) {\n        this.theaterIni = theaterIni;\n        this.highBridgeSetNums = [\n            this.getGeneralValue('BridgeSet'),\n            this.getGeneralValue('WoodBridgeSet'),\n        ];\n        this.cliffSetNums = [\n            this.getGeneralValue('CliffSet'),\n            this.getGeneralValue('WaterCliffs'),\n            this.getGeneralValue('DestroyableCliffs'),\n        ];\n    }\n    public getTile(tileNum: number): TileSetEntry | undefined {\n        return this.orderedEntries[tileNum];\n    }\n    public getTileImage(tileNum: number, subTile: number, randomIndexSelector: (min: number, max: number) => number): unknown {\n        const tileEntry = this.getTile(tileNum);\n        if (!tileEntry) {\n            throw new Error(`TileNum ${tileNum} not found`);\n        }\n        const tmpFile = tileEntry.getTmpFile(subTile, randomIndexSelector);\n        if (!tmpFile || subTile >= tmpFile.images.length) {\n            throw new Error(`SubTile ${subTile} not found`);\n        }\n        return tmpFile.images[subTile];\n    }\n    public getSetNum(tileNum: number): number {\n        const tileEntry = this.orderedEntries[tileNum];\n        if (!tileEntry) {\n            throw new Error('Invalid tileNum ' + tileNum);\n        }\n        return this.tileSets.indexOf(tileEntry.owner);\n    }\n    public getTileNumFromSet(setIndex: number, tileIndexInSet = 0): number {\n        let totalTileCount = 0;\n        this.tileSets.some((set, currentIndex) => {\n            if (currentIndex === setIndex) {\n                totalTileCount += tileIndexInSet;\n                return true;\n            }\n            totalTileCount += set.entries.length;\n            return false;\n        });\n        return totalTileCount;\n    }\n    public getGeneralValue(key: string): number {\n        const generalSection = this.theaterIni.getSection('General');\n        if (!generalSection) {\n            throw new Error('Missing [General] section in theater ini');\n        }\n        return generalSection.getNumber(key);\n    }\n    public loadTileData(vfs: any, extension: string): void {\n        this.tileSets.length = 0;\n        this.orderedEntries.length = 0;\n        this.initTileSets(vfs, extension);\n        this.initAnimations();\n    }\n    public readMaxTileNum(): number {\n        let setIndex = 0;\n        let totalTiles = 0;\n        for (;;) {\n            const sectionName = 'TileSet' + stringUtils.pad(String(setIndex), '0000');\n            const section = this.theaterIni.getSection(sectionName);\n            if (!section) {\n                break;\n            }\n            setIndex++;\n            totalTiles += section.getNumber('TilesInSet');\n        }\n        return totalTiles;\n    }\n    private initTileSets(vfs: any, extension: string): void {\n        let setIndex = 0;\n        let section;\n        for (;;) {\n            const sectionName = 'TileSet' + stringUtils.pad(String(setIndex), '0000');\n            section = this.theaterIni.getSection(sectionName);\n            if (!section) {\n                break;\n            }\n            setIndex++;\n            const tileSet = new TileSet(section.getString('FileName'), section.getString('SetName'), section.getNumber('TilesInSet'));\n            this.tileSets.push(tileSet);\n            for (let i = 1; i <= tileSet.tilesInSet; i++) {\n                const entry = new TileSetEntry(tileSet, i - 1);\n                const charA = 'a'.charCodeAt(0);\n                for (let charCode = charA - 1; charCode <= 'z'.charCodeAt(0); charCode++) {\n                    if (!(charCode >= charA && tileSet.setName === 'Bridges')) {\n                        let fileName = tileSet.fileName + stringUtils.pad(String(i), '00');\n                        if (charCode >= charA) {\n                            fileName += String.fromCharCode(charCode);\n                        }\n                        fileName += extension;\n                        const fileData = vfs.get(fileName) as TmpFile | null;\n                        if (!fileData) {\n                            break;\n                        }\n                        entry.addFile(fileData);\n                    }\n                }\n                tileSet.entries.push(entry);\n                this.orderedEntries.push(entry);\n            }\n        }\n    }\n    private initAnimations(): void {\n        const orderedSections = this.theaterIni.getOrderedSections();\n        for (let i = this.tileSets.length; i < orderedSections.length; ++i) {\n            const section = orderedSections[i];\n            const tileSet = this.tileSets.find((ts) => ts.setName === section.name);\n            if (tileSet) {\n                for (let j = 1; j <= tileSet.tilesInSet; ++j) {\n                    const tileKey = 'Tile' + stringUtils.pad(String(j), '00');\n                    const animKey = tileKey + 'Anim';\n                    const animName = section.getString(animKey);\n                    if (animName) {\n                        const anim = new TileSetAnim(animName, section.getNumber(tileKey + 'AttachesTo'), section.getNumber(tileKey + 'XOffset'), section.getNumber(tileKey + 'YOffset'));\n                        tileSet.entries[j - 1].setAnimation(anim);\n                    }\n                    else {\n                        console.warn(`Missing anim \"${animKey}\" for tileset ` + tileSet.setName);\n                    }\n                }\n            }\n        }\n    }\n    public isLAT(setNum: number): boolean {\n        return (setNum === this.getGeneralValue('RoughTile') ||\n            setNum === this.getGeneralValue('SandTile') ||\n            setNum === this.getGeneralValue('GreenTile') ||\n            setNum === this.getGeneralValue('PaveTile'));\n    }\n    public isCLAT(setNum: number): boolean {\n        return (setNum === this.getGeneralValue('ClearToRoughLat') ||\n            setNum === this.getGeneralValue('ClearToSandLat') ||\n            setNum === this.getGeneralValue('ClearToGreenLat') ||\n            setNum === this.getGeneralValue('ClearToPaveLat'));\n    }\n    public getLAT(clatSetNum: number): number {\n        if (clatSetNum === this.getGeneralValue('ClearToRoughLat')) {\n            return this.getGeneralValue('RoughTile');\n        }\n        if (clatSetNum === this.getGeneralValue('ClearToSandLat')) {\n            return this.getGeneralValue('SandTile');\n        }\n        if (clatSetNum === this.getGeneralValue('ClearToGreenLat')) {\n            return this.getGeneralValue('GreenTile');\n        }\n        if (clatSetNum === this.getGeneralValue('ClearToPaveLat')) {\n            return this.getGeneralValue('PaveTile');\n        }\n        return -1;\n    }\n    public getCLATSet(latSetNum: number): number {\n        if (latSetNum === this.getGeneralValue('RoughTile')) {\n            return this.getGeneralValue('ClearToRoughLat');\n        }\n        if (latSetNum === this.getGeneralValue('SandTile')) {\n            return this.getGeneralValue('ClearToSandLat');\n        }\n        if (latSetNum === this.getGeneralValue('GreenTile')) {\n            return this.getGeneralValue('ClearToGreenLat');\n        }\n        if (latSetNum === this.getGeneralValue('PaveTile')) {\n            return this.getGeneralValue('ClearToPaveLat');\n        }\n        return -1;\n    }\n    public canConnectTiles(setNum1: number, setNum2: number): boolean {\n        if (setNum1 === setNum2)\n            return false;\n        const greenTile = this.getGeneralValue('GreenTile');\n        const paveTile = this.getGeneralValue('PaveTile');\n        const miscPaveTile = this.getGeneralValue('MiscPaveTile');\n        const shorePieces = this.getGeneralValue('ShorePieces');\n        const waterBridge = this.getGeneralValue('WaterBridge');\n        const pavedRoads = this.getGeneralValue('PavedRoads');\n        const medians = this.getGeneralValue('Medians');\n        return !((setNum1 === greenTile && setNum2 === shorePieces) ||\n            (setNum2 === greenTile && setNum1 === shorePieces) ||\n            (setNum1 === greenTile && setNum2 === waterBridge) ||\n            (setNum2 === greenTile && setNum1 === waterBridge) ||\n            (setNum1 === paveTile && setNum2 === pavedRoads) ||\n            (setNum2 === paveTile && setNum1 === pavedRoads) ||\n            (setNum1 === paveTile && setNum2 === miscPaveTile) ||\n            (setNum2 === paveTile && setNum1 === miscPaveTile) ||\n            (setNum1 === paveTile && setNum2 === medians) ||\n            (setNum2 === paveTile && setNum1 === medians));\n    }\n    public getHighBridgeHeadType(tileIndex: number): HighBridgeHeadType | undefined {\n        for (const [type, names] of highBridgeHeadMapping) {\n            for (const name of names) {\n                if (this.getGeneralValue(name) === tileIndex + 1) {\n                    return type;\n                }\n            }\n        }\n        return undefined;\n    }\n    public getOppositeHighBridgeHeadType(headType: HighBridgeHeadType): HighBridgeHeadType {\n        switch (headType) {\n            case HighBridgeHeadType.TopLeft:\n                return HighBridgeHeadType.BottomRight;\n            case HighBridgeHeadType.TopRight:\n                return HighBridgeHeadType.BottomLeft;\n            case HighBridgeHeadType.BottomLeft:\n                return HighBridgeHeadType.TopRight;\n            case HighBridgeHeadType.BottomRight:\n                return HighBridgeHeadType.TopLeft;\n            case HighBridgeHeadType.MiddleTlBr:\n            case HighBridgeHeadType.MiddleTrBl:\n                throw new Error('Middle bridge heads can\\'t have opposites');\n            default:\n                throw new Error(`Unhandled headType ${headType}`);\n        }\n    }\n    public isCliffTile(tileNum: number): boolean {\n        return this.cliffSetNums.includes(this.getSetNum(tileNum));\n    }\n    public isHighBridgeBoundaryTile(tileNum: number): boolean {\n        if (this.highBridgeSetNums.includes(this.getSetNum(tileNum))) {\n            const tileEntry = this.getTile(tileNum);\n            if (!tileEntry)\n                return false;\n            const headType = this.getHighBridgeHeadType(tileEntry.index);\n            return (headType !== undefined &&\n                ![HighBridgeHeadType.MiddleTlBr, HighBridgeHeadType.MiddleTrBl].includes(headType));\n        }\n        return false;\n    }\n    public isHighBridgeMiddleTile(tileNum: number): boolean {\n        if (this.highBridgeSetNums.includes(this.getSetNum(tileNum))) {\n            const tileEntry = this.getTile(tileNum);\n            if (!tileEntry)\n                return false;\n            const headType = this.getHighBridgeHeadType(tileEntry.index);\n            return (headType !== undefined &&\n                [HighBridgeHeadType.MiddleTlBr, HighBridgeHeadType.MiddleTrBl].includes(headType));\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/game/theater/rampHeights.ts",
    "content": "export const rampHeights = [\n    [0, 0, 0, 0],\n    [0, 0, 1, 1],\n    [1, 0, 0, 1],\n    [1, 1, 0, 0],\n    [0, 1, 1, 0],\n    [0, 0, 0, 1],\n    [1, 0, 0, 0],\n    [0, 1, 0, 0],\n    [0, 0, 1, 0],\n    [1, 0, 1, 1],\n    [1, 1, 0, 1],\n    [1, 1, 1, 0],\n    [0, 1, 1, 1],\n    [1, 0, 1, 2],\n    [2, 1, 0, 1],\n    [1, 2, 1, 0],\n    [0, 1, 2, 1],\n    [1, 0, 1, 0],\n    [0, 1, 0, 1],\n    [1, 0, 1, 0],\n    [0, 1, 0, 1],\n];\n"
  },
  {
    "path": "src/game/trait/CrateGeneratorTrait.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { TerrainType } from \"@/engine/type/TerrainType\";\nimport { CratePickupEvent } from \"@/game/event/CratePickupEvent\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { OBS_COUNTRY_ID } from \"@/game/gameopts/constants\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nimport { RadialTileFinder } from \"@/game/map/tileFinder/RadialTileFinder\";\nimport { PowerupType } from \"@/game/type/PowerupType\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { NotifyTick } from \"@/game/trait/interface/NotifyTick\";\nimport { SuperWeaponType } from \"@/game/type/SuperWeaponType\";\nimport { SuperWeaponsTrait } from \"@/game/trait/SuperWeaponsTrait\";\nimport { CloakableTrait } from \"@/game/gameobject/trait/CloakableTrait\";\nimport { Warhead } from \"@/game/Warhead\";\nimport { CollisionType } from \"@/game/gameobject/unit/CollisionType\";\nimport { RandomTileFinder } from \"@/game/map/tileFinder/RandomTileFinder\";\nimport { OreSpread } from \"@/game/map/OreSpread\";\nimport { OverlayTibType } from \"@/engine/type/OverlayTibType\";\nimport { TiberiumTrait } from \"@/game/gameobject/trait/TiberiumTrait\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { Box2 } from \"@/game/math/Box2\";\nexport const UNSUPPORTED_POWERUP_TYPES = [\n    PowerupType.IonStorm,\n    PowerupType.Gas,\n    PowerupType.Pod,\n    PowerupType.Squad,\n];\ninterface CrateInfo {\n    obj: any;\n    powerup: any;\n    ticksLeft: number;\n}\nexport class CrateGeneratorTrait implements NotifyTick {\n    private randomCrateSpawn: boolean;\n    private crates: CrateInfo[] = [];\n    private availEdgeTiles: any[] = [];\n    private allTiles: any[] = [];\n    private mapEdgeIsWater: boolean = false;\n    private minCrates: number = 0;\n    constructor(randomCrateSpawn: boolean) {\n        this.randomCrateSpawn = randomCrateSpawn;\n        this.crates = [];\n        this.availEdgeTiles = [];\n        this.allTiles = [];\n    }\n    init(gameState: any): void {\n        const mapSize = gameState.map.tiles.getMapSize();\n        const tiles = gameState.map.tiles;\n        const edgeTiles: any[] = [];\n        let landEdgeCount = 0;\n        for (let x = 0; x < mapSize.width; ++x) {\n            let firstTile: any = null;\n            let lastTile: any = null;\n            let hasWaterStart = false;\n            let hasWaterEnd = false;\n            for (let y = 0; y < mapSize.height; ++y) {\n                const tile = tiles.getByMapCoords(x, y);\n                if (tile && this.canPlaceCrateOnTile(gameState, tile)) {\n                    const isWater = gameState.map.getTileZone(tile) === ZoneType.Water;\n                    if (!firstTile) {\n                        if (isWater) {\n                            hasWaterStart = hasWaterEnd = true;\n                        }\n                        else {\n                            firstTile = lastTile = tile;\n                        }\n                    }\n                    else {\n                        if (!isWater) {\n                            lastTile = tile;\n                        }\n                        hasWaterEnd = isWater;\n                    }\n                }\n                else if (firstTile && !tile)\n                    break;\n            }\n            if (firstTile) {\n                edgeTiles.push(firstTile);\n                if (lastTile && lastTile !== firstTile) {\n                    edgeTiles.push(lastTile);\n                }\n                if (!hasWaterStart && !hasWaterEnd) {\n                    landEdgeCount++;\n                }\n            }\n        }\n        this.availEdgeTiles = edgeTiles;\n        this.allTiles = tiles.getAll();\n        this.mapEdgeIsWater = landEdgeCount === 0;\n        this.minCrates = gameState.rules.crateRules.crateMinimum *\n            gameState.gameOpts.humanPlayers.filter((player: any) => player.countryId !== OBS_COUNTRY_ID).length;\n    }\n    [NotifyTick.onTick](gameState: any): void {\n        for (const crate of this.crates) {\n            crate.ticksLeft--;\n            if (crate.ticksLeft <= 0) {\n                gameState.unspawnObject(crate.obj);\n                crate.obj.dispose();\n            }\n        }\n        this.crates = this.crates.filter(crate => crate.ticksLeft > 0);\n        if (this.randomCrateSpawn) {\n            for (let i = 0; i < this.minCrates - this.crates.length && this.spawnCrateAtRandom(this.allTiles, gameState); i++)\n                ;\n        }\n    }\n    spawnCrateAtRandom(tiles: any[], gameState: any): boolean {\n        const spawnTile = this.chooseSpawnTile(tiles, gameState);\n        if (spawnTile) {\n            return this.spawnRandomCrateAt(spawnTile, gameState);\n        }\n        return false;\n    }\n    spawnRandomCrateAt(tile: any, gameState: any): boolean {\n        if (this.canPlaceCrateOnTile(gameState, tile)) {\n            const isWater = gameState.map.getTileZone(tile, true) === ZoneType.Water;\n            const powerup = this.choosePowerup(isWater, gameState.rules.powerups.powerups, gameState);\n            if (powerup) {\n                return !!this.spawnCrateAt(tile, powerup, gameState);\n            }\n        }\n        return false;\n    }\n    spawnCrateAt(tile: any, powerup: any, gameState: any): any {\n        if (this.canPlaceCrateOnTile(gameState, tile)) {\n            const isWater = gameState.map.getTileZone(tile, true) === ZoneType.Water;\n            const crateRules = gameState.rules.crateRules;\n            const crateImage = isWater ? crateRules.waterCrateImg : crateRules.crateImg;\n            const crateObject = gameState.createObject(ObjectType.Overlay, crateImage);\n            crateObject.overlayId = gameState.rules.getOverlayId(crateImage);\n            crateObject.value = 0;\n            gameState.spawnObject(crateObject, tile);\n            const ticksLeft = 60 * crateRules.crateRegen * GameSpeed.BASE_TICKS_PER_SECOND *\n                (0.5 + 1.5 * gameState.generateRandom());\n            this.crates.push({\n                obj: crateObject,\n                powerup: powerup,\n                ticksLeft: ticksLeft\n            });\n            return crateObject;\n        }\n        return null;\n    }\n    chooseSpawnTile(tiles: any[], gameState: any): any {\n        let tilesToUse = tiles;\n        if (gameState.generateRandom() < (this.mapEdgeIsWater ? 1 / 3 : 2 / 3) && this.availEdgeTiles.length) {\n            tilesToUse = this.availEdgeTiles;\n        }\n        return this.chooseRandomTile(tilesToUse, gameState);\n    }\n    chooseRandomTile(tiles: any[], gameState: any): any {\n        let selectedTile: any;\n        let attempts = 0;\n        do {\n            selectedTile = tiles[gameState.generateRandomInt(0, tiles.length - 1)];\n            attempts++;\n        } while (attempts < 100 && !this.canPlaceCrateOnTile(gameState, selectedTile));\n        if (attempts >= 100) {\n            const emptyTiles = gameState.map.tileOccupation.getEmptyTiles();\n            if (!emptyTiles.length) {\n                return null;\n            }\n            selectedTile = emptyTiles[gameState.generateRandomInt(0, emptyTiles.length - 1)];\n        }\n        return selectedTile;\n    }\n    canPlaceCrateOnTile(gameState: any, tile: any): boolean {\n        return gameState.map.mapBounds.isWithinBounds(tile) &&\n            !gameState.map.getGroundObjectsOnTile(tile).length &&\n            gameState.map.terrain.getPassableSpeed(tile, SpeedType.Amphibious, false, false) > 0 &&\n            tile.terrainType !== TerrainType.Shore &&\n            tile.rampType === 0;\n    }\n    choosePowerup(isWater: boolean, powerups: any[], gameState: any): any {\n        const availablePowerups = isWater ?\n            powerups.filter(p => p.waterAllowed) :\n            powerups;\n        if (!availablePowerups.length) {\n            return null;\n        }\n        const totalShares = availablePowerups.reduce((sum, powerup) => sum + powerup.probShares, 0);\n        const randomValue = gameState.generateRandomInt(0, totalShares);\n        let currentSum = 0;\n        for (const powerup of availablePowerups) {\n            currentSum += powerup.probShares;\n            if (randomValue < currentSum) {\n                return powerup;\n            }\n        }\n        return null;\n    }\n    peekInsideCrate(crateObject: any): PowerupType | undefined {\n        return this.crates.find(crate => crate.obj === crateObject)?.powerup.type;\n    }\n    pickupCrate(unit: any, crateObject: any, gameState: any): PowerupType | undefined {\n        const crateInfo = this.crates.find(crate => crate.obj === crateObject);\n        if (!crateInfo) {\n            return undefined;\n        }\n        this.crates.splice(this.crates.indexOf(crateInfo), 1);\n        gameState.unspawnObject(crateInfo.obj);\n        crateInfo.obj.dispose();\n        const powerupType = this.grantPowerup(unit, crateInfo.powerup, crateInfo.obj.tile, gameState);\n        if (powerupType !== undefined) {\n            unit.owner.cratesPickedUp++;\n            const powerupRule = gameState.rules.powerups.powerups.find((p: any) => p.type === powerupType);\n            gameState.events.dispatch(new CratePickupEvent(powerupRule, unit.owner, unit, crateInfo.obj.tile));\n        }\n        if (this.randomCrateSpawn) {\n            this.spawnCrateAtRandom(this.allTiles, gameState);\n        }\n        return powerupType;\n    }\n    grantPowerup(unit: any, powerup: any, tile: any, gameState: any): PowerupType | undefined {\n        const player = unit.owner;\n        let success = false;\n        if (!player.isCombatant()) {\n            return undefined;\n        }\n        switch (powerup.type) {\n            case PowerupType.Unit:\n                success = this.grantUnitPowerup(unit, player, tile, gameState);\n                break;\n            case PowerupType.Money:\n                success = this.grantMoneyPowerup(powerup, player, gameState);\n                break;\n            case PowerupType.HealBase:\n                success = this.grantHealBasePowerup(player, gameState);\n                break;\n            case PowerupType.Reveal:\n                gameState.mapShroudTrait.revealMap(player, gameState);\n                success = true;\n                break;\n            case PowerupType.Darkness:\n                gameState.mapShroudTrait.resetShroud(player, gameState);\n                success = true;\n                break;\n            case PowerupType.Veteran:\n                success = this.grantVeteranPowerup(unit, powerup, tile, gameState, player);\n                break;\n            case PowerupType.Armor:\n                success = this.grantArmorPowerup(unit, powerup, tile, gameState, player);\n                break;\n            case PowerupType.Firepower:\n                success = this.grantFirepowerPowerup(unit, powerup, tile, gameState, player);\n                break;\n            case PowerupType.Speed:\n                success = this.grantSpeedPowerup(unit, powerup, tile, gameState, player);\n                break;\n            case PowerupType.Cloak:\n                success = this.grantCloakPowerup(unit, tile, gameState, player);\n                break;\n            case PowerupType.ICBM:\n                success = this.grantICBMPowerup(player, gameState);\n                break;\n            case PowerupType.Invulnerability:\n                success = this.grantInvulnerabilityPowerup(player, tile, gameState);\n                break;\n            case PowerupType.Explosion:\n            case PowerupType.Napalm:\n                success = this.grantExplosionPowerup(unit, powerup, tile, gameState);\n                break;\n            case PowerupType.Tiberium:\n                success = this.grantTiberiumPowerup(tile, gameState);\n                break;\n            default:\n                console.warn(`Unhandled powerup type \"${PowerupType[powerup.type]}\"`);\n                return undefined;\n        }\n        if (success) {\n            return powerup.type;\n        }\n        const moneyPowerup = gameState.rules.powerups.powerups.find((p: any) => p.type === PowerupType.Money && p.probShares > 0);\n        if (moneyPowerup) {\n            return this.grantPowerup(unit, moneyPowerup, tile, gameState);\n        }\n        return undefined;\n    }\n    private grantUnitPowerup(unit: any, player: any, tile: any, gameState: any): boolean {\n        let unitRule: any = null;\n        const hasConstructionYard = [...player.buildings].some((building: any) => building.rules.constructionYard);\n        if (!hasConstructionYard && gameState.rules.crateRules.freeMCV) {\n            const baseUnits = gameState.rules.general.baseUnit;\n            const hasBaseUnit = player.getOwnedObjects(true).some((obj: any) => baseUnits.includes(obj.name));\n            if (!hasBaseUnit) {\n                const requiredCost = [\n                    ...gameState.rules.ai.buildPower,\n                    ...gameState.rules.ai.buildRefinery,\n                ]\n                    .map((name: string) => gameState.rules.getBuilding(name))\n                    .filter((building: any) => building.aiBasePlanningSide === player.country.side)\n                    .reduce((sum: number, building: any) => sum + building.cost, 0);\n                if (player.credits >= requiredCost) {\n                    const suitableMCV = baseUnits.find((unitName: string) => {\n                        const rule = gameState.rules.getObject(unitName, ObjectType.Vehicle);\n                        return rule.isAvailableTo(player.country) && rule.hasOwner(player.country);\n                    });\n                    if (!suitableMCV) {\n                        throw new Error(`No suitable MCV found for player country ${player.country?.name}`);\n                    }\n                    unitRule = gameState.rules.getObject(suitableMCV, ObjectType.Vehicle);\n                }\n            }\n        }\n        if (!unitRule) {\n            const crateUnitType = gameState.rules.crateRules.unitCrateType;\n            let availableUnits: any[] = [];\n            if (crateUnitType) {\n                if (gameState.rules.hasObject(crateUnitType, ObjectType.Vehicle)) {\n                    availableUnits = [gameState.rules.getObject(crateUnitType, ObjectType.Vehicle)];\n                }\n            }\n            else {\n                availableUnits = [...gameState.rules.vehicleRules.values()].filter((rule: any) => rule.crateGoodie &&\n                    gameState.map.terrain.getPassableSpeed(tile, rule.speedType, false, false) > 0);\n            }\n            if (availableUnits.length) {\n                unitRule = availableUnits[gameState.generateRandomInt(0, availableUnits.length - 1)];\n            }\n        }\n        if (!unitRule) {\n            return false;\n        }\n        const newUnit = gameState.createUnitForPlayer(unitRule, player);\n        const tileFinder = new RadialTileFinder(gameState.map.tiles, gameState.map.mapBounds, tile, { width: 1, height: 1 }, 0, 3, (testTile: any) => gameState.map.terrain.getPassableSpeed(testTile, newUnit.rules.speedType, newUnit.isInfantry(), false) > 0 &&\n            !gameState.map.terrain.findObstacles({ tile: testTile, onBridge: undefined }, newUnit).length);\n        const spawnTile = tileFinder.getNextTile();\n        if (spawnTile) {\n            gameState.spawnObject(newUnit, spawnTile);\n            return true;\n        }\n        else {\n            player.removeOwnedObject(newUnit);\n            newUnit.dispose();\n            return false;\n        }\n    }\n    private grantMoneyPowerup(powerup: any, player: any, gameState: any): boolean {\n        if (!powerup.data) {\n            throw new Error(\"Money powerup missing data field\");\n        }\n        const amount = Math.floor(Number(powerup.data) * (0.55 + 2 * gameState.generateRandom() * 0.45));\n        player.credits = Math.max(0, player.credits + amount);\n        return true;\n    }\n    private grantHealBasePowerup(player: any, gameState: any): boolean {\n        for (const obj of player.getOwnedObjects(true)) {\n            if (!obj.isDestroyed) {\n                obj.healthTrait.healToFull(undefined, gameState);\n            }\n        }\n        return true;\n    }\n    private grantVeteranPowerup(unit: any, powerup: any, tile: any, gameState: any, player: any): boolean {\n        if (unit.veteranTrait && !unit.veteranTrait.isMaxLevel()) {\n            const promotionLevel = Number(powerup.data);\n            for (const targetUnit of this.getUnitsInCrateRadius(gameState, tile, player)) {\n                targetUnit.veteranTrait?.promote(promotionLevel, gameState);\n            }\n            return true;\n        }\n        return false;\n    }\n    private grantArmorPowerup(unit: any, powerup: any, tile: any, gameState: any, player: any): boolean {\n        if (unit.crateBonuses.armor === 1) {\n            const armorBonus = Number(powerup.data);\n            for (const targetUnit of this.getUnitsInCrateRadius(gameState, tile, player)) {\n                if (targetUnit.crateBonuses.armor === 1) {\n                    targetUnit.crateBonuses.armor = armorBonus;\n                }\n            }\n            return true;\n        }\n        return false;\n    }\n    private grantFirepowerPowerup(unit: any, powerup: any, tile: any, gameState: any, player: any): boolean {\n        if (unit.crateBonuses.firepower === 1) {\n            const firepowerBonus = Number(powerup.data);\n            for (const targetUnit of this.getUnitsInCrateRadius(gameState, tile, player)) {\n                if (targetUnit.crateBonuses.firepower === 1) {\n                    targetUnit.crateBonuses.firepower = firepowerBonus;\n                }\n            }\n            return true;\n        }\n        return false;\n    }\n    private grantSpeedPowerup(unit: any, powerup: any, tile: any, gameState: any, player: any): boolean {\n        if (unit.crateBonuses.speed === 1) {\n            const speedBonus = Number(powerup.data);\n            for (const targetUnit of this.getUnitsInCrateRadius(gameState, tile, player)) {\n                if (targetUnit.crateBonuses.speed === 1) {\n                    targetUnit.crateBonuses.speed = speedBonus;\n                }\n            }\n            return true;\n        }\n        return false;\n    }\n    private grantCloakPowerup(unit: any, tile: any, gameState: any, player: any): boolean {\n        if (!unit.cloakableTrait) {\n            for (const targetUnit of this.getUnitsInCrateRadius(gameState, tile, player)) {\n                if (!targetUnit.cloakableTrait) {\n                    targetUnit.cloakableTrait = new CloakableTrait(targetUnit, gameState.rules.general.cloakDelay);\n                    gameState.addObjectTrait(targetUnit, targetUnit.cloakableTrait);\n                }\n            }\n            return true;\n        }\n        return false;\n    }\n    private grantICBMPowerup(player: any, gameState: any): boolean {\n        const icbmRule = [...gameState.rules.superWeaponRules.values()].find((rule: any) => rule.type === SuperWeaponType.MultiMissile);\n        if (icbmRule && player.superWeaponsTrait && !player.superWeaponsTrait.has(icbmRule.name)) {\n            const superWeapon = gameState.createSuperWeapon(icbmRule.name, player, true);\n            superWeapon.isGift = true;\n            player.superWeaponsTrait.add(superWeapon);\n            return true;\n        }\n        return false;\n    }\n    private grantInvulnerabilityPowerup(player: any, tile: any, gameState: any): boolean {\n        const ironCurtainRule = [...gameState.rules.superWeaponRules.values()].find((rule: any) => rule.type === SuperWeaponType.IronCurtain);\n        if (ironCurtainRule) {\n            gameState.traits.get(SuperWeaponsTrait).activateEffect(ironCurtainRule, player, gameState, tile, undefined, true);\n            return true;\n        }\n        return false;\n    }\n    private grantExplosionPowerup(unit: any, powerup: any, tile: any, gameState: any): boolean {\n        const damage = Number(powerup.data);\n        const warheadType = powerup.type === PowerupType.Napalm ?\n            gameState.rules.combatDamage.flameDamage :\n            gameState.rules.combatDamage.c4Warhead;\n        const warhead = new Warhead(gameState.rules.getWarhead(warheadType));\n        warhead.detonate(gameState, damage, unit.tile, unit.tileElevation, unit.position.worldPosition, unit.zone, CollisionType.None, gameState.createTarget(unit, unit.tile), { player: unit.owner, weapon: undefined } as any, false, undefined, 0);\n        return true;\n    }\n    private grantTiberiumPowerup(tile: any, gameState: any): boolean {\n        const tileFinder = new RandomTileFinder(gameState.map.tiles, gameState.map.mapBounds, tile, 2, gameState, (testTile: any) => TiberiumTrait.canBePlacedOn(testTile, gameState.map));\n        let success = false;\n        let attempts = 0;\n        let targetTile: any;\n        while (attempts++ < 6 && (targetTile = tileFinder.getNextTile())) {\n            const overlayId = OreSpread.calculateOverlayId(OverlayTibType.Ore, targetTile);\n            if (overlayId === undefined) {\n                throw new Error(\"Expected an overlayId\");\n            }\n            const overlayObject = gameState.createObject(ObjectType.Overlay, gameState.rules.getOverlayName(overlayId));\n            overlayObject.overlayId = overlayId;\n            overlayObject.value = 3;\n            gameState.spawnObject(overlayObject, targetTile);\n            success = true;\n        }\n        return success;\n    }\n    private getUnitsInCrateRadius(gameState: any, tile: any, player: any): any[] {\n        const radius = gameState.rules.crateRules.crateRadius;\n        const rangeHelper = new RangeHelper(gameState.map.tileOccupation);\n        return gameState.map.technosByTile\n            .queryRange(new Box2().setFromCenterAndSize(new Vector2(tile.rx, tile.ry), new Vector2(radius, radius)))\n            .filter((unit: any) => unit.owner === player &&\n            unit.isUnit() &&\n            rangeHelper.tileDistance(unit, tile) <= radius);\n    }\n}\n"
  },
  {
    "path": "src/game/trait/MapLightingTrait.ts",
    "content": "import { MapLighting } from '@/data/map/MapLighting';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { EventDispatcher } from '@/util/event';\nimport { NotifyTick } from '@/game/trait/interface/NotifyTick';\nexport class MapLightingTrait {\n    private mapLighting: MapLighting;\n    private _onChange: EventDispatcher;\n    private ambientChangeRate: number;\n    private ambientChangeStep: number;\n    private targetAmbient?: number;\n    private ambientUpdateTicks?: number;\n    get onChange() {\n        return this._onChange.asEvent();\n    }\n    constructor(config: {\n        ambientChangeRate: number;\n        ambientChangeStep: number;\n    }, initialLighting?: MapLighting) {\n        this.mapLighting = new MapLighting();\n        this._onChange = new EventDispatcher();\n        this.ambientChangeRate = config.ambientChangeRate;\n        this.ambientChangeStep = config.ambientChangeStep;\n        if (initialLighting) {\n            this.mapLighting.copy(initialLighting);\n        }\n    }\n    setAmbientChangeRate(rate: number): void {\n        this.ambientChangeRate = rate;\n    }\n    setAmbientChangeStep(step: number): void {\n        this.ambientChangeStep = step;\n    }\n    setTargetAmbientIntensity(intensity: number): void {\n        this.targetAmbient = intensity;\n    }\n    getAmbient(): MapLighting {\n        return this.mapLighting;\n    }\n    [NotifyTick.onTick](): void {\n        if (this.targetAmbient === undefined) {\n            return;\n        }\n        if (this.ambientUpdateTicks === undefined) {\n            this.ambientUpdateTicks = Math.floor(60 * GameSpeed.BASE_TICKS_PER_SECOND * this.ambientChangeRate);\n        }\n        if (this.ambientUpdateTicks <= 0) {\n            this.ambientUpdateTicks = undefined;\n            const currentAmbient = this.mapLighting.ambient;\n            const diff = this.targetAmbient - currentAmbient;\n            if (diff !== 0) {\n                const step = Math.sign(diff) * Math.min(this.ambientChangeStep, Math.abs(diff));\n                this.mapLighting.ambient += step;\n                this._onChange.dispatch(this, this.mapLighting);\n            }\n            else {\n                this.targetAmbient = undefined;\n            }\n        }\n        else {\n            this.ambientUpdateTicks--;\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trait/MapRadiationTrait.ts",
    "content": "import { StanceType } from '../gameobject/infantry/StanceType';\nimport { RangeHelper } from '../gameobject/unit/RangeHelper';\nimport { RadialTileFinder } from '../map/tileFinder/RadialTileFinder';\nimport { Warhead } from '../Warhead';\nimport { EventDispatcher } from '../../util/event';\nimport { lerp } from '../../util/math';\nimport { NotifyTick } from './interface/NotifyTick';\nexport class MapRadiationTrait {\n    private map: any;\n    private radSites: Map<any, {\n        radLevel: number;\n        radius: number;\n    }>;\n    private radLevelByTile: Map<any, number>;\n    private _onChange: EventDispatcher;\n    private nextDamage?: number;\n    private nextDecay?: number;\n    get onChange() {\n        return this._onChange.asEvent();\n    }\n    constructor(map: any) {\n        this.map = map;\n        this.radSites = new Map();\n        this.radLevelByTile = new Map();\n        this._onChange = new EventDispatcher();\n    }\n    getRadLevel(tile: any): number | undefined {\n        return this.radLevelByTile.get(tile);\n    }\n    [NotifyTick.onTick](game: any): void {\n        if (!this.radLevelByTile.size)\n            return;\n        const radiation = game.rules.radiation;\n        if (this.nextDamage === undefined) {\n            this.nextDamage = Math.max(0, radiation.radApplicationDelay - 1);\n        }\n        else if (this.nextDamage <= 0) {\n            this.applyDamage(game);\n            this.nextDamage = Math.max(0, radiation.radApplicationDelay);\n        }\n        else {\n            this.nextDamage--;\n        }\n        if (this.nextDecay === undefined) {\n            this.nextDecay = Math.max(0, radiation.radLevelDelay - 1);\n        }\n        else if (this.nextDecay <= 0) {\n            this.applyDecay(Math.ceil(radiation.radLevelDelay / radiation.radDurationMultiple));\n            this.nextDecay = this.radLevelByTile.size ? Math.max(0, radiation.radLevelDelay) : undefined;\n        }\n        else {\n            this.nextDecay--;\n        }\n    }\n    private applyDamage(game: any): void {\n        const radiation = game.rules.radiation;\n        const warhead = new Warhead(game.rules.getWarhead(radiation.radSiteWarhead));\n        this.radLevelByTile.forEach((level, tile) => {\n            const damage = Math.min(radiation.radLevelMax, level) * radiation.radLevelFactor;\n            for (const unit of game.map.getGroundObjectsOnTile(tile).filter((obj: any) => obj.isUnit() &&\n                !(obj.isInfantry() && obj.stance === StanceType.Paradrop && obj.tileElevation > 1))) {\n                if (warhead.canDamage(unit, tile, unit.zone)) {\n                    const computedDamage = warhead.computeDamage(damage, unit, game);\n                    if (computedDamage > 0) {\n                        warhead.inflictDamage(computedDamage, unit, undefined, game, true);\n                    }\n                }\n            }\n        });\n    }\n    private applyDecay(decayAmount: number): void {\n        const affectedTiles = new Set(this.radLevelByTile.keys());\n        this.radLevelByTile.clear();\n        this.radSites.forEach(({ radLevel, radius }, position) => {\n            const newLevel = radLevel - decayAmount;\n            if (newLevel <= 0) {\n                this.radSites.delete(position);\n            }\n            else {\n                this.radSites.set(position, { radLevel: newLevel, radius });\n                this.setRadLevelAround(position, radius, newLevel);\n            }\n        });\n        this._onChange.dispatch(this, affectedTiles);\n    }\n    createRadSite(position: any, level: number, radius: number): void {\n        const currentLevel = this.radSites.get(position)?.radLevel ?? 0;\n        const additionalLevel = level - currentLevel;\n        if (additionalLevel <= 0)\n            return;\n        this.radSites.set(position, {\n            radLevel: currentLevel + additionalLevel,\n            radius\n        });\n        const affectedTiles = this.setRadLevelAround(position, radius, additionalLevel);\n        if (affectedTiles.size) {\n            this._onChange.dispatch(this, affectedTiles);\n        }\n    }\n    private setRadLevelAround(position: any, radius: number, level: number): Set<any> {\n        const rangeHelper = new RangeHelper(this.map.tileOccupation);\n        const tileFinder = new RadialTileFinder(this.map.tiles, this.map.mapBounds, position, { width: 1, height: 1 }, 0, radius, (tile: any) => !!tile, false);\n        const affectedTiles = new Set<any>();\n        let tile;\n        while ((tile = tileFinder.getNextTile())) {\n            const distance = rangeHelper.tileDistance(position, tile);\n            if (distance <= radius) {\n                const radLevel = Math.ceil(lerp(level, 0, distance / radius));\n                this.radLevelByTile.set(tile, Math.min(1000, (this.radLevelByTile.get(tile) ?? 0) + radLevel));\n                affectedTiles.add(tile);\n            }\n        }\n        return affectedTiles;\n    }\n    getRadSiteLevel(position: any): number | undefined {\n        return this.radSites.get(position)?.radLevel;\n    }\n}\n"
  },
  {
    "path": "src/game/trait/MapShroudTrait.ts",
    "content": "import { MapShroud, ShroudFlag } from \"@/game/map/MapShroud\";\nimport { NotifyTick } from \"@/game/trait/interface/NotifyTick\";\nimport { NotifyOwnerChange } from \"@/game/trait/interface/NotifyOwnerChange\";\nimport { NotifyAllianceChange } from \"@/game/trait/interface/NotifyAllianceChange\";\nimport { isNotNullOrUndefined } from \"@/util/typeGuard\";\nimport { NotifySpawn } from \"@/game/trait/interface/NotifySpawn\";\nimport { NotifyUnspawn } from \"@/game/trait/interface/NotifyUnspawn\";\nimport { RadarTrait } from \"@/game/trait/RadarTrait\";\nimport { RadarEventType } from \"@/game/rules/general/RadarRules\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { NotifyPower } from \"@/game/trait/interface/NotifyPower\";\nimport { NotifyElevationChange } from \"@/game/trait/interface/NotifyElevationChange\";\nexport class MapShroudTrait implements NotifyTick, NotifyOwnerChange, NotifyAllianceChange, NotifySpawn, NotifyUnspawn, NotifyPower, NotifyElevationChange {\n    private map: any;\n    private alliances: any;\n    private shroudByPlayer: Map<any, any>;\n    private revealedToAll: Set<any>;\n    private gapGenerators: Set<any>;\n    private handleTileOccupationUpdate: (params: {\n        object: any;\n        type: string;\n    }) => void;\n    constructor(map: any, alliances: any) {\n        this.map = map;\n        this.alliances = alliances;\n        this.shroudByPlayer = new Map();\n        this.revealedToAll = new Set();\n        this.gapGenerators = new Set();\n        this.handleTileOccupationUpdate = ({ object: e, type: t }) => {\n            if (\"removed\" !== t && e.isTechno()) {\n                const r = e.owner;\n                for (const i of [r, ...this.alliances.getAllies(r)]) {\n                    this.shroudByPlayer.get(i)?.revealFrom(e);\n                }\n            }\n        };\n    }\n    getPlayerShroud(player: any) {\n        return this.shroudByPlayer.get(player);\n    }\n    init(gameState: any) {\n        gameState.map.tileOccupation.onChange.subscribe(this.handleTileOccupationUpdate);\n        const baseShroud = new MapShroud().fromTiles(this.map.tiles);\n        for (const combatant of gameState.getCombatants()) {\n            const playerShroud = baseShroud.clone();\n            this.shroudByPlayer.set(combatant, playerShroud);\n            this.revealObjects(playerShroud, combatant, gameState);\n            playerShroud.update();\n        }\n    }\n    [NotifyElevationChange.onElevationChange](object: any, gameState: any, previousElevation: number) {\n        if (Math.floor(object.tileElevation) !== Math.floor(previousElevation)) {\n            const owner = object.owner;\n            for (const ally of [owner, ...this.alliances.getAllies(owner)]) {\n                this.shroudByPlayer.get(ally)?.revealFrom(object);\n            }\n        }\n    }\n    [NotifyTick.onTick](gameState: any) {\n        for (const [player, shroud] of this.shroudByPlayer) {\n            if (player.defeated && !player.isObserver) {\n                this.shroudByPlayer.delete(player);\n            }\n            else {\n                shroud.update();\n            }\n        }\n    }\n    [NotifyOwnerChange.onChange](object: any, previousOwner: any, gameState: any) {\n        if (object.isBuilding() &&\n            object.rules.spySat &&\n            (this.revealMap(object.owner, gameState),\n                previousOwner\n                    .getOwnedObjectsByType(ObjectType.Building)\n                    .find((e: any) => e.rules.spySat) || this.resetShroud(previousOwner, gameState))) {\n        }\n        if (object.isSpawned) {\n            for (const ally of [object.owner, ...gameState.alliances.getAllies(object.owner)]) {\n                this.shroudByPlayer.get(ally)?.revealFrom(object);\n            }\n        }\n    }\n    [NotifyAllianceChange.onChange](alliance: any, isFormed: boolean, gameState: any) {\n        if (isFormed) {\n            const firstPlayerShroud = this.getPlayerShroud(alliance.players.first);\n            const alliedShrouds = gameState.alliances\n                .getAllies(alliance.players.first)\n                .map((player: any) => this.getPlayerShroud(player))\n                .filter(isNotNullOrUndefined);\n            for (const alliedShroud of alliedShrouds) {\n                firstPlayerShroud.merge(alliedShroud);\n            }\n            firstPlayerShroud.invalidateFull();\n            for (const alliedShroud of alliedShrouds) {\n                alliedShroud.copy(firstPlayerShroud);\n                alliedShroud.invalidateFull();\n            }\n        }\n    }\n    [NotifySpawn.onSpawn](object: any, gameState: any) {\n        if (object.isBuilding()) {\n            if (object.rules.spySat) {\n                this.revealMap(object.owner, gameState);\n            }\n            if (object.rules.revealToAll) {\n                this.revealedToAll.add(object);\n                for (const combatant of gameState.getCombatants()) {\n                    if (combatant === object.owner || gameState.alliances.areAllied(object.owner, combatant)) {\n                        continue;\n                    }\n                    this.shroudByPlayer.get(combatant)?.revealObject(object);\n                    gameState.traits\n                        .get(RadarTrait)\n                        .addEventForPlayer(RadarEventType.EnemyObjectSensed, combatant, object.centerTile, gameState);\n                }\n            }\n            if (object.gapGeneratorTrait) {\n                this.gapGenerators.add(object);\n            }\n        }\n    }\n    [NotifyUnspawn.onUnspawn](object: any, gameState: any) {\n        if (object.isBuilding()) {\n            if (object.rules.spySat &&\n                !object.owner\n                    .getOwnedObjectsByType(ObjectType.Building)\n                    .find((e: any) => e.rules.spySat)) {\n                this.resetShroud(object.owner, gameState);\n            }\n            if (object.rules.revealToAll) {\n                this.revealedToAll.delete(object);\n            }\n            if (object.gapGeneratorTrait) {\n                this.gapGenerators.delete(object);\n            }\n        }\n    }\n    [NotifyPower.onPowerLow](player: any, gameState: any) {\n        this.updateGaps(gameState, player);\n    }\n    [NotifyPower.onPowerRestore](player: any, gameState: any) {\n        this.updateGaps(gameState, player);\n    }\n    [NotifyPower.onPowerChange](player: any, gameState: any) { }\n    revealMap(player: any, gameState: any) {\n        this.shroudByPlayer.get(player)?.revealAll();\n        this.markOwnGapTiles(gameState, player);\n        this.updateGaps(gameState);\n    }\n    resetShroud(player: any, gameState: any) {\n        const shroud = this.shroudByPlayer.get(player);\n        if (shroud) {\n            shroud.reset();\n            this.markOwnGapTiles(gameState, player);\n            this.revealObjects(shroud, player, gameState);\n        }\n    }\n    revealObjects(shroud: any, player: any, gameState: any) {\n        const objectsToReveal = [\n            ...player.getOwnedObjects(),\n            ...gameState.alliances\n                .getAllies(player)\n                .map((ally: any) => ally.getOwnedObjects())\n                .flat(),\n        ];\n        for (const object of objectsToReveal) {\n            shroud.revealFrom(object);\n        }\n        this.revealedToAll.forEach((object) => shroud.revealObject(object));\n    }\n    updateGaps(gameState: any, specificPlayer?: any) {\n        for (const gapGenerator of this.gapGenerators) {\n            if (!specificPlayer || gapGenerator.owner === specificPlayer) {\n                gapGenerator.gapGeneratorTrait.update(gapGenerator, gameState);\n            }\n        }\n    }\n    markOwnGapTiles(gameState: any, player: any) {\n        for (const gapGenerator of this.gapGenerators) {\n            if (gapGenerator.owner === player || gameState.alliances.areAllied(gapGenerator.owner, player)) {\n                this.getPlayerShroud(player)?.toggleFlagsAround(gapGenerator.tile, gapGenerator.gapGeneratorTrait.radiusTiles, ShroudFlag.Darken, true);\n            }\n        }\n    }\n    dispose() {\n        this.map.tileOccupation.onChange.unsubscribe(this.handleTileOccupationUpdate);\n    }\n}\n"
  },
  {
    "path": "src/game/trait/PowerTrait.ts",
    "content": "import { NotifySpawn } from './interface/NotifySpawn';\nimport { NotifyHealthChange } from './interface/NotifyHealthChange';\nimport { NotifyUnspawn } from './interface/NotifyUnspawn';\nimport { NotifyOwnerChange } from './interface/NotifyOwnerChange';\nimport { NotifyWarpChange } from './interface/NotifyWarpChange';\nimport { NotifyTick } from './interface/NotifyTick';\nexport class PowerTrait {\n    [NotifySpawn.onSpawn](entity: any, timestamp: number): void {\n        if (entity.isTechno() &&\n            entity.rules.power &&\n            !this.isCapturablePower(entity, entity.owner)) {\n            entity.owner.powerTrait?.updateFrom(entity, \"add\", timestamp);\n        }\n    }\n    [NotifyUnspawn.onUnspawn](entity: any, timestamp: number): void {\n        if (entity.isTechno() &&\n            entity.rules.power &&\n            !entity.warpedOutTrait.isActive() &&\n            !this.isCapturablePower(entity, entity.owner)) {\n            entity.owner.powerTrait?.updateFrom(entity, \"remove\", timestamp);\n        }\n    }\n    [NotifyHealthChange.onChange](entity: any, timestamp: number): void {\n        if (entity.isTechno() &&\n            entity.rules.power &&\n            !entity.warpedOutTrait.isActive() &&\n            !this.isCapturablePower(entity, entity.owner)) {\n            entity.owner.powerTrait?.updateFrom(entity, \"update\", timestamp);\n        }\n    }\n    [NotifyOwnerChange.onChange](entity: any, oldOwner: any, timestamp: number): void {\n        if (entity.rules.power && !entity.warpedOutTrait.isActive()) {\n            if (!this.isCapturablePower(entity, oldOwner)) {\n                oldOwner.powerTrait?.updateFrom(entity, \"remove\", timestamp);\n            }\n            if (!this.isCapturablePower(entity, entity.owner)) {\n                entity.owner.powerTrait?.updateFrom(entity, \"add\", timestamp);\n            }\n        }\n    }\n    [NotifyWarpChange.onChange](entity: any, timestamp: number, isWarping: boolean): void {\n        if (entity.rules.power && !this.isCapturablePower(entity, entity.owner)) {\n            entity.owner.powerTrait?.updateFrom(entity, isWarping ? \"remove\" : \"add\", timestamp);\n        }\n    }\n    [NotifyTick.onTick](game: any): void {\n        for (const combatant of game.getCombatants()) {\n            combatant.powerTrait.updateBlackout(game);\n        }\n    }\n    private isCapturablePower(entity: any, owner: any): boolean {\n        return entity.rules.power > 0 && owner.isNeutral && entity.rules.needsEngineer;\n    }\n}\n"
  },
  {
    "path": "src/game/trait/ProductionTrait.ts",
    "content": "import { NotifyTick } from \"@/game/trait/interface/NotifyTick\";\nimport { NotifyUnspawn } from \"@/game/trait/interface/NotifyUnspawn\";\nimport { NotifyOwnerChange } from \"@/game/trait/interface/NotifyOwnerChange\";\nimport { ProductionQueue, QueueStatus } from \"@/game/player/production/ProductionQueue\";\nimport { InsufficientFundsEvent } from \"@/game/event/InsufficientFundsEvent\";\nimport { TechnoRules, FactoryType } from \"@/game/rules/TechnoRules\";\nimport { NotifySpawn } from \"@/game/trait/interface/NotifySpawn\";\nimport { NotifyPower } from \"@/game/trait/interface/NotifyPower\";\nimport { PowerTrait, PowerLevel } from \"@/game/player/trait/PowerTrait\";\nimport { clamp, floorTo } from \"@/util/math\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { GameMath } from \"@/game/math/GameMath\";\nexport class ProductionTrait implements NotifyTick, NotifySpawn, NotifyUnspawn, NotifyOwnerChange, NotifyPower {\n    private rules: any;\n    private speedCheat: any;\n    private availableObjectRules: Set<any>;\n    private baseBuildSpeed: number;\n    constructor(rules: any, speedCheat: any) {\n        if (!speedCheat || !('value' in speedCheat)) {\n            throw new Error('ProductionTrait requires a shared speedCheat BoxedVar');\n        }\n        this.rules = rules;\n        this.speedCheat = speedCheat;\n        this.availableObjectRules = new Set();\n        const buildSpeedTicks = 60 * rules.general.buildSpeed * GameSpeed.BASE_TICKS_PER_SECOND;\n        this.baseBuildSpeed = 1 / (buildSpeedTicks / 1000);\n        [\n            ...rules.buildingRules.values(),\n            ...rules.infantryRules.values(),\n            ...rules.vehicleRules.values(),\n            ...rules.aircraftRules.values(),\n        ].forEach((rule) => {\n            if (rule.owner.length) {\n                this.availableObjectRules.add(rule);\n            }\n        });\n    }\n    [NotifyTick.onTick](gameState: any): void {\n        for (const combatant of gameState.getCombatants()) {\n            for (const queue of combatant.production.getAllQueues()) {\n                this.tickQueue(queue, combatant, gameState);\n            }\n        }\n    }\n    [NotifySpawn.onSpawn](entity: any, gameState: any): void {\n        if (entity.isBuilding() && entity.owner.production) {\n            const factoryType = entity.rules.factory;\n            if (factoryType) {\n                if (!entity.owner.production.getPrimaryFactory(factoryType)) {\n                    entity.owner.production.setPrimaryFactory(entity);\n                }\n                entity.owner.production.incrementFactoryCount(factoryType);\n                if (factoryType === FactoryType.AircraftType) {\n                    this.updateAircraftQueueMaxSize(entity.owner, gameState);\n                }\n            }\n        }\n        else if (entity.isAircraft() &&\n            entity.owner.production &&\n            this.rules.general.padAircraft.includes(entity.name)) {\n            this.updateAircraftQueueMaxSize(entity.owner, gameState);\n        }\n    }\n    [NotifyUnspawn.onUnspawn](entity: any, gameState: any): void {\n        if (entity.isBuilding() && entity.owner.production) {\n            this.ensurePrerequisites(entity.owner);\n            const factoryType = entity.rules.factory;\n            if (factoryType) {\n                if (entity.owner.production.getPrimaryFactory(factoryType) === entity) {\n                    entity.owner.production.crownPrimaryFactoryHeir(factoryType);\n                }\n                entity.owner.production.decrementFactoryCount(factoryType);\n                if (factoryType === FactoryType.AircraftType) {\n                    this.updateAircraftQueueMaxSize(entity.owner, gameState);\n                }\n            }\n        }\n        else if (entity.isAircraft() &&\n            entity.owner.production &&\n            this.rules.general.padAircraft.includes(entity.name)) {\n            this.updateAircraftQueueMaxSize(entity.owner, gameState);\n        }\n    }\n    [NotifyOwnerChange.onChange](entity: any, oldOwner: any, gameState: any): void {\n        if (entity.isBuilding()) {\n            this.ensurePrerequisites(oldOwner);\n            const factoryType = entity.rules.factory;\n            if (factoryType) {\n                if (oldOwner.production?.getPrimaryFactory(factoryType) === entity) {\n                    oldOwner.production.crownPrimaryFactoryHeir(factoryType);\n                }\n                if (entity.owner.production && !entity.owner.production.getPrimaryFactory(factoryType)) {\n                    entity.owner.production.setPrimaryFactory(entity);\n                }\n                oldOwner.production?.decrementFactoryCount(factoryType);\n                entity.owner.production?.incrementFactoryCount(factoryType);\n                if (factoryType === FactoryType.AircraftType) {\n                    this.updateAircraftQueueMaxSize(entity.owner, gameState);\n                    this.updateAircraftQueueMaxSize(oldOwner, gameState);\n                }\n            }\n        }\n        else if (entity.isAircraft() &&\n            this.rules.general.padAircraft.includes(entity.name)) {\n            this.updateAircraftQueueMaxSize(entity.owner, gameState);\n            this.updateAircraftQueueMaxSize(oldOwner, gameState);\n        }\n    }\n    [NotifyPower.onPowerLow](player: any): void {\n        if (player.production) {\n            player.production.buildSpeedModifier = this.computeLowPowerBuildSpeedModifier(player.powerTrait.power, player.powerTrait.drain);\n        }\n    }\n    [NotifyPower.onPowerRestore](player: any): void {\n        if (player.production) {\n            player.production.buildSpeedModifier = 1;\n        }\n    }\n    [NotifyPower.onPowerChange](player: any): void {\n        if (player.powerTrait?.level === PowerLevel.Low && player.production) {\n            player.production.buildSpeedModifier = this.computeLowPowerBuildSpeedModifier(player.powerTrait.power, player.powerTrait.drain);\n        }\n    }\n    private computeLowPowerBuildSpeedModifier(power: number, drain: number): number {\n        const powerRatio = 1 - Math.min(1, power / drain);\n        const generalRules = this.rules.general;\n        const penaltyModifier = (0.3 * generalRules.lowPowerPenaltyModifier * powerRatio) / 0.15;\n        return clamp(1 - penaltyModifier, generalRules.minLowPowerProductionSpeed, generalRules.maxLowPowerProductionSpeed);\n    }\n    private updateAircraftQueueMaxSize(player: any, gameState: any): void {\n        if (!player.production)\n            return;\n        gameState.afterTick(() => {\n            const helipadCapacity = [...player.buildings]\n                .filter(building => building.helipadTrait)\n                .reduce((total, building) => total + building.dockTrait.numberOfDocks, 0);\n            const currentAircraft = player\n                .getOwnedObjectsByType(ObjectType.Aircraft, true)\n                .filter(aircraft => gameState.rules.general.padAircraft.includes(aircraft.name))\n                .length;\n            const aircraftQueue = player.production.getQueueForFactory(FactoryType.AircraftType);\n            aircraftQueue.maxSize = Math.max(0, helipadCapacity - currentAircraft);\n        });\n    }\n    private tickQueue(queue: ProductionQueue, player: any, gameState: any): void {\n        if (queue.status !== QueueStatus.Active)\n            return;\n        let hasProgress = false;\n        const currentItem = queue.getFirst();\n        const factoryType = player.production.getFactoryTypeForQueueType(queue.type);\n        const factoryCount = player.production.getFactoryCount(factoryType);\n        const buildSpeedModifier = player.production.buildSpeedModifier;\n        const multipleFactoryPenalty = 1 / GameMath.pow(this.rules.general.multipleFactory, factoryCount - 1);\n        const wallSpeedModifier = currentItem.rules.wall\n            ? 1 / this.rules.general.wallBuildSpeedCoefficient\n            : 1;\n        const effectiveBuildSpeed = this.baseBuildSpeed * buildSpeedModifier * multipleFactoryPenalty * wallSpeedModifier;\n        const itemCost = currentItem.creditsEach;\n        const buildTime = itemCost && !this.speedCheat.value\n            ? floorTo((itemCost / effectiveBuildSpeed) * currentItem.rules.buildTimeMultiplier, 54)\n            : 54;\n        const finalBuildTime = Math.max(54, buildTime);\n        const playerCredits = player.credits;\n        const remainingCost = currentItem.creditsEach - currentItem.creditsSpent;\n        const affordableAmount = Math.min(player.credits, itemCost / finalBuildTime + currentItem.creditsSpentLeftover, remainingCost);\n        if (affordableAmount > 0) {\n            const spendAmount = Math.floor(affordableAmount);\n            currentItem.creditsSpentLeftover = affordableAmount - spendAmount;\n            if (spendAmount) {\n                currentItem.creditsSpent += spendAmount;\n                currentItem.progress = currentItem.creditsSpent / currentItem.creditsEach;\n                player.credits -= spendAmount;\n                hasProgress = true;\n            }\n        }\n        else if (!currentItem.creditsEach) {\n            const progressIncrement = currentItem.progress * finalBuildTime;\n            currentItem.progress = Math.min(1, (1 + progressIncrement) / finalBuildTime);\n            hasProgress = true;\n        }\n        if (hasProgress && currentItem.progress === 1) {\n            queue.status = QueueStatus.Ready;\n        }\n        if (playerCredits > 0 && !player.credits) {\n            gameState.events.dispatch(new InsufficientFundsEvent(player));\n        }\n        if (hasProgress) {\n            queue.notifyUpdated();\n        }\n    }\n    private ensurePrerequisites(player: any): void {\n        if (!player.production)\n            return;\n        for (const queue of player.production.getAllQueues()) {\n            const itemsToRemove = queue.getAll().map(item => ({\n                rules: item.rules,\n                quantity: item.quantity,\n                creditsSpent: item.creditsSpent\n            }));\n            for (const item of itemsToRemove) {\n                if (!player.production.isAvailableForProduction(item.rules)) {\n                    queue.pop(item.rules, item.quantity);\n                    player.credits += item.creditsSpent;\n                }\n            }\n        }\n    }\n    getAvailableObjects(): any[] {\n        return [...this.availableObjectRules];\n    }\n}\n"
  },
  {
    "path": "src/game/trait/RadarTrait.ts",
    "content": "import { NotifySpawn } from \"@/game/trait/interface/NotifySpawn\";\nimport { NotifyUnspawn } from \"@/game/trait/interface/NotifyUnspawn\";\nimport { NotifyPower } from \"@/game/trait/interface/NotifyPower\";\nimport { PowerTrait, PowerLevel } from \"@/game/player/trait/PowerTrait\";\nimport { RadarOnOffEvent } from \"@/game/event/RadarOnOffEvent\";\nimport { NotifyOwnerChange } from \"@/game/trait/interface/NotifyOwnerChange\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { RadarRules, RadarEventType } from \"@/game/rules/general/RadarRules\";\nimport { RadarEvent } from \"@/game/event/RadarEvent\";\nimport { NotifyAttack } from \"@/game/trait/interface/NotifyAttack\";\nimport { NotifyWarpChange } from \"@/game/trait/interface/NotifyWarpChange\";\nimport { NotifySuperWeaponActivate } from \"@/game/trait/interface/NotifySuperWeaponActivate\";\nimport { SuperWeaponType } from \"@/game/type/SuperWeaponType\";\nimport { NotifySuperWeaponDeactivate } from \"@/game/trait/interface/NotifySuperWeaponDeactivate\";\nexport class RadarTrait {\n    private activeLightningStrikes: Map<any, number>;\n    constructor() {\n        this.activeLightningStrikes = new Map();\n    }\n    [NotifySpawn.onSpawn](entity: any, game: any): void {\n        if (entity.isBuilding() && entity.rules.radar) {\n            this.updateRadarForPlayer(entity.owner, game);\n        }\n    }\n    [NotifyUnspawn.onUnspawn](entity: any, game: any): void {\n        if (entity.isBuilding() && entity.rules.radar) {\n            this.updateRadarForPlayer(entity.owner, game);\n        }\n    }\n    [NotifyPower.onPowerLow](player: any, game: any): void {\n        this.updateRadarForPlayer(player, game);\n    }\n    [NotifyPower.onPowerRestore](player: any, game: any): void {\n        this.updateRadarForPlayer(player, game);\n    }\n    [NotifyPower.onPowerChange](): void { }\n    [NotifyOwnerChange.onChange](entity: any, oldOwner: any, game: any): void {\n        if (entity.rules.radar) {\n            this.updateRadarForPlayer(oldOwner, game);\n            this.updateRadarForPlayer(entity.owner, game);\n        }\n    }\n    [NotifyWarpChange.onChange](entity: any, game: any): void {\n        if (entity.rules.radar) {\n            this.updateRadarForPlayer(entity.owner, game);\n        }\n    }\n    [NotifySuperWeaponActivate.onActivate](type: SuperWeaponType, player: any, game: any): void {\n        if (type === SuperWeaponType.LightningStorm) {\n            this.activeLightningStrikes.set(player, (this.activeLightningStrikes.get(player) ?? 0) + 1);\n            for (const combatant of game.getCombatants()) {\n                if (combatant !== player && !game.alliances.areAllied(combatant, player)) {\n                    this.updateRadarForPlayer(combatant, game);\n                }\n            }\n        }\n    }\n    [NotifySuperWeaponDeactivate.onDeactivate](type: SuperWeaponType, player: any, game: any): void {\n        if (type === SuperWeaponType.LightningStorm) {\n            const count = (this.activeLightningStrikes.get(player) ?? 0) - 1;\n            if (count > 0) {\n                this.activeLightningStrikes.set(player, count);\n            }\n            else {\n                this.activeLightningStrikes.delete(player);\n            }\n            if (count <= 0) {\n                for (const combatant of game.getCombatants()) {\n                    this.updateRadarForPlayer(combatant, game);\n                }\n            }\n        }\n    }\n    private updateRadarForPlayer(player: any, game: any): void {\n        if (!player.radarTrait)\n            return;\n        const wasDisabled = player.radarTrait.isDisabled();\n        const shouldDisable = ![...player.buildings].find((building: any) => building.rules.radar && !building.warpedOutTrait.isActive()) ||\n            player.powerTrait.level === PowerLevel.Low ||\n            [...this.activeLightningStrikes.entries()].some(([strikePlayer, count]) => count && strikePlayer !== player && !game.alliances.areAllied(strikePlayer, player));\n        player.radarTrait.setDisabled(shouldDisable);\n        if (wasDisabled !== shouldDisable) {\n            game.events.dispatch(new RadarOnOffEvent(player, !shouldDisable));\n        }\n    }\n    [NotifyAttack.onAttack](attacker: any, target: any, game: any): void {\n        if (!attacker.isTechno())\n            return;\n        if (!attacker.isBuilding() || attacker.rules.canBeOccupied || attacker.rules.needsEngineer) {\n            if (attacker.isVehicle() && attacker.harvesterTrait) {\n                this.addEventForPlayer(RadarEventType.HarvesterUnderAttack, attacker.owner, attacker.tile, game);\n            }\n        }\n        else {\n            this.addEventForPlayer(RadarEventType.BaseUnderAttack, attacker.owner, attacker.tile, game);\n        }\n    }\n    private addEventForPlayer(eventType: RadarEventType, player: any, tile: any, game: any): void {\n        const radarTrait = player.radarTrait;\n        if (!radarTrait)\n            return;\n        const radarRules = game.rules.general.radar;\n        radarTrait.activeEvents = radarTrait.activeEvents.filter((event: any) => game.currentTick - event.startTick < radarRules.getEventDuration(event.type));\n        const rangeHelper = new RangeHelper(game.map.tileOccupation);\n        const hasExistingEvent = radarTrait.activeEvents.find((event: any) => event.type === eventType &&\n            rangeHelper.isInTileRange(tile, event.tile, 0, radarRules.getEventSuppresionDistance(event.type)));\n        if (!hasExistingEvent) {\n            radarTrait.activeEvents.push({\n                startTick: game.currentTick,\n                tile: tile,\n                type: eventType\n            });\n            game.events.dispatch(new RadarEvent(player, eventType, tile));\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trait/SellTrait.ts",
    "content": "import { ObjectSellEvent } from '../event/ObjectSellEvent';\nimport { NotifySell } from '../gameobject/trait/interface/NotifySell';\nexport class SellTrait {\n    private game: any;\n    private generalRules: any;\n    constructor(game: any, generalRules: any) {\n        this.game = game;\n        this.generalRules = generalRules;\n    }\n    sell(target: any): void {\n        if (!target.isBuilding() || !target.rules.unsellable) {\n            let refundValue = this.computeRefundValue(target);\n            if (refundValue) {\n                if (target.rules.wall) {\n                    refundValue = 0;\n                }\n                target.traits\n                    .filter(NotifySell)\n                    .forEach((trait: any) => {\n                    trait[NotifySell.onSell](target, this.game);\n                });\n                if (target.isBuilding()) {\n                    this.game\n                        .getConstructionWorker(target.owner)\n                        .unplace(target, () => this.afterObjectUnspawned(target, refundValue));\n                }\n                else {\n                    this.game.unspawnObject(target);\n                    this.afterObjectUnspawned(target, refundValue);\n                }\n            }\n        }\n    }\n    private afterObjectUnspawned(target: any, refundValue: number): void {\n        target.owner.credits += refundValue;\n        this.game.events.dispatch(new ObjectSellEvent(target));\n        target.dispose();\n    }\n    private computeRefundValue(target: any): number {\n        let refundValue = 0;\n        if (target.rules.soylent > 0) {\n            refundValue = target.rules.soylent;\n        }\n        else if (target.rules.cost) {\n            refundValue = target.purchaseValue;\n            if (!target.owner.isAi) {\n                refundValue = Math.floor(refundValue * this.generalRules.refundPercent);\n            }\n        }\n        return refundValue;\n    }\n    computePurchaseValue(target: any, cost: any): number {\n        return target.cost;\n    }\n    dispose(): void {\n        this.game = undefined;\n    }\n}\n"
  },
  {
    "path": "src/game/trait/SharedDetectCloakTrait.ts",
    "content": "import { NotifyOwnerChange } from \"@/game/trait/interface/NotifyOwnerChange\";\nimport { NotifySpawn } from \"@/game/trait/interface/NotifySpawn\";\nimport { NotifyTileChange } from \"@/game/trait/interface/NotifyTileChange\";\nimport { NotifyUnspawn } from \"@/game/trait/interface/NotifyUnspawn\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { NotifyPower } from \"@/game/trait/interface/NotifyPower\";\nimport { NotifyTick } from \"@/game/trait/interface/NotifyTick\";\nimport { RadarTrait } from \"@/game/trait/RadarTrait\";\nimport { RadarEventType } from \"@/game/rules/general/RadarRules\";\nimport { NotifyObjectTraitAdd } from \"@/game/trait/interface/NotifyObjectTraitAdd\";\nimport { CloakableTrait } from \"@/game/gameobject/trait/CloakableTrait\";\nimport { SensorsTrait } from \"@/game/gameobject/trait/SensorsTrait\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { Box2 } from \"@/game/math/Box2\";\nexport class SharedDetectCloakTrait {\n    private detectors: Set<any> = new Set();\n    [NotifySpawn.onSpawn](object: any, game: any) {\n        if (this.isGlobalDetector(object)) {\n            this.detectors.add(object);\n            this.updateAroundDetector(object, game);\n        }\n        if (this.isCloakable(object)) {\n            this.detect(object, game);\n        }\n    }\n    [NotifyUnspawn.onUnspawn](object: any, game: any) {\n        if (object.isTechno() && this.isGlobalDetector(object)) {\n            this.detectors.delete(object);\n        }\n    }\n    [NotifyOwnerChange.onChange](object: any, oldOwner: any, game: any) {\n        if (this.isGlobalDetector(object)) {\n            this.updateAroundDetector(object, game);\n        }\n        if (this.isCloakable(object)) {\n            this.detect(object, game);\n        }\n    }\n    [NotifyTileChange.onTileChange](object: any, game: any, oldTile: any) {\n        if (this.isGlobalDetector(object)) {\n            this.updateAroundDetector(object, game);\n        }\n        if (this.isCloakable(object)) {\n            this.detect(object, game);\n        }\n    }\n    [NotifyObjectTraitAdd.onAdd](object: any, trait: any, game: any) {\n        if (object.isTechno()) {\n            if (trait instanceof CloakableTrait) {\n                if (this.isCloakable(object)) {\n                    this.detect(object, game);\n                }\n            }\n            else if (trait instanceof SensorsTrait && this.isGlobalDetector(object)) {\n                this.updateAroundDetector(object, game);\n            }\n        }\n    }\n    [NotifyPower.onPowerLow](object: any, game: any) { }\n    [NotifyPower.onPowerRestore](owner: any, game: any) {\n        const poweredDetectors = [...this.detectors].filter((detector) => detector.owner === owner && detector.isBuilding() && detector.poweredTrait);\n        this.updateAroundDetectors(poweredDetectors, game);\n    }\n    [NotifyPower.onPowerChange](object: any, game: any) { }\n    [NotifyTick.onTick](game: any) {\n        for (const combatant of game.getCombatants()) {\n            for (const object of combatant.getOwnedObjects()) {\n                if (object.cloakableTrait && !object.cloakableTrait.isCloaked()) {\n                    this.detect(object, game);\n                }\n            }\n        }\n    }\n    private updateAroundDetectors(detectors: any[], game: any) {\n        const detectedObjects = new Set();\n        for (const detector of detectors) {\n            for (const object of this.findTechnosAroundDetector(detector, game)) {\n                detectedObjects.add(object);\n            }\n        }\n        for (const object of detectedObjects) {\n            if (this.isCloakable(object)) {\n                this.detect(object, game);\n            }\n        }\n    }\n    private updateAroundDetector(detector: any, game: any) {\n        for (const object of this.findTechnosAroundDetector(detector, game)) {\n            if (this.isCloakable(object)) {\n                this.detect(object, game);\n            }\n        }\n    }\n    private findTechnosAroundDetector(detector: any, game: any) {\n        const foundation = detector.getFoundation();\n        const size = Math.max(foundation.width, foundation.height);\n        const range = detector.rules.sensorsSight + size;\n        const minPoint = new Vector2(detector.tile.rx, detector.tile.ry).addScalar(-range);\n        const maxPoint = new Vector2(detector.tile.rx, detector.tile.ry).addScalar(range);\n        return game.map.technosByTile.queryRange(new Box2(minPoint, maxPoint));\n    }\n    private detect(object: any, game: any) {\n        const rangeHelper = new RangeHelper(game.map.tileOccupation);\n        for (const detector of this.detectors) {\n            if (!game.areFriendly(detector, object)) {\n                const sightRange = detector.rules.sensorsSight;\n                if (!(detector.isBuilding() && detector.poweredTrait && !detector.poweredTrait.isPoweredOn()) &&\n                    rangeHelper.tileDistance(object, detector.tile) <= sightRange) {\n                    const wasCloaked = object.cloakableTrait?.isCloaked();\n                    object.cloakableTrait.uncloak(game);\n                    if (wasCloaked) {\n                        for (const player of [detector.owner, ...game.alliances.getAllies(detector.owner)]) {\n                            game.traits\n                                .get(RadarTrait)\n                                .addEventForPlayer(RadarEventType.GenericNonCombat, player, object.tile, game);\n                        }\n                    }\n                    break;\n                }\n            }\n        }\n    }\n    private isGlobalDetector(object: any): boolean {\n        return !(!object.isTechno() ||\n            (!object.sensorsTrait && !object.rules.sensorArray) ||\n            !object.rules.sensorsSight);\n    }\n    private isCloakable(object: any): boolean {\n        return object.isTechno() && !!object.cloakableTrait;\n    }\n}\n"
  },
  {
    "path": "src/game/trait/SharedDetectDisguiseTrait.ts",
    "content": "import { NotifyOwnerChange } from \"@/game/trait/interface/NotifyOwnerChange\";\nimport { NotifySpawn } from \"@/game/trait/interface/NotifySpawn\";\nimport { NotifyTileChange } from \"@/game/trait/interface/NotifyTileChange\";\nimport { NotifyUnspawn } from \"@/game/trait/interface/NotifyUnspawn\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { NotifyPower } from \"@/game/trait/interface/NotifyPower\";\nimport { Vector2 } from \"@/game/math/Vector2\";\nimport { Box2 } from \"@/game/math/Box2\";\nexport class SharedDetectDisguiseTrait {\n    private detectors: Set<any>;\n    constructor() {\n        this.detectors = new Set();\n    }\n    [NotifySpawn.onSpawn](entity: any, game: any) {\n        if (this.isGlobalDetector(entity)) {\n            this.detectors.add(entity);\n            this.updateAroundDetector(entity, game);\n        }\n        if (this.isDisguisable(entity)) {\n            this.detect(entity, game);\n        }\n    }\n    [NotifyUnspawn.onUnspawn](entity: any, game: any) {\n        if (entity.isTechno()) {\n            if (this.isGlobalDetector(entity)) {\n                this.detectors.delete(entity);\n                this.updateAroundDetector(entity, game);\n            }\n            if (this.isDisguisable(entity)) {\n                this.undetect(entity, game);\n            }\n        }\n    }\n    [NotifyOwnerChange.onChange](entity: any, oldOwner: any, game: any) {\n        if (this.isGlobalDetector(entity)) {\n            this.updateAroundDetector(entity, game);\n        }\n        if (this.isDisguisable(entity)) {\n            this.undetect(entity, game);\n            this.detect(entity, game);\n        }\n    }\n    [NotifyTileChange.onTileChange](entity: any, game: any, oldTile: any) {\n        if (this.isGlobalDetector(entity)) {\n            this.updateAroundDetector(entity, game, oldTile);\n            this.updateAroundDetector(entity, game);\n        }\n        if (this.isDisguisable(entity)) {\n            this.undetect(entity, game);\n            this.detect(entity, game);\n        }\n    }\n    [NotifyPower.onPowerLow](owner: any, game: any) {\n        const affectedDetectors = [...this.detectors].filter((detector) => detector.owner === owner &&\n            detector.isBuilding() &&\n            detector.poweredTrait &&\n            !detector.poweredTrait.isPoweredOn());\n        this.updateAroundDetectors(affectedDetectors, game);\n    }\n    [NotifyPower.onPowerRestore](owner: any, game: any) {\n        const affectedDetectors = [...this.detectors].filter((detector) => detector.owner === owner &&\n            detector.isBuilding() &&\n            detector.poweredTrait);\n        this.updateAroundDetectors(affectedDetectors, game);\n    }\n    [NotifyPower.onPowerChange](entity: any, game: any) { }\n    private updateAroundDetectors(detectors: any[], game: any) {\n        const affectedTechnos = new Set();\n        for (const detector of detectors) {\n            for (const techno of this.findTechnosAroundDetector(detector, game, detector.tile)) {\n                affectedTechnos.add(techno);\n            }\n        }\n        for (const techno of affectedTechnos) {\n            if (this.isDisguisable(techno)) {\n                this.undetect(techno, game);\n                this.detect(techno, game);\n            }\n        }\n    }\n    private updateAroundDetector(detector: any, game: any, tile: any = detector.tile) {\n        for (const techno of this.findTechnosAroundDetector(detector, game, tile)) {\n            if (this.isDisguisable(techno)) {\n                this.undetect(techno, game);\n                this.detect(techno, game);\n            }\n        }\n    }\n    private findTechnosAroundDetector(detector: any, game: any, tile: any) {\n        const foundation = detector.getFoundation();\n        const size = Math.max(foundation.width, foundation.height);\n        const range = detector.rules.detectDisguiseRange + size;\n        const minPoint = new Vector2(tile.rx, tile.ry).addScalar(-range);\n        const maxPoint = new Vector2(tile.rx, tile.ry).addScalar(range);\n        return game.map.technosByTile.queryRange(new Box2(minPoint, maxPoint));\n    }\n    private detect(entity: any, game: any) {\n        const detectedOwners = new Set();\n        const rangeHelper = new RangeHelper(game.map.tileOccupation);\n        for (const detector of this.detectors) {\n            if (!game.areFriendly(detector, entity)) {\n                const owner = detector.owner;\n                const range = detector.rules.detectDisguiseRange;\n                if (!detectedOwners.has(owner)) {\n                    if (!(detector.isBuilding() &&\n                        detector.poweredTrait &&\n                        !detector.poweredTrait.isPoweredOn()) &&\n                        rangeHelper.tileDistance(entity, detector.tile) <= range) {\n                        for (const allianceOwner of [owner, ...game.alliances.getAllies(owner)]) {\n                            detectedOwners.add(allianceOwner);\n                        }\n                    }\n                }\n            }\n        }\n        for (const owner of detectedOwners) {\n            (owner as any).sharedDetectDisguiseTrait?.add(entity);\n        }\n    }\n    private undetect(entity: any, game: any) {\n        for (const combatant of game.getCombatants()) {\n            combatant.sharedDetectDisguiseTrait?.delete(entity);\n        }\n    }\n    private isGlobalDetector(entity: any): boolean {\n        return entity.isTechno() && entity.rules.detectDisguiseRange;\n    }\n    private isDisguisable(entity: any): boolean {\n        return (entity.isInfantry() || entity.isVehicle()) && entity.disguiseTrait;\n    }\n}\n"
  },
  {
    "path": "src/game/trait/StalemateDetectTrait.ts",
    "content": "import { StalemateDetectEvent } from '../event/StalemateDetectEvent';\nimport { GameSpeed } from '../GameSpeed';\nimport { NotifyDestroy } from './interface/NotifyDestroy';\nimport { NotifyOwnerChange } from './interface/NotifyOwnerChange';\nimport { NotifyPlaceBuilding } from './interface/NotifyPlaceBuilding';\nimport { NotifyProduceUnit } from './interface/NotifyProduceUnit';\nimport { NotifyTick } from './interface/NotifyTick';\nexport class StalemateDetectTrait {\n    private static graceMinutes = 10;\n    private stale: boolean = false;\n    private allPlayersCredits: Map<any, number> = new Map();\n    private countdownTicks: number;\n    constructor() {\n        this.resetCountdown();\n    }\n    isStale(): boolean {\n        return this.stale;\n    }\n    getCountdownTicks(): number {\n        return this.countdownTicks;\n    }\n    resetCountdown(): void {\n        this.countdownTicks = Math.floor(60 * StalemateDetectTrait.graceMinutes * GameSpeed.BASE_TICKS_PER_SECOND);\n    }\n    clearStale(): void {\n        this.stale = false;\n        this.resetCountdown();\n    }\n    [NotifyTick.onTick](e: any): void {\n        if (this.countdownTicks > 0) {\n            this.countdownTicks--;\n        }\n        else if (!this.stale) {\n            this.stale = true;\n            this.resetCountdown();\n            e.events.dispatch(new StalemateDetectEvent());\n        }\n        for (const t of e.getCombatants()) {\n            const i = this.allPlayersCredits.get(t);\n            if (i !== t.credits) {\n                this.allPlayersCredits.set(t, t.credits);\n                if (t.credits > (i ?? 0) && t.production.hasAnyFactory()) {\n                    this.clearStale();\n                }\n            }\n        }\n    }\n    [NotifyProduceUnit.onProduce](): void {\n        this.clearStale();\n    }\n    [NotifyPlaceBuilding.onPlace](e: any): void {\n        if (!e.wallTrait) {\n            this.clearStale();\n        }\n    }\n    [NotifyDestroy.onDestroy](e: any, t: any, i: any): void {\n        if (e.isBuilding() &&\n            !e.owner.isNeutral &&\n            !e.wallTrait &&\n            !e.rules.insignificant &&\n            !(e.owner.defeated && this.stale) &&\n            !(i?.obj && t.areFriendly(e, i.obj))) {\n            this.clearStale();\n        }\n    }\n    [NotifyOwnerChange.onChange](e: any, t: any, i: any): void {\n        if (e.isBuilding() &&\n            !t.isNeutral &&\n            !i.alliances.areAllied(e.owner, t)) {\n            this.clearStale();\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trait/SuperWeaponsTrait.ts",
    "content": "import { NotifyWarpChange } from \"@/game/trait/interface/NotifyWarpChange\";\nimport { SuperWeapon, SuperWeaponStatus } from \"@/game/SuperWeapon\";\nimport { SuperWeaponEffect, EffectStatus } from \"@/game/superweapon/SuperWeaponEffect\";\nimport { NotifyPower } from \"@/game/trait/interface/NotifyPower\";\nimport { NotifyTick } from \"@/game/trait/interface/NotifyTick\";\nimport { SuperWeaponType } from \"@/game/type/SuperWeaponType\";\nimport { NotifySuperWeaponActivate } from \"@/game/trait/interface/NotifySuperWeaponActivate\";\nimport { SuperWeaponActivateEvent } from \"@/game/event/SuperWeaponActivateEvent\";\nimport { ParadropEffect } from \"@/game/superweapon/ParadropEffect\";\nimport { NukeEffect } from \"@/game/superweapon/NukeEffect\";\nimport { LightningStormEffect } from \"@/game/superweapon/LightningStormEffect\";\nimport { IronCurtainEffect } from \"@/game/superweapon/IronCurtainEffect\";\nimport { ChronoSphereEffect } from \"@/game/superweapon/ChronoSphereEffect\";\nimport { NotifySuperWeaponDeactivate } from \"@/game/trait/interface/NotifySuperWeaponDeactivate\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nexport class SuperWeaponsTrait {\n    private effects: SuperWeaponEffect[] = [];\n    [NotifyTick.onTick](t: any) {\n        for (const e of t.getCombatants()) {\n            for (const i of e.superWeaponsTrait.getAll()) {\n                i.update(t);\n            }\n        }\n        for (const r of this.effects) {\n            if (r.status === EffectStatus.NotStarted) {\n                r.onStart(t);\n                r.status = EffectStatus.Running;\n            }\n            if (r.onTick(t)) {\n                r.status = EffectStatus.Finished;\n                t.traits\n                    .filter(NotifySuperWeaponDeactivate)\n                    .forEach((e) => {\n                    e[NotifySuperWeaponDeactivate.onDeactivate](r.type, r.owner, t);\n                });\n            }\n        }\n        this.effects = this.effects.filter((e) => e.status !== EffectStatus.Finished);\n    }\n    [NotifyPower.onPowerLow](e: any, t: any) {\n        e.superWeaponsTrait\n            ?.getAll()\n            ?.filter((e: any) => e.rules.isPowered)\n            .forEach((e: any) => {\n            this.updateTimer(e, false);\n        });\n    }\n    [NotifyPower.onPowerRestore](e: any, t: any) {\n        e.superWeaponsTrait\n            ?.getAll()\n            ?.filter((e: any) => e.rules.isPowered)\n            .forEach((e: any) => {\n            this.updateTimer(e, true);\n        });\n    }\n    [NotifyPower.onPowerChange](e: any, t: any) { }\n    [NotifyWarpChange.onChange](e: any, t: any) {\n        const i = e.superWeaponTrait?.getSuperWeapon(e);\n        if (e.owner.powerTrait && e.isBuilding() && e.superWeaponTrait && i) {\n            this.updateTimer(i, !e.owner.powerTrait.isLowPower());\n        }\n    }\n    private updateTimer(e: any, t: boolean) {\n        const i = this.superWeaponHasValidBuilding(e);\n        if (t && i) {\n            e.resumeTimer();\n        }\n        else {\n            e.pauseTimer();\n        }\n    }\n    private superWeaponHasValidBuilding(t: any) {\n        return [...t.owner.buildings].find((e: any) => !(e.superWeaponTrait?.getSuperWeapon(e) !== t ||\n            (e.warpedOutTrait.isActive() && t.rules.isPowered)));\n    }\n    private addEffect(e: SuperWeaponEffect) {\n        this.effects.push(e);\n    }\n    activateSuperWeapon(t: SuperWeaponType, e: any, i: any, r: any, s: any) {\n        const a = e.superWeaponsTrait\n            ?.getAll()\n            .find((e: any) => e.rules.type === t);\n        if (a && a.status === SuperWeaponStatus.Ready) {\n            if (a.oneTimeOnly) {\n                e.superWeaponsTrait.remove(a.name);\n                for (const n of e.buildings) {\n                    if (n.rules.superWeapon === a.name && n.superWeaponTrait) {\n                        n.superWeaponTrait.addSuperWeaponToPlayerIfNeeded(e, i);\n                    }\n                }\n            }\n            else {\n                a.resetTimer();\n            }\n            this.activateEffect(a.rules, e, i, r, s);\n        }\n    }\n    private activateEffect(e: any, i: any, r: any, s: any, a: any, n: boolean = false) {\n        const o = e.type;\n        if (o !== undefined) {\n            const t: SuperWeaponEffect[] = [];\n            switch (o) {\n                case SuperWeaponType.AmerParaDrop:\n                    for (const [l, c] of r.rules.general.paradrop.amerParaDrop.entries()) {\n                        if (r.rules.hasObject(c.inf, ObjectType.Infantry)) {\n                            t.push(new ParadropEffect(o, i, s, c, l));\n                        }\n                        else {\n                            console.warn(`Can't paradrop unknown infantry type \"${c.inf}\"`);\n                        }\n                    }\n                    break;\n                case SuperWeaponType.ParaDrop: {\n                    const e = r.rules.general.paradrop.getParadropSquads(i.country.side);\n                    for (const [h, u] of e.entries()) {\n                        if (r.rules.hasObject(u.inf, ObjectType.Infantry)) {\n                            t.push(new ParadropEffect(o, i, s, u, h));\n                        }\n                        else {\n                            console.warn(`Can't paradrop unknown infantry type \"${u.inf}\"`);\n                        }\n                    }\n                    break;\n                }\n                case SuperWeaponType.MultiMissile:\n                    if (!e.weaponType) {\n                        throw new Error(\"Missing WeaponType in super weapon rules\");\n                    }\n                    t.push(new NukeEffect(o, i, s, e.weaponType));\n                    break;\n                case SuperWeaponType.LightningStorm:\n                    t.push(new LightningStormEffect(o, i, s));\n                    break;\n                case SuperWeaponType.IronCurtain:\n                    t.push(new IronCurtainEffect(o, i, s));\n                    break;\n                case SuperWeaponType.ChronoSphere:\n                    if (!a) {\n                        throw new Error(\"Missing tile2 action param\");\n                    }\n                    t.push(new ChronoSphereEffect(o, i, s, a));\n                    break;\n            }\n            for (const d of t) {\n                this.addEffect(d);\n            }\n            r.traits.filter(NotifySuperWeaponActivate).forEach((e) => {\n                e[NotifySuperWeaponActivate.onActivate](o, i, r, s, a);\n            });\n            r.events.dispatch(new SuperWeaponActivateEvent(o, i, s, a, n));\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyAllianceChange.ts",
    "content": "export const NotifyAllianceChange = {\n    onChange: Symbol()\n};\nexport interface NotifyAllianceChange {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyAttack.ts",
    "content": "export const NotifyAttack = {\n    onAttack: Symbol()\n};\nexport interface NotifyAttack {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyDestroy.ts",
    "content": "export const NotifyDestroy = {\n    onDestroy: Symbol()\n};\nexport interface NotifyDestroy {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyElevationChange.ts",
    "content": "export const NotifyElevationChange = {\n    onElevationChange: Symbol()\n};\nexport interface NotifyElevationChange {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyHealthChange.ts",
    "content": "export const NotifyHealthChange = {\n    onChange: Symbol()\n};\nexport interface NotifyHealthChange {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyObjectTraitAdd.ts",
    "content": "export const NotifyObjectTraitAdd = {\n    onAdd: Symbol()\n};\nexport interface NotifyObjectTraitAdd {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyOwnerChange.ts",
    "content": "export const NotifyOwnerChange = {\n    onChange: Symbol()\n};\nexport interface NotifyOwnerChange {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyPlaceBuilding.ts",
    "content": "export const NotifyPlaceBuilding = {\n    onPlace: Symbol()\n};\nexport interface NotifyPlaceBuilding {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyPower.ts",
    "content": "export const NotifyPower = {\n    onPowerLow: Symbol(),\n    onPowerRestore: Symbol(),\n    onPowerChange: Symbol()\n};\nexport interface NotifyPower {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyProduceUnit.ts",
    "content": "export const NotifyProduceUnit = {\n    onProduce: Symbol()\n};\nexport interface NotifyProduceUnit {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifySpawn.ts",
    "content": "export const NotifySpawn = {\n    onSpawn: Symbol()\n};\nexport interface NotifySpawn {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifySuperWeaponActivate.ts",
    "content": "export const NotifySuperWeaponActivate = {\n    onActivate: Symbol()\n};\nexport interface NotifySuperWeaponActivate {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifySuperWeaponDeactivate.ts",
    "content": "export const NotifySuperWeaponDeactivate = {\n    onDeactivate: Symbol()\n};\nexport interface NotifySuperWeaponDeactivate {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyTargetDestroy.ts",
    "content": "export const NotifyTargetDestroy = {\n    onDestroy: Symbol()\n};\nexport interface NotifyTargetDestroy {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyTick.ts",
    "content": "export const NotifyTick = {\n    onTick: Symbol()\n};\nexport interface NotifyTick {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyTileChange.ts",
    "content": "export const NotifyTileChange = {\n    onTileChange: Symbol()\n};\nexport interface NotifyTileChange {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyUnspawn.ts",
    "content": "export const NotifyUnspawn = {\n    onUnspawn: Symbol()\n};\nexport interface NotifyUnspawn {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trait/interface/NotifyWarpChange.ts",
    "content": "export const NotifyWarpChange = {\n    onChange: Symbol()\n};\nexport interface NotifyWarpChange {\n    [key: symbol]: (...args: any[]) => void;\n}\n"
  },
  {
    "path": "src/game/trigger/TriggerCondition.ts",
    "content": "export class TriggerCondition {\n    [key: string]: any;\n    public event: any;\n    public trigger: any;\n    public blocking: boolean;\n    public targets: any[];\n    public player?: any;\n    constructor(event: any, trigger: any) {\n        this.event = event;\n        this.trigger = trigger;\n        this.blocking = false;\n        this.targets = [];\n    }\n    init(game: any): void {\n        const player = game\n            .getAllPlayers()\n            .find((p: any) => p.country?.name === this.trigger.houseName);\n        if (player) {\n            this.player = player;\n        }\n    }\n    setTargets(targets: any[]): void {\n        this.targets = targets;\n    }\n    reset(): void { }\n    getDebugName(): string {\n        return `${this.event.triggerId}[${this.event.eventIndex}] (${this.trigger.name}).`;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/TriggerConditionFactory.ts",
    "content": "import { TriggerEventType } from \"@/data/map/trigger/TriggerEventType\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { AmbientLightCondition } from \"@/game/trigger/condition/AmbientLightCondition\";\nimport { AnyEventCondition } from \"@/game/trigger/condition/AnyEventCondition\";\nimport { AttackedByAnyCondition } from \"@/game/trigger/condition/AttackedByAnyCondition\";\nimport { AttackedByHouseCondition } from \"@/game/trigger/condition/AttackedByHouseCondition\";\nimport { BuildingExistsCondition } from \"@/game/trigger/condition/BuildingExistsCondition\";\nimport { BuildObjectTypeCondition } from \"@/game/trigger/condition/BuildObjectTypeCondition\";\nimport { ComesNearWaypointCondition } from \"@/game/trigger/condition/ComesNearWaypointCondition\";\nimport { CreditsBelowCondition } from \"@/game/trigger/condition/CreditsBelowCondition\";\nimport { CreditsExceedCondition } from \"@/game/trigger/condition/CreditsExceedCondition\";\nimport { CrossHorizLineCondition } from \"@/game/trigger/condition/CrossHorizLineCondition\";\nimport { CrossVertLineCondition } from \"@/game/trigger/condition/CrossVertLineCondition\";\nimport { DestroyedAllBuildingsCondition } from \"@/game/trigger/condition/DestroyedAllBuildingsCondition\";\nimport { DestroyedAllCondition } from \"@/game/trigger/condition/DestroyedAllCondition\";\nimport { DestroyedAllUnitsCondition } from \"@/game/trigger/condition/DestroyedAllUnitsCondition\";\nimport { DestroyedAllUnitsLandCondition } from \"@/game/trigger/condition/DestroyedAllUnitsLandCondition\";\nimport { DestroyedAllUnitsNavalCondition } from \"@/game/trigger/condition/DestroyedAllUnitsNavalCondition\";\nimport { DestroyedBridgeCondition } from \"@/game/trigger/condition/DestroyedBridgeCondition\";\nimport { DestroyedBuildingsCondition } from \"@/game/trigger/condition/DestroyedBuildingsCondition\";\nimport { DestroyedByAnyCondition } from \"@/game/trigger/condition/DestroyedByAnyCondition\";\nimport { DestroyedOrCapturedCondition } from \"@/game/trigger/condition/DestroyedOrCapturedCondition\";\nimport { DestroyedOrCapturedOrInfiltratedCondition } from \"@/game/trigger/condition/DestroyedOrCapturedOrInfiltratedCondition\";\nimport { DestroyedUnitsCondition } from \"@/game/trigger/condition/DestroyedUnitsCondition\";\nimport { ElapsedScenarioTimeCondition } from \"@/game/trigger/condition/ElapsedScenarioTimeCondition\";\nimport { ElapsedTimeCondition } from \"@/game/trigger/condition/ElapsedTimeCondition\";\nimport { EnteredByCondition } from \"@/game/trigger/condition/EnteredByCondition\";\nimport { GlobalVariableCondition } from \"@/game/trigger/condition/GlobalVariableCondition\";\nimport { HealthBelowAnyCondition } from \"@/game/trigger/condition/HealthBelowAnyCondition\";\nimport { HealthBelowCombatCondition } from \"@/game/trigger/condition/HealthBelowCombatCondition\";\nimport { LocalVariableCondition } from \"@/game/trigger/condition/LocalVariableCondition\";\nimport { LowPowerCondition } from \"@/game/trigger/condition/LowPowerCondition\";\nimport { NoEventCondition } from \"@/game/trigger/condition/NoEventCondition\";\nimport { NoFactoriesLeftCondition } from \"@/game/trigger/condition/NoFactoriesLeftCondition\";\nimport { PickupCrateAnyCondition } from \"@/game/trigger/condition/PickupCrateAnyCondition\";\nimport { PickupCrateCondition } from \"@/game/trigger/condition/PickupCrateCondition\";\nimport { RandomDelayCondition } from \"@/game/trigger/condition/RandomDelayCondition\";\nimport { SpiedByCondition } from \"@/game/trigger/condition/SpiedByCondition\";\nimport { SpyEnteringAsHouseCondition } from \"@/game/trigger/condition/SpyEnteringAsHouseCondition\";\nimport { SpyEnteringAsInfantryCondition } from \"@/game/trigger/condition/SpyEnteringAsInfantryCondition\";\nimport { TimerExpiredCondition } from \"@/game/trigger/condition/TimerExpiredCondition\";\nexport class TriggerConditionFactory {\n    create(e: any, t: any) {\n        switch (e.type) {\n            case TriggerEventType.NoEvent:\n                return new NoEventCondition(e, t);\n            case TriggerEventType.EnteredBy:\n                return new EnteredByCondition(e, t);\n            case TriggerEventType.SpiedBy:\n                return new SpiedByCondition(e, t);\n            case TriggerEventType.AttackedByAny:\n                return new AttackedByAnyCondition(e, t);\n            case TriggerEventType.DestroyedByAny:\n                return new DestroyedByAnyCondition(e, t);\n            case TriggerEventType.AnyEvent:\n                return new AnyEventCondition(e, t);\n            case TriggerEventType.DestroyedAllUnits:\n                return new DestroyedAllUnitsCondition(e, t);\n            case TriggerEventType.DestroyedAllBuildings:\n                return new DestroyedAllBuildingsCondition(e, t);\n            case TriggerEventType.DestroyedAll:\n                return new DestroyedAllCondition(e, t);\n            case TriggerEventType.CreditsExceed:\n                return new CreditsExceedCondition(e, t);\n            case TriggerEventType.ElapsedTime:\n                return new ElapsedTimeCondition(e, t);\n            case TriggerEventType.MissionTimerExpired:\n                return new TimerExpiredCondition(e, t);\n            case TriggerEventType.DestroyedBuildings:\n                return new DestroyedBuildingsCondition(e, t);\n            case TriggerEventType.DestroyedUnits:\n                return new DestroyedUnitsCondition(e, t);\n            case TriggerEventType.NoFactoriesLeft:\n                return new NoFactoriesLeftCondition(e, t);\n            case TriggerEventType.BuildBuilding:\n                return new BuildObjectTypeCondition(e, t, ObjectType.Building);\n            case TriggerEventType.BuildUnit:\n                return new BuildObjectTypeCondition(e, t, ObjectType.Vehicle);\n            case TriggerEventType.BuildInfantry:\n                return new BuildObjectTypeCondition(e, t, ObjectType.Infantry);\n            case TriggerEventType.BuildAircraft:\n                return new BuildObjectTypeCondition(e, t, ObjectType.Aircraft);\n            case TriggerEventType.CrossesHorizontalLine:\n                return new CrossHorizLineCondition(e, t);\n            case TriggerEventType.CrossesVerticalLine:\n                return new CrossVertLineCondition(e, t);\n            case TriggerEventType.GlobalIsSet:\n                return new GlobalVariableCondition(e, t, true);\n            case TriggerEventType.GlobalIsCleared:\n                return new GlobalVariableCondition(e, t, false);\n            case TriggerEventType.DestroyedOrCaptured:\n                return new DestroyedOrCapturedCondition(e, t);\n            case TriggerEventType.LowPower:\n                return new LowPowerCondition(e, t);\n            case TriggerEventType.DestroyedBridge:\n                return new DestroyedBridgeCondition(e, t);\n            case TriggerEventType.BuildingExists:\n                return new BuildingExistsCondition(e, t);\n            case TriggerEventType.ComesNearWaypoint:\n                return new ComesNearWaypointCondition(e, t);\n            case TriggerEventType.LocalIsSet:\n                return new LocalVariableCondition(e, t, true);\n            case TriggerEventType.LocalIsCleared:\n                return new LocalVariableCondition(e, t, false);\n            case TriggerEventType.FirstDamagedCombat:\n                return new HealthBelowCombatCondition(e, t, 100);\n            case TriggerEventType.HalfHealthCombat:\n                return new HealthBelowCombatCondition(e, t, 50);\n            case TriggerEventType.QuarterHealthCombat:\n                return new HealthBelowCombatCondition(e, t, 25);\n            case TriggerEventType.FirstDamagedAny:\n                return new HealthBelowAnyCondition(e, t, 100);\n            case TriggerEventType.HalfHealthAny:\n                return new HealthBelowAnyCondition(e, t, 50);\n            case TriggerEventType.QuarterHealthAny:\n                return new HealthBelowAnyCondition(e, t, 25);\n            case TriggerEventType.AttackedByHouse:\n                return new AttackedByHouseCondition(e, t);\n            case TriggerEventType.AmbientLightBelow:\n                return new AmbientLightCondition(e, t, \"below\");\n            case TriggerEventType.AmbientLightAbove:\n                return new AmbientLightCondition(e, t, \"above\");\n            case TriggerEventType.ElapsedScenarioTime:\n                return new ElapsedScenarioTimeCondition(e, t);\n            case TriggerEventType.DestroyedOrCapturedOrInfiltrated:\n                return new DestroyedOrCapturedOrInfiltratedCondition(e, t);\n            case TriggerEventType.PickupCrate:\n                return new PickupCrateCondition(e, t);\n            case TriggerEventType.PickupCrateAny:\n                return new PickupCrateAnyCondition(e, t);\n            case TriggerEventType.RandomDelay:\n                return new RandomDelayCondition(e, t);\n            case TriggerEventType.CreditsBelow:\n                return new CreditsBelowCondition(e, t);\n            case TriggerEventType.SpyEnteringAsHouse:\n                return new SpyEnteringAsHouseCondition(e, t);\n            case TriggerEventType.SpyEnteringAsInfantry:\n                return new SpyEnteringAsInfantryCondition(e, t);\n            case TriggerEventType.DestroyedAllUnitsNaval:\n                return new DestroyedAllUnitsNavalCondition(e, t);\n            case TriggerEventType.DestroyedAllUnitsLand:\n                return new DestroyedAllUnitsLandCondition(e, t);\n            case TriggerEventType.BuildingNotExists:\n                return new BuildingExistsCondition(e, t, true);\n            default:\n                throw new Error(`Unhandled trigger event type \"${TriggerEventType[e.type]}\"`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/TriggerExecutor.ts",
    "content": "export class TriggerExecutor {\n    protected action: any;\n    protected trigger: any;\n    constructor(action: any, trigger: any) {\n        this.action = action;\n        this.trigger = trigger;\n    }\n    getDebugName(): string {\n        return `${this.action.triggerId}[${this.action.index}] (${this.trigger.name}).`;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/TriggerExecutorFactory.ts",
    "content": "import { TriggerActionType } from \"@/data/map/trigger/TriggerActionType\";\nimport { AddSuperWeaponExecutor } from \"@/game/trigger/executor/AddSuperWeaponExecutor\";\nimport { ApplyDamageExecutor } from \"@/game/trigger/executor/ApplyDamageExecutor\";\nimport { ChangeHouseAllExecutor } from \"@/game/trigger/executor/ChangeHouseAllExecutor\";\nimport { ChangeHouseExecutor } from \"@/game/trigger/executor/ChangeHouseExecutor\";\nimport { CheerExecutor } from \"@/game/trigger/executor/CheerExecutor\";\nimport { CreateCrateExecutor } from \"@/game/trigger/executor/CreateCrateExecutor\";\nimport { CreateRadarEventExecutor } from \"@/game/trigger/executor/CreateRadarEventExecutor\";\nimport { DestroyObjectExecutor } from \"@/game/trigger/executor/DestroyObjectExecutor\";\nimport { DestroyTagExecutor } from \"@/game/trigger/executor/DestroyTagExecutor\";\nimport { DestroyTriggerExecutor } from \"@/game/trigger/executor/DestroyTriggerExecutor\";\nimport { DetonateWarheadExecutor } from \"@/game/trigger/executor/DetonateWarheadExecutor\";\nimport { EvictOccupiersExecutor } from \"@/game/trigger/executor/EvictOccupiersExecutor\";\nimport { FireSaleExecutor } from \"@/game/trigger/executor/FireSaleExecutor\";\nimport { ForceEndExecutor } from \"@/game/trigger/executor/ForceEndExecutor\";\nimport { ForceTriggerExecutor } from \"@/game/trigger/executor/ForceTriggerExecutor\";\nimport { GlobalVariableExecutor } from \"@/game/trigger/executor/GlobalVariableExecutor\";\nimport { IronCurtainExecutor } from \"@/game/trigger/executor/IronCurtainExecutor\";\nimport { LightningStrikeExecutor } from \"@/game/trigger/executor/LightningStrikeExecutor\";\nimport { LocalVariableExecutor } from \"@/game/trigger/executor/LocalVariableExecutor\";\nimport { NoActionExecutor } from \"@/game/trigger/executor/NoActionExecutor\";\nimport { NukeStrikeExecutor } from \"@/game/trigger/executor/NukeStrikeExecutor\";\nimport { PlayAnimAtExecutor } from \"@/game/trigger/executor/PlayAnimAtExecutor\";\nimport { PlaySoundFxAtExecutor } from \"@/game/trigger/executor/PlaySoundFxAtExecutor\";\nimport { PlaySoundFxExecutor } from \"@/game/trigger/executor/PlaySoundFxExecutor\";\nimport { PlaySpeechExecutor } from \"@/game/trigger/executor/PlaySpeechExecutor\";\nimport { ReshroudMapExecutor } from \"@/game/trigger/executor/ReshroudMapExecutor\";\nimport { ResizePlayerViewExecutor } from \"@/game/trigger/executor/ResizePlayerViewExecutor\";\nimport { RevealAroundWaypointExecutor } from \"@/game/trigger/executor/RevealAroundWaypointExecutor\";\nimport { RevealMapExecutor } from \"@/game/trigger/executor/RevealMapExecutor\";\nimport { SellBuildingExecutor } from \"@/game/trigger/executor/SellBuildingExecutor\";\nimport { SetAmbientLightExecutor } from \"@/game/trigger/executor/SetAmbientLightExecutor\";\nimport { SetAmbientRateExecutor } from \"@/game/trigger/executor/SetAmbientRateExecutor\";\nimport { SetAmbientStepExecutor } from \"@/game/trigger/executor/SetAmbientStepExecutor\";\nimport { StopSoundFxAtExecutor } from \"@/game/trigger/executor/StopSoundFxAtExecutor\";\nimport { TextTriggerExecutor } from \"@/game/trigger/executor/TextTriggerExecutor\";\nimport { TimerExtendExecutor } from \"@/game/trigger/executor/TimerExtendExecutor\";\nimport { TimerSetExecutor } from \"@/game/trigger/executor/TimerSetExecutor\";\nimport { TimerShortenExecutor } from \"@/game/trigger/executor/TimerShortenExecutor\";\nimport { TimerStartExecutor } from \"@/game/trigger/executor/TimerStartExecutor\";\nimport { TimerStopExecutor } from \"@/game/trigger/executor/TimerStopExecutor\";\nimport { TimerTextExecutor } from \"@/game/trigger/executor/TimerTextExecutor\";\nimport { ToggleTriggerExecutor } from \"@/game/trigger/executor/ToggleTriggerExecutor\";\nimport { TurnOnOffBuildingExecutor } from \"@/game/trigger/executor/TurnOnOffBuildingExecutor\";\nimport { UnrevealAroundWaypointExecutor } from \"@/game/trigger/executor/UnrevealAroundWaypointExecutor\";\nexport class TriggerExecutorFactory {\n    create(e: any, t: any) {\n        switch (e.type) {\n            case TriggerActionType.NoAction:\n                return new NoActionExecutor(e, t);\n            case TriggerActionType.FireSale:\n                return new FireSaleExecutor(e, t);\n            case TriggerActionType.TextTrigger:\n                return new TextTriggerExecutor(e, t);\n            case TriggerActionType.DestroyTrigger:\n                return new DestroyTriggerExecutor(e, t);\n            case TriggerActionType.ChangeHouse:\n                return new ChangeHouseExecutor(e, t);\n            case TriggerActionType.RevealMap:\n                return new RevealMapExecutor(e, t);\n            case TriggerActionType.RevealAroundWaypoint:\n                return new RevealAroundWaypointExecutor(e, t);\n            case TriggerActionType.PlaySoundFx:\n                return new PlaySoundFxExecutor(e, t);\n            case TriggerActionType.PlaySpeech:\n                return new PlaySpeechExecutor(e, t);\n            case TriggerActionType.ForceTrigger:\n                return new ForceTriggerExecutor(e, t);\n            case TriggerActionType.TimerStart:\n                return new TimerStartExecutor(e, t);\n            case TriggerActionType.TimerStop:\n                return new TimerStopExecutor(e, t);\n            case TriggerActionType.TimerExtend:\n                return new TimerExtendExecutor(e, t);\n            case TriggerActionType.TimerShorten:\n                return new TimerShortenExecutor(e, t);\n            case TriggerActionType.TimerSet:\n                return new TimerSetExecutor(e, t);\n            case TriggerActionType.GlobalSet:\n                return new GlobalVariableExecutor(e, t, true);\n            case TriggerActionType.GlobalClear:\n                return new GlobalVariableExecutor(e, t, false);\n            case TriggerActionType.DestroyObject:\n                return new DestroyObjectExecutor(e, t);\n            case TriggerActionType.AddOneTimeSuperWeapon:\n                return new AddSuperWeaponExecutor(e, t, true);\n            case TriggerActionType.AddRepeatingSuperWeapon:\n                return new AddSuperWeaponExecutor(e, t, false);\n            case TriggerActionType.AllChangeHouse:\n                return new ChangeHouseAllExecutor(e, t);\n            case TriggerActionType.ResizePlayerView:\n                return new ResizePlayerViewExecutor(e, t);\n            case TriggerActionType.PlayAnimAt:\n                return new PlayAnimAtExecutor(e, t);\n            case TriggerActionType.DetonateWarhead:\n                return new DetonateWarheadExecutor(e, t);\n            case TriggerActionType.ReshroudMap:\n                return new ReshroudMapExecutor(e, t);\n            case TriggerActionType.EnableTrigger:\n                return new ToggleTriggerExecutor(e, t, true);\n            case TriggerActionType.DisableTrigger:\n                return new ToggleTriggerExecutor(e, t, false);\n            case TriggerActionType.CreateRadarEvent:\n                return new CreateRadarEventExecutor(e, t);\n            case TriggerActionType.LocalSet:\n                return new LocalVariableExecutor(e, t, true);\n            case TriggerActionType.LocalClear:\n                return new LocalVariableExecutor(e, t, false);\n            case TriggerActionType.SellBuilding:\n                return new SellBuildingExecutor(e, t);\n            case TriggerActionType.TurnOffBuilding:\n                return new TurnOnOffBuildingExecutor(e, t, false);\n            case TriggerActionType.TurnOnBuilding:\n                return new TurnOnOffBuildingExecutor(e, t, true);\n            case TriggerActionType.ApplyOneHundredDamage:\n                return new ApplyDamageExecutor(e, t, 100);\n            case TriggerActionType.ForceEnd:\n                return new ForceEndExecutor(e, t);\n            case TriggerActionType.DestroyTag:\n                return new DestroyTagExecutor(e, t);\n            case TriggerActionType.SetAmbientStep:\n                return new SetAmbientStepExecutor(e, t);\n            case TriggerActionType.SetAmbientRate:\n                return new SetAmbientRateExecutor(e, t);\n            case TriggerActionType.SetAmbientLight:\n                return new SetAmbientLightExecutor(e, t);\n            case TriggerActionType.NukeStrike:\n                return new NukeStrikeExecutor(e, t);\n            case TriggerActionType.PlaySoundFxAt:\n                return new PlaySoundFxAtExecutor(e, t);\n            case TriggerActionType.UnrevealAroundWaypoint:\n                return new UnrevealAroundWaypointExecutor(e, t);\n            case TriggerActionType.LightningStrike:\n                return new LightningStrikeExecutor(e, t);\n            case TriggerActionType.TimerText:\n                return new TimerTextExecutor(e, t);\n            case TriggerActionType.CreateCrate:\n                return new CreateCrateExecutor(e, t);\n            case TriggerActionType.IronCurtainAt:\n                return new IronCurtainExecutor(e, t);\n            case TriggerActionType.EvictOccupiers:\n                return new EvictOccupiersExecutor(e, t);\n            case TriggerActionType.Cheer:\n                return new CheerExecutor(e, t);\n            case TriggerActionType.StopSoundsAt:\n                return new StopSoundFxAtExecutor(e, t);\n            default:\n                throw new Error(`Unhandled action type \"${TriggerActionType[e.type]}\"`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/TriggerInstance.ts",
    "content": "export class TriggerInstance {\n    private id: string;\n    constructor(id: string) {\n        this.id = id;\n    }\n    public getId(): string {\n        return this.id;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/TriggerManager.ts",
    "content": "import { TagRepeatType } from \"@/data/map/tag/TagRepeatType\";\nimport { TriggerExecutorFactory } from \"@/game/trigger/TriggerExecutorFactory\";\nimport { TriggerConditionFactory } from \"@/game/trigger/TriggerConditionFactory\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { Variable } from \"@/data/map/Variable\";\nimport { Trigger } from \"@/data/map/trigger/Trigger\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nimport { TriggerExecutor } from \"@/game/trigger/TriggerExecutor\";\nimport { MapObject } from \"@/data/map/MapObjects\";\ninterface TriggerInstance {\n    trigger: Trigger;\n    conditions: TriggerCondition[];\n    targets: MapObject[];\n    remainingTargets: Set<MapObject>;\n    disabled: boolean;\n    finished: boolean;\n}\nexport class TriggerManager {\n    private disposables: CompositeDisposable;\n    private triggerInstances: Map<string, TriggerInstance>;\n    private targetsByTag: Map<string, MapObject[]>;\n    private conditionFactory: TriggerConditionFactory;\n    private executorFactory: TriggerExecutorFactory;\n    private pendingGameEvents: any[];\n    private globalVariables: Map<string, Variable>;\n    private localVariables: Map<string, Variable>;\n    constructor() {\n        this.disposables = new CompositeDisposable();\n        this.triggerInstances = new Map();\n        this.targetsByTag = new Map();\n        this.conditionFactory = new TriggerConditionFactory();\n        this.executorFactory = new TriggerExecutorFactory();\n        this.pendingGameEvents = [];\n        this.globalVariables = new Map();\n        this.localVariables = new Map();\n    }\n    init(context: GameContext): void {\n        const initialObjects = context.map.getInitialMapObjects()[\"technos\"];\n        for (const obj of initialObjects) {\n            if (obj.tag) {\n                let targets = this.targetsByTag.get(obj.tag);\n                if (!targets) {\n                    targets = [];\n                    this.targetsByTag.set(obj.tag, targets);\n                }\n                const tile = context.map.tiles.getByMapCoords(obj.rx, obj.ry);\n                if (tile) {\n                    const mapObj = context.map.getObjectsOnTile(tile)\n                        .find(e => e.name === obj.name && e.type === obj.type);\n                    if (mapObj) {\n                        targets.push(mapObj);\n                    }\n                }\n            }\n        }\n        for (const cellTag of context.map.getCellTags()) {\n            const tile = context.map.tiles.getByMapCoords(cellTag.coords.x, cellTag.coords.y);\n            if (tile) {\n                let targets = this.targetsByTag.get(cellTag.tagId);\n                if (!targets) {\n                    targets = [];\n                    this.targetsByTag.set(cellTag.tagId, targets);\n                }\n                targets.push(tile);\n            }\n            else {\n                console.warn(`CellTag out of bounds at (${cellTag.coords.x}, ${cellTag.coords.y}). Skipping.`);\n            }\n        }\n        for (const [id, variable] of context.map.getVariables()) {\n            this.localVariables.set(id, variable.clone());\n        }\n        for (const trigger of context.map.getTriggers()) {\n            this.triggerInstances.set(trigger.id, this.createTriggerInstance(trigger, context));\n        }\n        this.disposables.add(context.events.subscribe(event => this.pendingGameEvents.push(event)));\n    }\n    private createTriggerInstance(trigger: Trigger, context: GameContext): TriggerInstance {\n        const targets = this.targetsByTag.get(trigger.tag.id) ?? [];\n        return {\n            trigger,\n            conditions: trigger.events\n                .map(event => {\n                const condition = this.conditionFactory.create(event, trigger);\n                condition.setTargets(targets);\n                condition.init(context);\n                return condition;\n            })\n                .sort((a, b) => Number(b.blocking) - Number(a.blocking)),\n            targets,\n            remainingTargets: new Set(trigger.tag.repeatType === TagRepeatType.OnceAll ? targets : []),\n            disabled: trigger.disabled,\n            finished: false\n        };\n    }\n    update(context: GameContext): void {\n        const events = this.pendingGameEvents.splice(0, this.pendingGameEvents.length);\n        for (const instance of this.triggerInstances.values()) {\n            if (!instance.finished && !instance.disabled) {\n                let allConditionsMet = true;\n                const triggeredTargets: MapObject[] = [];\n                for (const condition of instance.conditions) {\n                    const result = condition.check(context, events);\n                    if (typeof result === \"boolean\") {\n                        if (!result) {\n                            allConditionsMet = false;\n                        }\n                    }\n                    else if (result.length) {\n                        triggeredTargets.push(...result);\n                    }\n                    else {\n                        allConditionsMet = false;\n                    }\n                    if (condition.blocking && !allConditionsMet) {\n                        break;\n                    }\n                }\n                if (allConditionsMet) {\n                    const trigger = instance.trigger;\n                    instance.conditions.forEach(condition => condition.reset?.());\n                    let targets: MapObject[] = [];\n                    if (trigger.tag.repeatType === TagRepeatType.OnceAll) {\n                        for (const target of triggeredTargets) {\n                            instance.remainingTargets.delete(target);\n                        }\n                        if (instance.remainingTargets.size) {\n                            continue;\n                        }\n                        targets = triggeredTargets.length ? [triggeredTargets[triggeredTargets.length - 1]] : [];\n                    }\n                    else {\n                        targets = instance.targets;\n                    }\n                    this.executeActions(trigger, targets, context);\n                    if (trigger.tag.repeatType !== TagRepeatType.Repeat) {\n                        instance.finished = true;\n                    }\n                }\n            }\n        }\n    }\n    private executeActions(trigger: Trigger, targets: MapObject[], context: GameContext): void {\n        for (const action of trigger.actions) {\n            const executor = this.executorFactory.create(action, trigger);\n            executor.execute(context, targets as any);\n        }\n    }\n    setTriggerEnabled(triggerId: string, enabled: boolean): void {\n        const instance = this.triggerInstances.get(triggerId);\n        if (instance) {\n            instance.disabled = !enabled;\n        }\n    }\n    forceTrigger(triggerId: string, context: GameContext): void {\n        const instance = this.triggerInstances.get(triggerId);\n        if (instance) {\n            this.executeActions(instance.trigger, instance.targets, context);\n        }\n    }\n    destroyTrigger(triggerId: string): void {\n        this.triggerInstances.delete(triggerId);\n    }\n    destroyTag(tagId: string): void {\n        const triggerIds: string[] = [];\n        for (const [id, instance] of this.triggerInstances) {\n            if (instance.trigger.tag.id === tagId) {\n                triggerIds.push(id);\n            }\n        }\n        for (const id of triggerIds) {\n            this.destroyTrigger(id);\n        }\n    }\n    getGlobalVariable(id: string): boolean {\n        return !!this.globalVariables.get(id)?.value;\n    }\n    toggleGlobalVariable(id: string, value: boolean): void {\n        const variable = this.globalVariables.get(id);\n        if (variable === undefined) {\n            this.globalVariables.set(id, new Variable(\"No name\", value));\n        }\n        else {\n            variable.value = value;\n        }\n    }\n    getLocalVariable(id: string): boolean {\n        return !!this.localVariables.get(id)?.value;\n    }\n    toggleLocalVariable(id: string, value: boolean): void {\n        const variable = this.localVariables.get(id);\n        if (variable === undefined) {\n            this.localVariables.set(id, new Variable(\"No name\", value));\n        }\n        else {\n            variable.value = value;\n        }\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/TriggerTarget.ts",
    "content": "export class TriggerTarget {\n    public id: string;\n    constructor(id: string) {\n        this.id = id;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/AmbientLightCondition.ts",
    "content": "import { TriggerCondition } from '../TriggerCondition';\nexport class AmbientLightCondition extends TriggerCondition {\n    private type: string;\n    private threshold: number;\n    private previousAmbient?: number;\n    constructor(trigger: any, owner: any, type: string) {\n        super(trigger, owner);\n        this.type = type;\n        this.threshold = Number(trigger.params[1]) / 100;\n    }\n    check(context: any): boolean {\n        const previousAmbient = this.previousAmbient;\n        const currentAmbient = context.mapLightingTrait.getAmbient().ambient;\n        this.previousAmbient = currentAmbient;\n        return (previousAmbient !== undefined &&\n            previousAmbient !== currentAmbient &&\n            (this.type === 'above'\n                ? currentAmbient >= this.threshold && previousAmbient < this.threshold\n                : currentAmbient <= this.threshold && previousAmbient > this.threshold));\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/AnyEventCondition.ts",
    "content": "import { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class AnyEventCondition extends TriggerCondition {\n    check(event: any): boolean {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/AttackedByAnyCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class AttackedByAnyCondition extends TriggerCondition {\n    check(gameState: any, events: any[]) {\n        return events\n            .filter((event) => {\n            if (event.type !== EventType.ObjectAttacked)\n                return false;\n            const target = event.target;\n            if (!target.isTechno() || !this.targets.includes(target))\n                return false;\n            const attackerPlayer = event.attacker?.player;\n            return ((!attackerPlayer ||\n                (!gameState.alliances.areAllied(attackerPlayer, target.owner) &&\n                    attackerPlayer !== target.owner)) &&\n                !event.incidental);\n        })\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/AttackedByHouseCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class AttackedByHouseCondition extends TriggerCondition {\n    private houseId: number;\n    constructor(event: any, trigger: any) {\n        super(event, trigger);\n        this.houseId = Number(event.params[1]);\n    }\n    check(context: any, events: any[]): any[] {\n        return events\n            .filter((event) => {\n            if (event.type !== EventType.ObjectAttacked)\n                return false;\n            const target = event.target;\n            if (!target.isTechno() || !this.targets.includes(target))\n                return false;\n            const attacker = event.attacker?.player;\n            return (attacker &&\n                (this.houseId === -1 || attacker?.country?.id === this.houseId));\n        })\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/BuildObjectTypeCondition.ts",
    "content": "import { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nimport { EventType } from \"@/game/event/EventType\";\nexport class BuildObjectTypeCondition extends TriggerCondition {\n    private objectType: any;\n    private objectIndex: number;\n    constructor(params: any[], trigger: any, objectType: any) {\n        super(params, trigger);\n        this.objectType = objectType;\n        this.objectIndex = Number(params[1]);\n    }\n    check(event: any, events: any[]): boolean {\n        return events.some((event) => event.type === EventType.ObjectSpawn &&\n            event.gameObject.type === this.objectType &&\n            event.gameObject.rules.index === this.objectIndex);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/BuildingExistsCondition.ts",
    "content": "import { TriggerCondition } from '../TriggerCondition';\nexport class BuildingExistsCondition extends TriggerCondition {\n    private negate: boolean;\n    private objectIndex: number;\n    constructor(trigger: any, player: any, negate: boolean = false) {\n        super(trigger, player);\n        this.negate = negate;\n        this.objectIndex = Number(trigger.params[1]);\n    }\n    check(): boolean {\n        if (!this.player) {\n            return false;\n        }\n        for (const building of this.player.buildings) {\n            if (building.rules.index === this.objectIndex) {\n                return !this.negate;\n            }\n        }\n        return this.negate;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/ComesNearWaypointCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { RangeHelper } from \"@/game/gameobject/unit/RangeHelper\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class ComesNearWaypointCondition extends TriggerCondition {\n    private waypointTile: any;\n    constructor(event: any, player: any) {\n        super(event, player);\n    }\n    init(game: any): void {\n        super.init(game);\n        const waypointId = Number(this.event.params[1]);\n        this.waypointTile = game.map.getTileAtWaypoint(waypointId);\n        if (!this.waypointTile) {\n            console.warn(`No valid location found for waypoint ${waypointId}. ` +\n                `Skipping event ${this.getDebugName()}.`);\n        }\n    }\n    check(game: any, events: any[]): boolean {\n        if (!this.waypointTile || !this.player) {\n            return false;\n        }\n        for (const event of events) {\n            if (event.type === EventType.EnterTile &&\n                event.source.owner === this.player) {\n                const rangeHelper = new RangeHelper(game.map.tileOccupation);\n                if (rangeHelper.tileDistance(event.target, this.waypointTile) < 2) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/CreditsBelowCondition.ts",
    "content": "import { TriggerCondition } from '../TriggerCondition';\nexport class CreditsBelowCondition extends TriggerCondition {\n    private threshold: number;\n    constructor(params: any[], context: any) {\n        super(params, context);\n        this.threshold = Number(params[1]);\n    }\n    check(event: any, context: any): boolean {\n        return !!this.player && this.player.credits < this.threshold;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/CreditsExceedCondition.ts",
    "content": "import { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class CreditsExceedCondition extends TriggerCondition {\n    private threshold: number;\n    constructor(params: any[], context: any) {\n        super(params, context);\n        this.threshold = Number(params[1]);\n    }\n    check(params: any, context: any): boolean {\n        return !!this.player && this.player.credits > this.threshold;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/CrossHorizLineCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class CrossHorizLineCondition extends TriggerCondition {\n    private houseId: number;\n    constructor(event: any, targets: any) {\n        super(event, targets);\n        this.houseId = Number(this.event.params[1]);\n    }\n    check(event: any, events: any[]): any[] {\n        return events\n            .filter((event) => event.type === EventType.EnterTile &&\n            event.source.zone !== ZoneType.Air &&\n            this.targets.some((target) => target.ry === event.target.ry) &&\n            (-1 === this.houseId ||\n                event.source.owner.country?.id === this.houseId))\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/CrossVertLineCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class CrossVertLineCondition extends TriggerCondition {\n    private houseId: number;\n    constructor(event: any, targets: any) {\n        super(event, targets);\n        this.houseId = Number(this.event.params[1]);\n    }\n    check(event: any, events: any[]): any[] {\n        return events\n            .filter((event) => event.type === EventType.EnterTile &&\n            event.source.zone !== ZoneType.Air &&\n            this.targets.some((target) => target.rx === event.target.rx) &&\n            (-1 === this.houseId ||\n                event.source.owner.country?.id === this.houseId))\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/DestroyedAllBuildingsCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class DestroyedAllBuildingsCondition extends TriggerCondition {\n    private allDestroyed: boolean = false;\n    private houseId: number;\n    constructor(params: any[], trigger: any) {\n        super(params, trigger);\n        this.houseId = Number(params[1]);\n    }\n    check(event: any, events: any[]): boolean {\n        if (this.allDestroyed) {\n            return true;\n        }\n        const hasDestroyedAll = events.some((event) => {\n            if (event.type !== EventType.ObjectDestroy) {\n                return false;\n            }\n            const target = event.target;\n            const isTargetBuilding = target.isBuilding();\n            const isTargetOwner = target.owner.country?.id === this.houseId;\n            const hasNoBuildings = !target.owner.buildings.size;\n            return isTargetBuilding && isTargetOwner && hasNoBuildings;\n        });\n        if (hasDestroyedAll) {\n            this.allDestroyed = true;\n        }\n        return hasDestroyedAll;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/DestroyedAllCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class DestroyedAllCondition extends TriggerCondition {\n    private allDestroyed: boolean;\n    private houseId: number;\n    constructor(params: any[], trigger: any) {\n        super(params, trigger);\n        this.allDestroyed = false;\n        this.houseId = Number(params[1]);\n    }\n    check(event: any, events: any[]): boolean {\n        if (this.allDestroyed) {\n            return true;\n        }\n        const hasDestroyedAll = events.some((event) => {\n            if (event.type !== EventType.ObjectDestroy) {\n                return false;\n            }\n            const target = event.target;\n            const isTargetTechno = target.isTechno();\n            const isTargetOwner = target.owner.country?.id === this.houseId;\n            const hasNoRemainingObjects = !target.owner.getOwnedObjects(true).length;\n            return isTargetTechno && isTargetOwner && hasNoRemainingObjects;\n        });\n        if (hasDestroyedAll) {\n            this.allDestroyed = true;\n        }\n        return hasDestroyedAll;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/DestroyedAllUnitsCondition.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class DestroyedAllUnitsCondition extends TriggerCondition {\n    private allDestroyed: boolean;\n    private houseId: number;\n    constructor(params: any[], trigger: any) {\n        super(params, trigger);\n        this.allDestroyed = false;\n        this.houseId = Number(params[1]);\n    }\n    check(events: any[], context: any): boolean {\n        if (this.allDestroyed) {\n            return true;\n        }\n        const hasDestroyedAll = events.some((event) => {\n            if (event.type !== EventType.ObjectDestroy) {\n                return false;\n            }\n            const target = event.target;\n            if (!target.isUnit() || target.owner.country?.id !== this.houseId) {\n                return false;\n            }\n            return !this.hasUnitsLeft(target.owner);\n        });\n        if (hasDestroyedAll) {\n            this.allDestroyed = true;\n        }\n        return hasDestroyedAll;\n    }\n    private hasUnitsLeft(owner: any): boolean {\n        const unitTypes = [\n            ObjectType.Aircraft,\n            ObjectType.Vehicle,\n            ObjectType.Infantry,\n        ];\n        for (const type of unitTypes) {\n            if (owner.getOwnedObjectsByType(type, true).length) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/DestroyedAllUnitsLandCondition.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class DestroyedAllUnitsLandCondition extends TriggerCondition {\n    private allDestroyed: boolean;\n    private houseId: number;\n    constructor(params: any, context: any) {\n        super(params, context);\n        this.allDestroyed = false;\n        this.houseId = Number(params[1]);\n    }\n    check(events: any, eventList: any[]): boolean {\n        if (this.allDestroyed) {\n            return true;\n        }\n        const hasDestroyedAll = eventList.some((event) => {\n            if (event.type !== EventType.ObjectDestroy) {\n                return false;\n            }\n            const target = event.target;\n            if (!target.isUnit() || target.owner.country?.id !== this.houseId) {\n                return false;\n            }\n            return !this.hasLandUnitsLeft(target.owner);\n        });\n        if (hasDestroyedAll) {\n            this.allDestroyed = true;\n        }\n        return hasDestroyedAll;\n    }\n    private hasLandUnitsLeft(owner: any): boolean {\n        for (const type of [ObjectType.Vehicle, ObjectType.Infantry]) {\n            const units = owner.getOwnedObjectsByType(type, true).filter((unit: any) => !unit.rules.naval);\n            if (units.length > 0) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/DestroyedAllUnitsNavalCondition.ts",
    "content": "import { ObjectType } from \"@/engine/type/ObjectType\";\nimport { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class DestroyedAllUnitsNavalCondition extends TriggerCondition {\n    private allDestroyed: boolean;\n    private houseId: number;\n    constructor(params: any[], context: any) {\n        super(params, context);\n        this.allDestroyed = false;\n        this.houseId = Number(params[1]);\n    }\n    check(event: any, events: any[]): boolean {\n        if (this.allDestroyed) {\n            return true;\n        }\n        const hasDestroyedAll = events.some((event) => {\n            if (event.type !== EventType.ObjectDestroy) {\n                return false;\n            }\n            const target = event.target;\n            if (!target.isVehicle() || target.owner.country?.id !== this.houseId) {\n                return false;\n            }\n            const remainingNavalUnits = target.owner\n                .getOwnedObjectsByType(ObjectType.Vehicle, true)\n                .filter((unit) => unit.rules.naval).length;\n            return remainingNavalUnits === 0;\n        });\n        if (hasDestroyedAll) {\n            this.allDestroyed = true;\n        }\n        return hasDestroyedAll;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/DestroyedBridgeCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class DestroyedBridgeCondition extends TriggerCondition {\n    check(context: any, events: any[]): any[] {\n        return events\n            .filter((event) => {\n            if (event.type !== EventType.ObjectDestroy)\n                return false;\n            const target = event.target;\n            if (!target.isOverlay() || !target.isBridge())\n                return false;\n            const bridgeSpec = target.bridgeTrait?.bridgeSpec;\n            if (!bridgeSpec)\n                return false;\n            const bridgeTiles = context.map.bridges.findAllBridgeTiles(bridgeSpec);\n            return bridgeTiles.find((tile) => this.targets.includes(tile));\n        })\n            .map((event) => event.target.tile);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/DestroyedBuildingsCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class DestroyedBuildingsCondition extends TriggerCondition {\n    private count: number;\n    private threshold: number;\n    private houseId: number;\n    constructor(params: any[], trigger: any) {\n        super(params, trigger);\n        this.count = 0;\n        this.threshold = Number(params[1]);\n    }\n    check(context: any, events: any[]): boolean {\n        if (!this.player) {\n            return false;\n        }\n        if (this.count >= this.threshold) {\n            return true;\n        }\n        for (const event of events) {\n            if (event.type === EventType.ObjectDestroy) {\n                const target = event.target;\n                if (target.isBuilding() && target.owner.country?.id === this.houseId) {\n                    this.count++;\n                }\n            }\n        }\n        return this.count >= this.threshold;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/DestroyedByAnyCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class DestroyedByAnyCondition extends TriggerCondition {\n    check(context: any, events: any[]): any[] {\n        return events\n            .filter((event) => {\n            if (event.type !== EventType.ObjectDestroy)\n                return false;\n            const target = event.target;\n            if (!target.isTechno() || !this.targets.includes(target))\n                return false;\n            const attacker = event.attackerInfo?.player;\n            return ((!attacker ||\n                (!context.alliances.areAllied(attacker, target.owner) &&\n                    attacker !== target.owner)) &&\n                !event.incidental);\n        })\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/DestroyedOrCapturedCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class DestroyedOrCapturedCondition extends TriggerCondition {\n    check(events: any[], targets: any[]) {\n        return targets\n            .filter((event) => {\n            if (event.type !== EventType.ObjectDestroy &&\n                event.type !== EventType.ObjectOwnerChange) {\n                return false;\n            }\n            const target = event.target;\n            return !(!target.isTechno() || !this.targets.includes(target));\n        })\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/DestroyedOrCapturedOrInfiltratedCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class DestroyedOrCapturedOrInfiltratedCondition extends TriggerCondition {\n    private eventsFilter: EventType[];\n    constructor(event?: any, trigger?: any) {\n        super(event ?? null, trigger ?? null);\n        this.eventsFilter = [\n            EventType.ObjectDestroy,\n            EventType.ObjectOwnerChange,\n            EventType.BuildingInfiltration\n        ];\n    }\n    check(event: any, events: any[]): any[] {\n        return events\n            .filter(event => {\n            if (!this.eventsFilter.includes(event.type)) {\n                return false;\n            }\n            const target = event.target;\n            return !(!target.isTechno() || !this.targets.includes(target));\n        })\n            .map(event => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/DestroyedUnitsCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class DestroyedUnitsCondition extends TriggerCondition {\n    private count: number = 0;\n    private threshold: number;\n    private houseId: number;\n    constructor(params: any, context: any) {\n        super(params, context);\n        this.threshold = Number(params[1]);\n    }\n    check(events: any, eventList: any[]): boolean {\n        if (!this.player)\n            return false;\n        if (this.count >= this.threshold)\n            return true;\n        for (const event of eventList) {\n            if (event.type === EventType.ObjectDestroy) {\n                const target = event.target;\n                if (target.isUnit() && target.owner.country?.id === this.houseId) {\n                    this.count++;\n                }\n            }\n        }\n        return this.count >= this.threshold;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/ElapsedScenarioTimeCondition.ts",
    "content": "import { GameSpeed } from \"@/game/GameSpeed\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class ElapsedScenarioTimeCondition extends TriggerCondition {\n    private timerTicks: number;\n    constructor(event: any, trigger: any) {\n        super(event, trigger);\n        this.timerTicks = Number(this.event.params[1]) * GameSpeed.BASE_TICKS_PER_SECOND;\n    }\n    check(context: any): boolean {\n        return context.currentTick > this.timerTicks;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/ElapsedTimeCondition.ts",
    "content": "import { GameSpeed } from \"@/game/GameSpeed\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class ElapsedTimeCondition extends TriggerCondition {\n    private elapsedTicks: number;\n    private timerTicks: number;\n    constructor(event: any, trigger: any) {\n        super(event, trigger);\n        this.elapsedTicks = 0;\n        this.timerTicks = Number(this.event.params[1]) * GameSpeed.BASE_TICKS_PER_SECOND;\n    }\n    check(context: any): boolean {\n        return this.elapsedTicks++ > this.timerTicks;\n    }\n    reset(): void {\n        this.elapsedTicks = 0;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/EnteredByCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class EnteredByCondition extends TriggerCondition {\n    private houseId: number;\n    constructor(event: any, targets: any) {\n        super(event, targets);\n        this.houseId = Number(this.event.params[1]);\n    }\n    check(event: any, events: any[]): any[] {\n        return events\n            .filter((event) => (event.type === EventType.EnterObject ||\n            event.type === EventType.EnterTile) &&\n            this.targets.includes(event.target) &&\n            (event.type !== EventType.EnterTile ||\n                event.source.zone !== ZoneType.Air) &&\n            (-1 === this.houseId ||\n                event.source.owner.country?.id === this.houseId))\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/GlobalVariableCondition.ts",
    "content": "import { TriggerCondition } from '../TriggerCondition';\nexport class GlobalVariableCondition extends TriggerCondition {\n    private value: any;\n    private variableIdx: number;\n    constructor(trigger: any, type: any, value: any) {\n        super(trigger, type);\n        this.value = value;\n        this.blocking = true;\n        this.variableIdx = Number(trigger.params[1]);\n    }\n    check(context: any): boolean {\n        return context.triggers.getGlobalVariable(this.variableIdx) === this.value;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/HealthBelowAnyCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class HealthBelowAnyCondition extends TriggerCondition {\n    private threshold: number;\n    constructor(id: string, targets: string[], threshold: number) {\n        super(id, targets);\n        this.threshold = threshold;\n    }\n    check(events: any[], targets: any[]): any[] {\n        return events\n            .filter((event) => {\n            if (event.type !== EventType.HealthChange)\n                return false;\n            const target = event.target;\n            return (!(!target.isTechno() || !this.targets.includes(target)) &&\n                event.currentHealth < this.threshold &&\n                event.prevHealth > this.threshold);\n        })\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/HealthBelowCombatCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class HealthBelowCombatCondition extends TriggerCondition {\n    private threshold: number;\n    constructor(id: string, targets: any[], threshold: number) {\n        super(id, targets);\n        this.threshold = threshold;\n    }\n    check(events: any[], targets: any[]): any[] {\n        return targets\n            .filter((event) => {\n            if (event.type !== EventType.InflictDamage)\n                return false;\n            const target = event.target;\n            return (!(!target.isTechno() || !this.targets.includes(target)) &&\n                event.currentHealth < this.threshold &&\n                event.prevHealth > this.threshold);\n        })\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/LocalVariableCondition.ts",
    "content": "import { TriggerCondition } from '../TriggerCondition';\nexport class LocalVariableCondition extends TriggerCondition {\n    private value: any;\n    private variableIdx: number;\n    constructor(trigger: any, type: any, value: any) {\n        super(trigger, type);\n        this.value = value;\n        this.blocking = true;\n        this.variableIdx = Number(trigger.params[1]);\n    }\n    check(context: any): boolean {\n        return context.triggers.getLocalVariable(this.variableIdx) === this.value;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/LowPowerCondition.ts",
    "content": "import { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class LowPowerCondition extends TriggerCondition {\n    private houseId: number;\n    private targetPlayer?: any;\n    constructor(event: any, trigger: any) {\n        super(event, trigger);\n        this.houseId = Number(this.event.params[1]);\n    }\n    init(game: any): void {\n        super.init(game);\n        this.targetPlayer = game\n            .getAllPlayers()\n            .find((player: any) => player.country?.id === this.houseId);\n    }\n    check(): boolean {\n        return !!this.targetPlayer?.powerTrait?.isLowPower();\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/NoEventCondition.ts",
    "content": "import { TriggerCondition } from \"../TriggerCondition\";\nexport class NoEventCondition extends TriggerCondition {\n    check(): boolean {\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/NoFactoriesLeftCondition.ts",
    "content": "import { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class NoFactoriesLeftCondition extends TriggerCondition {\n    check(): boolean {\n        if (!this.player)\n            return false;\n        for (const building of this.player.buildings) {\n            if (building.factoryTrait)\n                return false;\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/PickupCrateAnyCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class PickupCrateAnyCondition extends TriggerCondition {\n    check(event: any, events: any[]): boolean {\n        return events.some((event) => event.type === EventType.CratePickup);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/PickupCrateCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class PickupCrateCondition extends TriggerCondition {\n    check(e: any, t: any[]) {\n        return t\n            .filter((e) => e.type === EventType.CratePickup &&\n            this.targets.includes(e.source))\n            .map((e) => e.source);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/RandomDelayCondition.ts",
    "content": "import { GameSpeed } from \"@/game/GameSpeed\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class RandomDelayCondition extends TriggerCondition {\n    private elapsedTicks: number = 0;\n    private timerTicks?: number;\n    check(e: any): boolean {\n        if (!this.timerTicks) {\n            this.timerTicks = Math.floor((e.generateRandomInt(50, 150) / 100) * Number(this.event.params[1])) * GameSpeed.BASE_TICKS_PER_SECOND;\n        }\n        return this.elapsedTicks++ > this.timerTicks;\n    }\n    reset(): void {\n        this.timerTicks = undefined;\n        this.elapsedTicks = 0;\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/SpiedByCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class SpiedByCondition extends TriggerCondition {\n    private houseId: number;\n    constructor(event: any, targets: any[]) {\n        super(event, targets);\n        this.houseId = Number(this.event.params[1]);\n    }\n    check(event: any, events: any[]): any[] {\n        return events\n            .filter((event) => event.type === EventType.BuildingInfiltration &&\n            this.targets.includes(event.target) &&\n            (this.houseId === -1 ||\n                event.source.owner.country?.id === this.houseId))\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/SpyEnteringAsHouseCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class SpyEnteringAsHouseCondition extends TriggerCondition {\n    private houseId: number;\n    constructor(params: any[], targets: any[]) {\n        super(params, targets);\n        this.houseId = Number(params[1]);\n    }\n    check(events: any[], targets: any[]): any[] {\n        return events\n            .filter((event) => {\n            if (event.type !== EventType.BuildingInfiltration)\n                return false;\n            const target = event.target;\n            return (this.targets.includes(target) &&\n                (this.houseId === -1 ||\n                    event.source.disguiseTrait?.getDisguise()?.owner?.country?.id === this.houseId));\n        })\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/SpyEnteringAsInfantryCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class SpyEnteringAsInfantryCondition extends TriggerCondition {\n    private infantryIdx: number;\n    constructor(params: any[], targets: any[]) {\n        super(params, targets);\n        this.infantryIdx = Number(params[1]);\n    }\n    check(events: any[], targets: any[]): any[] {\n        return events\n            .filter((event) => {\n            if (event.type !== EventType.BuildingInfiltration)\n                return false;\n            const target = event.target;\n            return (this.targets.includes(target) &&\n                event.source.disguiseTrait?.getDisguise()?.rules.index === this.infantryIdx);\n        })\n            .map((event) => event.target);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/condition/TimerExpiredCondition.ts",
    "content": "import { EventType } from \"@/game/event/EventType\";\nimport { TriggerCondition } from \"@/game/trigger/TriggerCondition\";\nexport class TimerExpiredCondition extends TriggerCondition {\n    check(event: any, events: any[]): boolean {\n        return events.some((event) => event.type === EventType.TimerExpire);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/AddSuperWeaponExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class AddSuperWeaponExecutor extends TriggerExecutor {\n    private oneTimeOnly: boolean;\n    private superWeaponIdx: number;\n    constructor(action: any, trigger: any, oneTimeOnly: boolean) {\n        super(action, trigger);\n        this.oneTimeOnly = oneTimeOnly;\n        this.superWeaponIdx = Number(action.params[1]);\n    }\n    execute(context: any): void {\n        const superWeaponRule = [...context.rules.superWeaponRules.values()].find((rule) => rule.index === this.superWeaponIdx);\n        if (superWeaponRule) {\n            const player = context\n                .getAllPlayers()\n                .find((p) => p.country?.name === this.trigger.houseName);\n            if (player &&\n                player.superWeaponsTrait &&\n                !player.superWeaponsTrait.has(superWeaponRule.name)) {\n                const superWeapon = context.createSuperWeapon(superWeaponRule.name, player, this.oneTimeOnly);\n                superWeapon.isGift = true;\n                player.superWeaponsTrait.add(superWeapon);\n            }\n        }\n        else {\n            console.warn(`No superweapon found with index \"${this.superWeaponIdx}\". ` +\n                `Skipping action ${this.getDebugName()}.`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/ApplyDamageExecutor.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport { CollisionType } from '@/game/gameobject/unit/CollisionType';\nimport { Warhead } from '@/game/Warhead';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class ApplyDamageExecutor extends TriggerExecutor {\n    private damage: number;\n    constructor(action: any, trigger: any, damage: number) {\n        super(action, trigger);\n        this.damage = damage;\n    }\n    execute(context: any): void {\n        const waypoint = Number(this.action.params[1]);\n        const tile = context.map.getTileAtWaypoint(waypoint);\n        if (tile) {\n            const warheadRule = context.rules.getWarhead(Warhead.HE_WARHEAD_NAME);\n            const warhead = new Warhead(warheadRule);\n            const bridge = context.map.tileOccupation.getBridgeOnTile(tile);\n            const elevation = bridge?.tileElevation ?? 0;\n            const zone = context.map.getTileZone(tile);\n            warhead.detonate(context, this.damage, tile, elevation, Coords.tile3dToWorld(tile.rx + 0.5, tile.ry + 0.5, tile.z + elevation), zone, bridge ? CollisionType.OnBridge : CollisionType.None, context.createTarget(bridge, tile), undefined, false, undefined, undefined);\n        }\n        else {\n            console.warn(`No valid location found for waypoint ${waypoint}. ` +\n                `Skipping action ${this.getDebugName()}.`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/ChangeHouseAllExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class ChangeHouseAllExecutor extends TriggerExecutor {\n    private static readonly locationHouseIdBegin: number = 4475;\n    public execute(game: any): void {\n        const sourcePlayer = game\n            .getAllPlayers()\n            .find((player: any) => player.country?.name === this.trigger.houseName);\n        if (!sourcePlayer) {\n            return;\n        }\n        const targetHouseId = Number(this.action.params[1]);\n        let targetPlayer;\n        if (targetHouseId >= ChangeHouseAllExecutor.locationHouseIdBegin &&\n            targetHouseId < ChangeHouseAllExecutor.locationHouseIdBegin + game.map.startingLocations.length) {\n            const locationIndex = targetHouseId - ChangeHouseAllExecutor.locationHouseIdBegin;\n            targetPlayer = game.getAllPlayers().find((player: any) => player.startLocation === locationIndex);\n        }\n        else {\n            targetPlayer = game.getAllPlayers().find((player: any) => player.country?.id === targetHouseId);\n        }\n        if (!targetPlayer) {\n            return;\n        }\n        for (const ownedObject of sourcePlayer.getOwnedObjects(true)) {\n            game.changeObjectOwner(ownedObject, targetPlayer);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/ChangeHouseExecutor.ts",
    "content": "import { GameObject } from '@/game/gameobject/GameObject';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class ChangeHouseExecutor extends TriggerExecutor {\n    private static readonly locationHouseIdBegin: number = 4475;\n    private readonly houseId: number;\n    constructor(params: string[], context: any) {\n        super(params, context);\n        this.houseId = Number(params[1]);\n    }\n    execute(game: any, objects: any[]): void {\n        let targetPlayer;\n        if (this.houseId >= ChangeHouseExecutor.locationHouseIdBegin &&\n            this.houseId < ChangeHouseExecutor.locationHouseIdBegin + game.map.startingLocations.length) {\n            const locationIndex = this.houseId - ChangeHouseExecutor.locationHouseIdBegin;\n            targetPlayer = game.getAllPlayers().find((player: any) => player.startLocation === locationIndex);\n        }\n        else {\n            targetPlayer = game.getAllPlayers().find((player: any) => player.country?.id === this.houseId);\n        }\n        if (targetPlayer) {\n            for (const obj of objects) {\n                if (obj instanceof GameObject && obj.isSpawned) {\n                    game.changeObjectOwner(obj, targetPlayer);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/CheerExecutor.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { CheerTask } from '@/game/gameobject/task/CheerTask';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class CheerExecutor extends TriggerExecutor {\n    private houseId: number;\n    constructor(action: any, trigger: any) {\n        super(action, trigger);\n        this.houseId = Number(action.params[1]);\n    }\n    execute(context: any): void {\n        let players = context.getAllPlayers().filter((player: any) => player.country && !player.defeated);\n        if (this.houseId !== -1) {\n            players = players.filter((player: any) => player.country?.id === this.houseId);\n        }\n        if (players.length) {\n            for (const infantry of players[0].getOwnedObjectsByType(ObjectType.Infantry)) {\n                if (infantry.unitOrderTrait.isIdle()) {\n                    infantry.unitOrderTrait.addTask(new CheerTask());\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/CreateCrateExecutor.ts",
    "content": "import { PowerupType } from '@/game/type/PowerupType';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nconst powerupTypeMap = new Map<number, PowerupType | ((game: any) => any)>([\n    [\n        0,\n        (game) => {\n            const powerup = game.rules.powerups.powerups.find((p: any) => p.type === PowerupType.Money);\n            return powerup ? { ...powerup, data: \"5000\" } : undefined;\n        },\n    ],\n    [1, PowerupType.Unit],\n    [2, PowerupType.HealBase],\n    [3, PowerupType.Cloak],\n    [4, PowerupType.Explosion],\n    [5, PowerupType.Napalm],\n    [6, PowerupType.Money],\n    [7, PowerupType.Darkness],\n    [8, PowerupType.Reveal],\n    [9, PowerupType.Armor],\n    [10, PowerupType.Speed],\n    [11, PowerupType.Firepower],\n    [12, PowerupType.ICBM],\n    [13, undefined],\n    [14, PowerupType.Veteran],\n    [15, undefined],\n    [16, PowerupType.Gas],\n    [17, PowerupType.Tiberium],\n    [18, undefined],\n]);\nexport class CreateCrateExecutor extends TriggerExecutor {\n    execute(game: any): void {\n        const typeId = Number(this.action.params[1]);\n        const waypointId = this.action.params[6];\n        const tile = game.map.getTileAtWaypoint(waypointId);\n        if (!tile) {\n            console.warn(`No valid location found for waypoint ${waypointId}. ` +\n                `Skipping action ${this.getDebugName()}.`);\n            return;\n        }\n        if (powerupTypeMap.has(typeId)) {\n            const powerupType = powerupTypeMap.get(typeId);\n            const powerup = typeof powerupType === 'function'\n                ? powerupType(game)\n                : game.rules.powerups.powerups.find((p: any) => p.type === powerupType);\n            if (powerup) {\n                game.crateGeneratorTrait.spawnCrateAt(tile, powerup, game);\n            }\n        }\n        else {\n            game.crateGeneratorTrait.spawnRandomCrateAt(tile, game);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/CreateRadarEventExecutor.ts",
    "content": "import { Game } from '@/game/Game';\nimport { RadarEventType } from '@/game/rules/general/RadarRules';\nimport { RadarTrait } from '@/game/trait/RadarTrait';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class CreateRadarEventExecutor extends TriggerExecutor {\n    execute(game: Game): void {\n        const eventType = Number(this.action.params[1]) - 1;\n        if (Object.values(RadarEventType).includes(eventType)) {\n            const waypointId = this.action.params[6];\n            const tile = game.map.getTileAtWaypoint(waypointId);\n            if (tile) {\n                for (const combatant of game.getCombatants()) {\n                    game.traits.get(RadarTrait).addEventForPlayer(eventType, combatant, tile, game);\n                }\n            }\n            else {\n                console.warn(`No valid location found for waypoint ${waypointId}. ` +\n                    `Skipping action ${this.getDebugName()}.`);\n            }\n        }\n        else {\n            console.warn(`Unknown radar event type \"${1 + eventType}\". Skipping action ${this.getDebugName()}.`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/DestroyObjectExecutor.ts",
    "content": "import { GameObject } from '@/game/gameobject/GameObject';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class DestroyObjectExecutor extends TriggerExecutor {\n    execute(context: any, targets: any[]): void {\n        for (const target of targets) {\n            if (target instanceof GameObject && target.isSpawned) {\n                context.destroyObject(target);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/DestroyTagExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class DestroyTagExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        const tagId = this.action.params[1];\n        context.triggers.destroyTag(tagId);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/DestroyTriggerExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class DestroyTriggerExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        const triggerId = this.action.params[1];\n        context.triggers.destroyTrigger(triggerId);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/DetonateWarheadExecutor.ts",
    "content": "import { Coords } from '@/game/Coords';\nimport { CollisionType } from '@/game/gameobject/unit/CollisionType';\nimport { Warhead } from '@/game/Warhead';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class DetonateWarheadExecutor extends TriggerExecutor {\n    execute(game: any): void {\n        const weaponId = Number(this.action.params[1]);\n        const waypointId = this.action.params[6];\n        const tile = game.map.getTileAtWaypoint(waypointId);\n        if (!tile) {\n            console.warn(`No valid location found for waypoint ${waypointId}. ` +\n                `Skipping action ${this.getDebugName()}.`);\n            return;\n        }\n        let weapon;\n        try {\n            weapon = game.rules.getWeaponByInternalId(weaponId);\n        }\n        catch (error) {\n            if (error instanceof RangeError) {\n                console.warn(`Weapon with internal ID \"${weaponId}\" not found. ` +\n                    `Skipping action ${this.getDebugName()}.`);\n                return;\n            }\n            throw error;\n        }\n        let warheadData;\n        try {\n            warheadData = game.rules.getWarhead(weapon.warhead);\n        }\n        catch (error) {\n            console.warn(`Warhead \"${weapon.warhead}\" not found. ` +\n                `Skipping action ${this.getDebugName()}.`);\n            return;\n        }\n        const warhead = new Warhead(warheadData);\n        const bridge = game.map.tileOccupation.getBridgeOnTile(tile);\n        const elevation = bridge?.tileElevation ?? 0;\n        const zone = game.map.getTileZone(tile);\n        warhead.detonate(game, weapon.damage, tile, elevation, Coords.tile3dToWorld(tile.rx + 0.5, tile.ry + 0.5, tile.z + elevation), zone, bridge ? CollisionType.OnBridge : CollisionType.None, game.createTarget(bridge, tile), undefined, false, undefined, undefined);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/EvictOccupiersExecutor.ts",
    "content": "import { GameObject } from '@/game/gameobject/GameObject';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class EvictOccupiersExecutor extends TriggerExecutor {\n    execute(game: any, targets: GameObject[]): void {\n        for (const target of targets) {\n            if (target instanceof GameObject &&\n                target.isBuilding() &&\n                target.garrisonTrait &&\n                !target.isDestroyed) {\n                target.garrisonTrait.evacuate(game);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/FireSaleExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nimport { Game } from '@/game/Game';\nimport { Player } from '@/game/Player';\nimport { Building } from '@/game/Building';\nexport class FireSaleExecutor extends TriggerExecutor {\n    private readonly houseId: number;\n    constructor(params: string[], game: Game) {\n        super(params, game);\n        this.houseId = Number(params[1]);\n    }\n    execute(game: Game): void {\n        const targetPlayer = game.getAllPlayers().find((player: Player) => player.country?.id === this.houseId as any);\n        if (targetPlayer) {\n            for (const building of targetPlayer.buildings) {\n                game.sellTrait.sell(building);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/ForceEndExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class ForceEndExecutor extends TriggerExecutor {\n    execute(trigger: any): void {\n        trigger.end();\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/ForceTriggerExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class ForceTriggerExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        const triggerId = this.action.params[1];\n        context.triggers.forceTrigger(triggerId, context);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/GlobalVariableExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class GlobalVariableExecutor extends TriggerExecutor {\n    private value: any;\n    private variableIdx: number;\n    constructor(params: any, context: any, value: any) {\n        super(params, context);\n        this.value = value;\n        this.variableIdx = Number(params[1]);\n    }\n    execute(context: any): void {\n        context.triggers.toggleGlobalVariable(this.variableIdx, this.value);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/IronCurtainExecutor.ts",
    "content": "import { SuperWeaponsTrait } from '@/game/trait/SuperWeaponsTrait';\nimport { SuperWeaponType } from '@/game/type/SuperWeaponType';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class IronCurtainExecutor extends TriggerExecutor {\n    execute(game: Game): void {\n        const waypoint = this.action.params[6];\n        const tile = game.map.getTileAtWaypoint(waypoint);\n        if (!tile) {\n            console.warn(`No valid location found for waypoint ${waypoint}. ` +\n                `Skipping action ${this.getDebugName()}.`);\n            return;\n        }\n        const player = game.getAllPlayers().find((p) => !p.defeated && p.country?.name === this.trigger.houseName);\n        if (!player) {\n            return;\n        }\n        const ironCurtainRule = [...game.rules.superWeaponRules.values()].find((rule) => rule.type === SuperWeaponType.IronCurtain);\n        if (ironCurtainRule) {\n            game.traits\n                .get(SuperWeaponsTrait)\n                .activateEffect(ironCurtainRule, player, game, tile, undefined, true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/LightningStrikeExecutor.ts",
    "content": "import { SuperWeaponsTrait } from '@/game/trait/SuperWeaponsTrait';\nimport { SuperWeaponType } from '@/game/type/SuperWeaponType';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class LightningStrikeExecutor extends TriggerExecutor {\n    execute(game: Game): void {\n        const waypoint = this.action.params[6];\n        const targetTile = game.map.getTileAtWaypoint(waypoint);\n        if (!targetTile) {\n            console.warn(`No valid location found for waypoint ${waypoint}. ` +\n                `Skipping action ${this.getDebugName()}.`);\n            return;\n        }\n        const player = game.getAllPlayers().find((p) => !p.defeated && p.country?.name === this.trigger.houseName);\n        if (!player) {\n            return;\n        }\n        const lightningStormRule = [...game.rules.superWeaponRules.values()].find((rule) => rule.type === SuperWeaponType.LightningStorm);\n        if (lightningStormRule) {\n            game.traits\n                .get(SuperWeaponsTrait)\n                .activateEffect(lightningStormRule, player, game, targetTile, undefined, true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/LocalVariableExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class LocalVariableExecutor extends TriggerExecutor {\n    private value: boolean;\n    private variableIdx: number;\n    constructor(trigger: any, context: any, value: boolean) {\n        super(trigger, context);\n        this.value = value;\n        this.variableIdx = Number(trigger.params[1]);\n    }\n    execute(context: any): void {\n        context.triggers.toggleLocalVariable(this.variableIdx, this.value);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/NoActionExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class NoActionExecutor extends TriggerExecutor {\n    execute(): void { }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/NukeStrikeExecutor.ts",
    "content": "import { SuperWeaponsTrait } from '@/game/trait/SuperWeaponsTrait';\nimport { SuperWeaponType } from '@/game/type/SuperWeaponType';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class NukeStrikeExecutor extends TriggerExecutor {\n    execute(game: Game): void {\n        const waypoint = this.action.params[6];\n        const targetTile = game.map.getTileAtWaypoint(waypoint);\n        if (!targetTile) {\n            console.warn(`No valid location found for waypoint ${waypoint}. ` +\n                `Skipping action ${this.getDebugName()}.`);\n            return;\n        }\n        const player = game.getAllPlayers().find((p) => !p.defeated && p.country?.name === this.trigger.houseName);\n        if (!player) {\n            return;\n        }\n        const superWeapon = [...game.rules.superWeaponRules.values()].find((sw) => sw.type === SuperWeaponType.MultiMissile);\n        if (superWeapon) {\n            game.traits\n                .get(SuperWeaponsTrait)\n                .activateEffect(superWeapon, player, game, targetTile, undefined, true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/PlayAnimAtExecutor.ts",
    "content": "import { TriggerAnimEvent } from '@/game/event/TriggerAnimEvent';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class PlayAnimAtExecutor extends TriggerExecutor {\n    execute(context: any) {\n        const action = this.action;\n        const animIndex = Number(action.params[1]);\n        const animName = context.rules.getAnimationName(animIndex);\n        if (animName !== undefined) {\n            const waypoint = action.params[6];\n            const tile = context.map.getTileAtWaypoint(waypoint);\n            if (tile) {\n                context.events.dispatch(new TriggerAnimEvent(animName, tile));\n            }\n            else {\n                console.warn(`No valid location found for waypoint ${waypoint}. ` +\n                    `Skipping action ${this.getDebugName()}.`);\n            }\n        }\n        else {\n            console.warn(`No animation found for index \"${animIndex}\". Skipping action ` +\n                this.getDebugName());\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/PlaySoundFxAtExecutor.ts",
    "content": "import { TriggerSoundFxEvent } from '@/game/event/TriggerSoundFxEvent';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class PlaySoundFxAtExecutor extends TriggerExecutor {\n    execute(game: Game): void {\n        const soundIndex = this.action.params[1];\n        const waypoint = this.action.params[6];\n        const tile = game.map.getTileAtWaypoint(waypoint);\n        if (tile) {\n            game.events.dispatch(new TriggerSoundFxEvent(soundIndex, tile));\n        }\n        else {\n            console.warn(`No valid location found for waypoint ${waypoint}. ` +\n                `Skipping action ${this.getDebugName()}.`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/PlaySoundFxExecutor.ts",
    "content": "import { TriggerSoundFxEvent } from '@/game/event/TriggerSoundFxEvent';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class PlaySoundFxExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        context.events.dispatch(new TriggerSoundFxEvent(this.action.params[1]));\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/PlaySpeechExecutor.ts",
    "content": "import { TriggerEvaEvent } from '@/game/event/TriggerEvaEvent';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class PlaySpeechExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        context.events.dispatch(new TriggerEvaEvent(this.action.params[1]));\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/ReshroudMapExecutor.ts",
    "content": "import { Game } from '@/game/Game';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class ReshroudMapExecutor extends TriggerExecutor {\n    execute(game: Game): void {\n        for (const combatant of game.getCombatants()) {\n            game.mapShroudTrait.resetShroud(combatant, game);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/ResizePlayerViewExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class ResizePlayerViewExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        const [x, y, width, height] = this.action.params.slice(2, 6).map(Number);\n        context.map.mapBounds.updateRawLocalSize({\n            x,\n            y,\n            width,\n            height,\n        });\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/RevealAroundWaypointExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class RevealAroundWaypointExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        const waypointId = Number(this.action.params[1]);\n        const tile = context.map.getTileAtWaypoint(waypointId);\n        if (tile) {\n            for (const combatant of context.getCombatants()) {\n                context.mapShroudTrait\n                    .getPlayerShroud(combatant)\n                    ?.revealAround(tile, context.rules.general.revealTriggerRadius);\n            }\n        }\n        else {\n            console.warn(`No valid location found for waypoint ${waypointId}. ` +\n                `Skipping action ${this.getDebugName()}.`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/RevealMapExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class RevealMapExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        for (const combatant of context.getCombatants()) {\n            context.mapShroudTrait.revealMap(combatant, context);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/SellBuildingExecutor.ts",
    "content": "import { GameObject } from '@/game/gameobject/GameObject';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class SellBuildingExecutor extends TriggerExecutor {\n    execute(trigger: any, targets: GameObject[]): void {\n        for (const target of targets) {\n            if (target instanceof GameObject &&\n                target.isBuilding() &&\n                !target.isDestroyed) {\n                trigger.sellTrait.sell(target);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/SetAmbientLightExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class SetAmbientLightExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        const intensity = Number(this.action.params[1]) / 100;\n        context.mapLightingTrait.setTargetAmbientIntensity(intensity);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/SetAmbientRateExecutor.ts",
    "content": "import { int32ToFloat32 } from '@/util/number';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class SetAmbientRateExecutor extends TriggerExecutor {\n    execute(context: any) {\n        const rate = int32ToFloat32(Number(this.action.params[1]));\n        context.mapLightingTrait.setAmbientChangeRate(rate);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/SetAmbientStepExecutor.ts",
    "content": "import { int32ToFloat32 } from '@/util/number';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class SetAmbientStepExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        const step = int32ToFloat32(Number(this.action.params[1]));\n        context.mapLightingTrait.setAmbientChangeStep(step);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/StopSoundFxAtExecutor.ts",
    "content": "import { TriggerStopSoundFxEvent } from '@/game/event/TriggerStopSoundFxEvent';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class StopSoundFxAtExecutor extends TriggerExecutor {\n    execute(context: any) {\n        const waypoint = this.action.params[6];\n        const tile = context.map.getTileAtWaypoint(waypoint);\n        if (tile) {\n            context.events.dispatch(new TriggerStopSoundFxEvent(tile));\n        }\n        else {\n            console.warn(`No valid location found for waypoint ${waypoint}. ` +\n                `Skipping action ${this.getDebugName()}.`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/TextTriggerExecutor.ts",
    "content": "import { TriggerTextEvent } from '@/game/event/TriggerTextEvent';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class TextTriggerExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        context.events.dispatch(new TriggerTextEvent(this.action.params[1]));\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/TimerExtendExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class TimerExtendExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        context.countdownTimer.addSeconds(Number(this.action.params[1]));\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/TimerSetExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class TimerSetExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        context.countdownTimer.setSeconds(Number(this.action.params[1]));\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/TimerShortenExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class TimerShortenExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        context.countdownTimer.addSeconds(-Number(this.action.params[1]));\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/TimerStartExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class TimerStartExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        context.countdownTimer.start();\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/TimerStopExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class TimerStopExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        context.countdownTimer.stop();\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/TimerTextExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class TimerTextExecutor extends TriggerExecutor {\n    execute(e: any) {\n        e.countdownTimer.text = this.action.params[1];\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/ToggleTriggerExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class ToggleTriggerExecutor extends TriggerExecutor {\n    private triggerEnable: boolean;\n    constructor(action: any, context: any, triggerEnable: boolean) {\n        super(action, context);\n        this.triggerEnable = triggerEnable;\n    }\n    execute(game: any): void {\n        const triggerId = this.action.params[1];\n        game.triggers.setTriggerEnabled(triggerId, this.triggerEnable);\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/TurnOnOffBuildingExecutor.ts",
    "content": "import { GameObject } from '@/game/gameobject/GameObject';\nimport { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class TurnOnOffBuildingExecutor extends TriggerExecutor {\n    private turnOn: boolean;\n    constructor(action: any, context: any, turnOn: boolean) {\n        super(action, context);\n        this.turnOn = turnOn;\n    }\n    execute(game: any, targets: GameObject[]): void {\n        for (const target of targets) {\n            if (target instanceof GameObject && target.isBuilding()) {\n                target.poweredTrait?.setTurnedOn(this.turnOn);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/trigger/executor/UnrevealAroundWaypointExecutor.ts",
    "content": "import { TriggerExecutor } from '@/game/trigger/TriggerExecutor';\nexport class UnrevealAroundWaypointExecutor extends TriggerExecutor {\n    execute(context: any): void {\n        const waypointId = Number(this.action.params[1]);\n        const tile = context.map.getTileAtWaypoint(waypointId);\n        if (tile) {\n            for (const combatant of context.getCombatants()) {\n                context.mapShroudTrait\n                    .getPlayerShroud(combatant)\n                    ?.unrevealAround(tile, context.rules.general.revealTriggerRadius);\n            }\n        }\n        else {\n            console.warn(`No valid location found for waypoint ${waypointId}. ` +\n                `Skipping action ${this.getDebugName()}.`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/game/type/ArmorType.ts",
    "content": "export enum ArmorType {\n    None = 0,\n    Flak = 1,\n    Plate = 2,\n    Light = 3,\n    Medium = 4,\n    Heavy = 5,\n    Wood = 6,\n    Steel = 7,\n    Concrete = 8,\n    Special_1 = 9,\n    Special_2 = 10\n}\n"
  },
  {
    "path": "src/game/type/LandTargeting.ts",
    "content": "export enum LandTargeting {\n    LandOk = 0,\n    LandNotOk = 1,\n    LandSecondary = 2\n}\n"
  },
  {
    "path": "src/game/type/LandType.ts",
    "content": "import { TerrainType } from '@/engine/type/TerrainType';\nexport enum LandType {\n    Clear = 0,\n    Road = 1,\n    Rock = 2,\n    Beach = 3,\n    Rough = 4,\n    Railroad = 5,\n    Weeds = 6,\n    Water = 7,\n    Wall = 8,\n    Tiberium = 9,\n    Cliff = 10\n}\nconst terrainToLandTypeMap = new Map([\n    [TerrainType.Default, LandType.Clear],\n    [TerrainType.Clear, LandType.Clear],\n    [TerrainType.Tunnel, LandType.Cliff],\n    [TerrainType.Railroad, LandType.Railroad],\n    [TerrainType.Rock1, LandType.Rock],\n    [TerrainType.Rock2, LandType.Rock],\n    [TerrainType.Water, LandType.Water],\n    [TerrainType.Shore, LandType.Beach],\n    [TerrainType.Pavement, LandType.Road],\n    [TerrainType.Dirt, LandType.Road],\n    [TerrainType.Rough, LandType.Rough],\n    [TerrainType.Cliff, LandType.Cliff]\n]);\nexport function getLandType(terrainType: TerrainType): LandType {\n    if (!terrainToLandTypeMap.has(terrainType)) {\n        throw new Error(`Unknown terrain type ${terrainType}`);\n    }\n    return terrainToLandTypeMap.get(terrainType)!;\n}\n"
  },
  {
    "path": "src/game/type/LocomotorType.ts",
    "content": "import { SpeedType } from './SpeedType';\nexport enum LocomotorType {\n    Statue = 0,\n    Aircraft = 1,\n    Chrono = 2,\n    Hover = 3,\n    Infantry = 4,\n    Jumpjet = 5,\n    Missile = 6,\n    Ship = 7,\n    Vehicle = 8\n}\nexport const locomotorTypesByClsId = new Map<string, LocomotorType>([\n    ['{4A582746-9839-11d1-B709-00A024DDAFD1}', LocomotorType.Aircraft],\n    ['{4A582747-9839-11d1-B709-00A024DDAFD1}', LocomotorType.Chrono],\n    ['{4A582742-9839-11d1-B709-00A024DDAFD1}', LocomotorType.Hover],\n    ['{4A582744-9839-11d1-B709-00A024DDAFD1}', LocomotorType.Infantry],\n    ['{92612C46-F71F-11d1-AC9F-006008055BB5}', LocomotorType.Jumpjet],\n    ['{B7B49766-E576-11d3-9BD9-00104B972FE8}', LocomotorType.Missile],\n    ['{2BEA74E1-7CCA-11d3-BE14-00104B62A16C}', LocomotorType.Ship],\n    ['{4A582741-9839-11d1-B709-00A024DDAFD1}', LocomotorType.Vehicle]\n]);\nexport const defaultSpeedsByLocomotor = new Map<LocomotorType, SpeedType>([\n    [LocomotorType.Infantry, SpeedType.Foot],\n    [LocomotorType.Ship, SpeedType.Float],\n    [LocomotorType.Hover, SpeedType.Hover],\n    [LocomotorType.Jumpjet, SpeedType.Winged],\n    [LocomotorType.Aircraft, SpeedType.Winged],\n    [LocomotorType.Missile, SpeedType.Winged]\n]);\n(LocomotorType as any).locomotorTypesByClsId = locomotorTypesByClsId;\n(LocomotorType as any).defaultSpeedsByLocomotor = defaultSpeedsByLocomotor;\n"
  },
  {
    "path": "src/game/type/MovementZone.ts",
    "content": "export enum MovementZone {\n    Amphibious = 0,\n    AmphibiousCrusher = 1,\n    AmphibiousDestroyer = 2,\n    Crusher = 3,\n    CrusherAll = 4,\n    Destroyer = 5,\n    Fly = 6,\n    Infantry = 7,\n    InfantryDestroyer = 8,\n    Normal = 9,\n    Subterranean = 10,\n    Water = 11\n}\n"
  },
  {
    "path": "src/game/type/NavalTargeting.ts",
    "content": "export enum NavalTargeting {\n    UnderwaterNever = 0,\n    UnderwaterSecondary = 1,\n    UnderwaterOnly = 2,\n    OrganicSecondary = 3,\n    SealSpecial = 4,\n    NavalAll = 5,\n    NavalNone = 6\n}\n"
  },
  {
    "path": "src/game/type/PipColor.ts",
    "content": "export enum PipColor {\n    Green = 0,\n    Yellow = 1,\n    White = 2,\n    Red = 3,\n    Blue = 4\n}\n"
  },
  {
    "path": "src/game/type/PowerupType.ts",
    "content": "export enum PowerupType {\n    Armor = 0,\n    Firepower = 1,\n    HealBase = 2,\n    Money = 3,\n    Reveal = 4,\n    Speed = 5,\n    Veteran = 6,\n    Unit = 7,\n    Invulnerability = 8,\n    IonStorm = 9,\n    Gas = 10,\n    Tiberium = 11,\n    Pod = 12,\n    Cloak = 13,\n    Darkness = 14,\n    Explosion = 15,\n    ICBM = 16,\n    Napalm = 17,\n    Squad = 18\n}\n"
  },
  {
    "path": "src/game/type/SpeedType.ts",
    "content": "export enum SpeedType {\n    Foot = 0,\n    Track = 1,\n    Wheel = 2,\n    Hover = 3,\n    Float = 4,\n    FloatBeach = 5,\n    Amphibious = 6,\n    Winged = 7\n}\n"
  },
  {
    "path": "src/game/type/SuperWeaponType.ts",
    "content": "export enum SuperWeaponType {\n    MultiMissile = 0,\n    IronCurtain = 1,\n    LightningStorm = 2,\n    ChronoSphere = 3,\n    ChronoWarp = 4,\n    ParaDrop = 5,\n    AmerParaDrop = 6\n}\n"
  },
  {
    "path": "src/game/type/VhpScan.ts",
    "content": "export enum VhpScan {\n    None = 0,\n    Normal = 1,\n    Strong = 2\n}\n"
  },
  {
    "path": "src/gui/CanvasMetrics.ts",
    "content": "import { CompositeDisposable } from '../util/disposable/CompositeDisposable';\nexport class CanvasMetrics {\n    public x: number;\n    public y: number;\n    public width: number;\n    public height: number;\n    public displayWidth: number;\n    public displayHeight: number;\n    private canvas: HTMLCanvasElement;\n    private window: Window;\n    private disposables: CompositeDisposable;\n    private updateCanvasBoxMetrics: () => void;\n    constructor(canvas: HTMLCanvasElement, window: Window) {\n        this.canvas = canvas;\n        this.window = window;\n        this.x = 0;\n        this.y = 0;\n        this.width = 0;\n        this.height = 0;\n        this.displayWidth = 0;\n        this.displayHeight = 0;\n        this.disposables = new CompositeDisposable();\n        this.updateCanvasBoxMetrics = () => {\n            const rect = this.canvas.getBoundingClientRect();\n            this.x = rect.left + this.window.scrollX;\n            this.y = rect.top + this.window.scrollY;\n            this.width = this.canvas.width;\n            this.height = this.canvas.height;\n            this.displayWidth = rect.width || this.canvas.clientWidth || this.width;\n            this.displayHeight = rect.height || this.canvas.clientHeight || this.height;\n        };\n    }\n    init(): void {\n        this.updateCanvasBoxMetrics();\n        this.window.addEventListener('resize', this.updateCanvasBoxMetrics);\n        this.window.visualViewport?.addEventListener('resize', this.updateCanvasBoxMetrics);\n        this.disposables.add(() => this.window.removeEventListener('resize', this.updateCanvasBoxMetrics));\n        this.disposables.add(() => this.window.visualViewport?.removeEventListener('resize', this.updateCanvasBoxMetrics));\n    }\n    notifyViewportChange(): void {\n        this.updateCanvasBoxMetrics();\n    }\n    toCanvasPosition(pageX: number, pageY: number): { x: number; y: number; } {\n        return this.scaleDisplayPosition({\n            x: pageX - this.x,\n            y: pageY - this.y,\n        });\n    }\n    toCanvasOffset(offsetX: number, offsetY: number): { x: number; y: number; } {\n        return this.scaleDisplayPosition({ x: offsetX, y: offsetY });\n    }\n    private scaleDisplayPosition(position: { x: number; y: number; }): { x: number; y: number; } {\n        const scaleX = this.displayWidth > 0 ? this.width / this.displayWidth : 1;\n        const scaleY = this.displayHeight > 0 ? this.height / this.displayHeight : 1;\n        return {\n            x: position.x * scaleX,\n            y: position.y * scaleY,\n        };\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/FullScreen.ts",
    "content": "import { CompositeDisposable } from '../util/disposable/CompositeDisposable';\nimport { setupFullScreenChangeListener } from '../util/fullScreen';\nimport { EventDispatcher } from '../util/event';\nexport interface HotKey {\n    altKey: boolean;\n    shiftKey: boolean;\n    ctrlKey: boolean;\n    metaKey: boolean;\n    keyCode: number;\n}\nexport class FullScreen {\n    public static readonly hotKey: HotKey = {\n        altKey: true,\n        shiftKey: false,\n        ctrlKey: false,\n        metaKey: false,\n        keyCode: \"F\".charCodeAt(0),\n    };\n    private readonly document: Document;\n    private readonly disposables: CompositeDisposable;\n    private readonly _onChange: EventDispatcher<FullScreen, boolean>;\n    public get onChange() {\n        return this._onChange.asEvent();\n    }\n    constructor(document: Document) {\n        this.document = document;\n        this.disposables = new CompositeDisposable();\n        this._onChange = new EventDispatcher<FullScreen, boolean>();\n    }\n    public static isFullScreenHotKey(event: KeyboardEvent): boolean {\n        return (event.keyCode === this.hotKey.keyCode &&\n            event.altKey === this.hotKey.altKey &&\n            event.shiftKey === this.hotKey.shiftKey &&\n            event.ctrlKey === this.hotKey.ctrlKey &&\n            event.metaKey === this.hotKey.metaKey);\n    }\n    public init(): void {\n        const keyDownHandler = (event: KeyboardEvent) => {\n            if (FullScreen.isFullScreenHotKey(event)) {\n                event.preventDefault();\n                event.stopPropagation();\n                this.toggle();\n            }\n        };\n        this.document.addEventListener(\"keydown\", keyDownHandler);\n        this.disposables.add(() => this.document.removeEventListener(\"keydown\", keyDownHandler));\n        const cleanup = setupFullScreenChangeListener(this.document, this.handleFullScreenChange);\n        if (cleanup) {\n            this.disposables.add(cleanup);\n        }\n    }\n    private handleFullScreenChange = (isFullScreen: boolean): void => {\n        this._onChange.dispatch(this, isFullScreen);\n    };\n    public toggle(): void {\n        this.toggleAsync().catch((error) => console.error(error));\n    }\n    public isFullScreen(): boolean {\n        return !!this.document.fullscreenElement;\n    }\n    public isAvailable(): boolean {\n        return !!(this.document.fullscreenEnabled ||\n            (this.document as any).webkitFullscreenEnabled);\n    }\n    public async toggleAsync(): Promise<void> {\n        if (this.document.fullscreenElement) {\n            try {\n                screen?.orientation?.unlock?.();\n            }\n            catch (_error) {\n            }\n            await this.document.exitFullscreen();\n        }\n        else {\n            await this.document.documentElement.requestFullscreen();\n            try {\n                await (screen?.orientation as any)?.lock?.(\"landscape\");\n            }\n            catch (error) {\n                console.warn(\"Orientation lock failed\", error);\n            }\n        }\n    }\n    public dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/HtmlContainer.ts",
    "content": "import { LazyHtmlElement } from \"./LazyHtmlElement\";\nexport class HtmlContainer extends LazyHtmlElement {\n    protected visible: boolean = true;\n    protected left: number = 0;\n    protected top: number = 0;\n    protected width: number | string = 0;\n    protected height: number | string = 0;\n    protected relativeMode: boolean = false;\n    protected translateMode: boolean = false;\n    constructor() {\n        super();\n    }\n    render(): void {\n        if (!this.isRendered()) {\n            let element = this.getElement();\n            if (!element) {\n                element = document.createElement(\"div\");\n                this.setElement(element);\n            }\n            this.updateMode();\n            this.updatePosition();\n            this.updateVisibility();\n            this.updateSize();\n        }\n        super.render();\n    }\n    setRelativeMode(relative: boolean): void {\n        if (this.relativeMode !== relative) {\n            this.relativeMode = relative;\n            this.updateMode();\n        }\n    }\n    setTranslateMode(translate: boolean): void {\n        if (this.translateMode !== translate) {\n            this.translateMode = translate;\n            this.updatePosition();\n        }\n    }\n    setPosition(left: number, top: number): void {\n        this.left = left;\n        this.top = top;\n        this.updatePosition();\n    }\n    setSize(width: number | string, height: number | string): void {\n        this.width = width;\n        this.height = height;\n        this.updateSize();\n    }\n    getSize(): {\n        width: number | string;\n        height: number | string;\n    } {\n        return { width: this.width, height: this.height };\n    }\n    setVisible(visible: boolean): void {\n        if (this.visible !== visible) {\n            this.visible = visible;\n            this.updateVisibility();\n        }\n    }\n    protected updateMode(): void {\n        const element = this.getElement();\n        if (element) {\n            if (this.relativeMode) {\n                element.style.position = \"relative\";\n            }\n            else {\n                element.style.overflow = \"visible\";\n                element.style.position = \"absolute\";\n            }\n        }\n    }\n    protected updatePosition(): void {\n        const element = this.getElement();\n        if (element) {\n            if (this.translateMode) {\n                element.style.top = \"0\";\n                element.style.left = \"0\";\n                element.style.transform = `translate(${this.left}px, ${this.top}px)`;\n            }\n            else {\n                element.style.left = this.left + 'px';\n                element.style.top = this.top + 'px';\n                element.style.transform = \"\";\n            }\n        }\n    }\n    protected updateSize(): void {\n        const element = this.getElement();\n        if (element) {\n            element.style.width = typeof this.width === 'number' ? this.width + 'px' : this.width;\n            element.style.height = typeof this.height === 'number' ? this.height + 'px' : this.height;\n        }\n    }\n    hide(): void {\n        this.setVisible(false);\n    }\n    show(): void {\n        this.setVisible(true);\n    }\n    protected updateVisibility(): void {\n        const element = this.getElement();\n        if (element) {\n            element.style.display = this.visible ? \"block\" : \"none\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/HtmlReactElement.ts",
    "content": "import React from 'react';\nimport { createRoot, Root } from 'react-dom/client';\nimport { HtmlContainer } from './HtmlContainer';\nexport class HtmlReactElement<P = any> extends HtmlContainer {\n    private options: P;\n    private Component: React.ComponentType<P>;\n    private root?: Root;\n    static factory<P>(Component: React.ComponentType<P>, options: P): HtmlReactElement<P> {\n        return new HtmlReactElement(options, Component);\n    }\n    constructor(options: P, Component: React.ComponentType<P>) {\n        super();\n        this.options = options;\n        this.Component = Component;\n    }\n    render(): void {\n        if (!this.isRendered()) {\n            const element = document.createElement('div');\n            this.setElement(element);\n            this.renderReactElement();\n        }\n        super.render();\n    }\n    private renderReactElement(): void {\n        const element = this.getElement();\n        if (element) {\n            this.root ??= createRoot(element);\n            this.root.render(React.createElement(this.Component, this.options));\n        }\n    }\n    applyOptions(callback: (options: P) => void): void {\n        callback(this.options);\n        this.refresh();\n    }\n    refresh(): void {\n        if (this.isRendered()) {\n            this.renderReactElement();\n        }\n    }\n    unrender(): void {\n        if (this.root && this.isRendered()) {\n            this.root.unmount();\n            this.root = undefined;\n        }\n        super.unrender();\n    }\n}\n"
  },
  {
    "path": "src/gui/HtmlReactElement.tsx",
    "content": "import React, { ComponentType, ReactElement } from 'react';\nimport { createRoot, Root } from 'react-dom/client';\nimport { HtmlContainer } from './HtmlContainer';\nexport class HtmlReactElement<P extends object> extends HtmlContainer {\n    private options: P;\n    private Component: ComponentType<P>;\n    private root?: Root;\n    static factory<P extends object>(Component: ComponentType<P>, options: P): HtmlReactElement<P> {\n        return new HtmlReactElement<P>(options, Component);\n    }\n    constructor(options: P, Component: ComponentType<P>) {\n        super();\n        this.options = options;\n        this.Component = Component;\n    }\n    render(): void {\n        if (!this.isRendered()) {\n            const newElement = document.createElement(\"div\");\n            this.setElement(newElement);\n            this.renderReactElement();\n        }\n        super.render();\n    }\n    private renderReactElement(): void {\n        const element = this.getElement();\n        if (element) {\n            const reactElement = React.createElement(this.Component, this.options);\n            this.root ??= createRoot(element);\n            this.root.render(reactElement);\n        }\n        else {\n            console.warn(\"HtmlReactElement: Attempted to renderReactElement but no DOM element is set.\");\n        }\n    }\n    applyOptions(updater: (currentOptions: P) => void): void {\n        updater(this.options);\n        this.refresh();\n    }\n    refresh(): void {\n        if (this.isRendered()) {\n            this.renderReactElement();\n        }\n    }\n    unrender(): void {\n        if (this.root && this.isRendered()) {\n            this.root.unmount();\n            this.root = undefined;\n        }\n        super.unrender();\n    }\n    setComponent(NewComponent: ComponentType<P>, newOptions?: P) {\n        this.Component = NewComponent;\n        if (newOptions !== undefined) {\n            this.options = newOptions;\n        }\n        this.refresh();\n    }\n}\n"
  },
  {
    "path": "src/gui/LazyHtmlElement.ts",
    "content": "export class LazyHtmlElement {\n    protected element?: HTMLElement;\n    protected children: Set<LazyHtmlElement> = new Set();\n    protected rendered: boolean = false;\n    constructor(element?: HTMLElement) {\n        if (element) {\n            this.setElement(element);\n        }\n    }\n    setElement(element: HTMLElement): void {\n        this.element = element;\n    }\n    getElement(): HTMLElement | undefined {\n        return this.element;\n    }\n    getChildren(): LazyHtmlElement[] {\n        return [...this.children];\n    }\n    isRendered(): boolean {\n        return this.rendered;\n    }\n    add(...children: LazyHtmlElement[]): void {\n        for (const child of children) {\n            if (!this.children.has(child)) {\n                this.children.add(child);\n                if (this.rendered) {\n                    this.renderChild(child);\n                }\n            }\n        }\n    }\n    remove(...children: LazyHtmlElement[]): void {\n        for (const child of children) {\n            if (this.children.has(child)) {\n                this.children.delete(child);\n                if (this.rendered) {\n                    this.unrenderChild(child);\n                }\n            }\n        }\n    }\n    removeAll(): void {\n        this.remove(...this.children);\n    }\n    render(): void {\n        if (!this.element) {\n            throw new Error('An HTML element must be passed in the constructor or using the setter.');\n        }\n        this.children.forEach(child => this.renderChild(child));\n        this.rendered = true;\n    }\n    protected renderChild(child: LazyHtmlElement): void {\n        child.render();\n        const childElement = child.getElement();\n        if (childElement) {\n            this.getElement()!.appendChild(childElement);\n        }\n    }\n    protected unrenderChild(child: LazyHtmlElement): void {\n        const childElement = child.getElement();\n        if (childElement) {\n            child.unrender();\n            if (childElement.parentElement === this.getElement()) {\n                this.getElement()!.removeChild(childElement);\n            }\n        }\n    }\n    unrender(): void {\n        if (this.isRendered()) {\n            this.children.forEach(child => this.unrenderChild(child));\n            this.rendered = false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/MobileTouchControls.ts",
    "content": "let mobileTouchButton: number = 0;\n\nexport function getMobileTouchButton(): number {\n  return mobileTouchButton;\n}\n\nexport function setMobileTouchButton(button: number): void {\n  mobileTouchButton = button;\n}\n\nexport function createMobileTouchControls(container: HTMLElement): () => void {\n  const wrapper = document.createElement(\"div\");\n  wrapper.className = \"mobile-touch-controls\";\n\n  const leftBtn = document.createElement(\"button\");\n  leftBtn.className = \"mobile-touch-btn mobile-touch-btn-left active\";\n  leftBtn.textContent = \"L\";\n  leftBtn.setAttribute(\"data-button\", \"0\");\n\n  const rightBtn = document.createElement(\"button\");\n  rightBtn.className = \"mobile-touch-btn mobile-touch-btn-right\";\n  rightBtn.textContent = \"R\";\n  rightBtn.setAttribute(\"data-button\", \"2\");\n\n  function setActive(button: number): void {\n    mobileTouchButton = button;\n    leftBtn.classList.toggle(\"active\", button === 0);\n    rightBtn.classList.toggle(\"active\", button === 2);\n  }\n\n  const onLeftClick = (e: Event) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setActive(0);\n  };\n\n  const onRightClick = (e: Event) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setActive(2);\n  };\n\n  leftBtn.addEventListener(\"touchstart\", onLeftClick, { passive: false });\n  leftBtn.addEventListener(\"mousedown\", onLeftClick);\n  rightBtn.addEventListener(\"touchstart\", onRightClick, { passive: false });\n  rightBtn.addEventListener(\"mousedown\", onRightClick);\n\n  wrapper.appendChild(leftBtn);\n  wrapper.appendChild(rightBtn);\n  container.appendChild(wrapper);\n\n  return () => {\n    leftBtn.removeEventListener(\"touchstart\", onLeftClick);\n    leftBtn.removeEventListener(\"mousedown\", onLeftClick);\n    rightBtn.removeEventListener(\"touchstart\", onRightClick);\n    rightBtn.removeEventListener(\"mousedown\", onRightClick);\n    wrapper.remove();\n  };\n}\n"
  },
  {
    "path": "src/gui/Pointer.ts",
    "content": "import { PointerLock } from \"../util/PointerLock\";\nimport { clamp } from \"../util/math\";\nimport { CompositeDisposable } from \"../util/disposable/CompositeDisposable\";\nimport { PointerSprite } from \"./PointerSprite\";\nimport { PointerEvents } from \"./PointerEvents\";\nimport { PointerType } from \"../engine/type/PointerType\";\nimport { SimpleRunner } from \"../engine/animation/SimpleRunner\";\nimport { Animation } from \"../engine/Animation\";\nimport { AnimProps } from \"../engine/AnimProps\";\nimport { IniSection } from \"../data/IniSection\";\nimport { BoxedVar } from \"../util/BoxedVar\";\nimport { CanvasMetrics } from \"./CanvasMetrics\";\ninterface Position {\n    x: number;\n    y: number;\n}\nexport class Pointer {\n    private pointerLock: PointerLock;\n    private sprite: PointerSprite;\n    private document: Document;\n    private canvas: HTMLCanvasElement;\n    private canvasMetrics: CanvasMetrics;\n    private mouseAcceleration: BoxedVar<boolean>;\n    private userLockMode: boolean = false;\n    private userPointerVisible: boolean = true;\n    private userPermissionGranted: boolean = false;\n    private position: Position = { x: 0, y: 0 };\n    private disposables: CompositeDisposable = new CompositeDisposable();\n    private pointerType: PointerType = PointerType.Default;\n    private pointerSubFrame: number = 0;\n    public pointerEvents?: PointerEvents;\n    static factory(shpFile: any, palette: any, canvasContainer: any, document: Document, canvasMetrics: CanvasMetrics, mouseAcceleration: BoxedVar<boolean>): Pointer {\n        const sprite = PointerSprite.fromShpFile(shpFile, palette);\n        sprite.setVisible(false);\n        const canvas = canvasContainer.getCanvas();\n        const pointerLock = new PointerLock(canvas, document);\n        const pointer = new Pointer(pointerLock, sprite, document, canvas, canvasMetrics, mouseAcceleration);\n        pointer.pointerEvents = new PointerEvents(canvasContainer, pointer.getPosition(), document, canvasMetrics);\n        pointer.disposables.add(pointer.pointerEvents);\n        return pointer;\n    }\n    constructor(pointerLock: PointerLock, sprite: PointerSprite, document: Document, canvas: HTMLCanvasElement, canvasMetrics: CanvasMetrics, mouseAcceleration: BoxedVar<boolean>) {\n        this.pointerLock = pointerLock;\n        this.sprite = sprite;\n        this.document = document;\n        this.canvas = canvas;\n        this.canvasMetrics = canvasMetrics;\n        this.mouseAcceleration = mouseAcceleration;\n        this.onMouseMove = this.onMouseMove.bind(this);\n    }\n    private onMouseMove = (event: MouseEvent): void => {\n        const position = this.position;\n        if (this.pointerLock.isActive()) {\n            position.x = position.x + event.movementX;\n            position.y = position.y + event.movementY;\n        }\n        else {\n            const pointerPosition = this.canvasMetrics.toCanvasPosition(event.pageX, event.pageY);\n            position.x = pointerPosition.x;\n            position.y = pointerPosition.y;\n        }\n        position.x = clamp(position.x, 0, this.canvasMetrics.width - 1);\n        position.y = clamp(position.y, 0, this.canvasMetrics.height - 1);\n        this.updateSpritePosition();\n    };\n    getPosition(): Position {\n        return this.position;\n    }\n    getPointerLock(): PointerLock {\n        return this.pointerLock;\n    }\n    init(): void {\n        this.listenForFirstCanvasClick();\n        this.pointerLock.onChange.subscribe((isActive: boolean) => {\n            this.sprite.setVisible(this.userPointerVisible && isActive);\n            const requestLock = (): void => {\n                if (this.userLockMode) {\n                    this.pointerLock\n                        .request({\n                        unadjustedMovement: !this.mouseAcceleration.value,\n                    })\n                        .catch((error) => {\n                        console.warn(\"Couldn't acquire pointer lock.\", error);\n                        this.canvas.addEventListener(\"click\", requestLock, { once: true });\n                    });\n                }\n            };\n            if (!isActive) {\n                this.canvas.addEventListener(\"click\", requestLock, { once: true });\n                this.disposables.add(() => this.canvas.removeEventListener(\"click\", requestLock));\n            }\n        });\n        this.document.addEventListener(\"mousemove\", this.onMouseMove, true);\n        this.disposables.add(() => this.document.removeEventListener(\"mousemove\", this.onMouseMove, true));\n    }\n    private listenForFirstCanvasClick(): void {\n        const handleClick = async (): Promise<void> => {\n            if (!this.userPermissionGranted) {\n                try {\n                    await this.pointerLock.request();\n                    if (!this.userLockMode) {\n                        await this.pointerLock.exit();\n                    }\n                    this.userPermissionGranted = true;\n                }\n                catch (error) {\n                    console.warn(\"Couldn't acquire initial pointer lock\", error);\n                    this.canvas.addEventListener(\"click\", handleClick, { once: true });\n                }\n            }\n        };\n        this.canvas.addEventListener(\"click\", handleClick, { once: true });\n        this.disposables.add(() => this.canvas.removeEventListener(\"click\", handleClick));\n    }\n    lock(): void {\n        this.userLockMode = true;\n        if (this.userPermissionGranted) {\n            this.pointerLock\n                .request({\n                unadjustedMovement: !this.mouseAcceleration.value,\n            })\n                .catch((error) => {\n                console.warn(\"Couldn't reacquire pointer lock. Will attempt to require lock on next click\", error);\n                this.userPermissionGranted = false;\n                this.listenForFirstCanvasClick();\n            });\n        }\n    }\n    unlock(): void {\n        this.userLockMode = false;\n        this.pointerLock\n            .exit()\n            .catch((error) => console.error(\"Couldn't release pointer lock. This should never happen\", error));\n    }\n    setVisible(visible: boolean): void {\n        this.userPointerVisible = visible;\n        this.sprite.setVisible(visible && this.pointerLock.isActive());\n    }\n    getUserLockMode(): boolean {\n        return this.userLockMode;\n    }\n    getSprite(): PointerSprite {\n        return this.sprite;\n    }\n    setPointerType(type: PointerType, subFrame: number = 0): void {\n        if (this.pointerType !== type || this.pointerSubFrame !== subFrame) {\n            this.pointerType = type;\n            this.pointerSubFrame = subFrame;\n            this.sprite.setAnimationRunner(undefined);\n            if ([\n                PointerType.Scroll,\n                PointerType.NoScroll,\n                PointerType.Pan,\n            ].includes(type)) {\n                this.sprite.setFrame(type + subFrame);\n            }\n            else {\n                const startFrame = type;\n                const endFrame = (Object.keys(PointerType)\n                    .map(Number)\n                    .find((value) => !Number.isNaN(value) && type < value) ??\n                    this.sprite.getFrameCount()) - 1;\n                this.sprite.setFrame(startFrame);\n                if (startFrame < endFrame) {\n                    const runner = new SimpleRunner();\n                    const animProps = new AnimProps(new IniSection(\"dummy\"), this.sprite.getFrameCount());\n                    animProps.loopCount = -1;\n                    animProps.start = startFrame;\n                    animProps.loopStart = startFrame;\n                    animProps.loopEnd = endFrame;\n                    const animation = new Animation(animProps, new BoxedVar(1.5));\n                    runner.animation = animation;\n                    this.sprite.setAnimationRunner(runner);\n                }\n            }\n            this.updateSpritePosition();\n        }\n    }\n    private updateSpritePosition(): void {\n        const position = { ...this.position };\n        const size = this.sprite.getSize();\n        const halfWidth = Math.floor(size.width / 2);\n        const halfHeight = Math.floor(size.height / 2);\n        if (this.pointerType > PointerType.Mini) {\n            position.x -= halfWidth;\n            position.y -= halfHeight;\n        }\n        if ([\n            PointerType.Scroll,\n            PointerType.NoScroll,\n            PointerType.Pan,\n        ].includes(this.pointerType)) {\n            position.x = clamp(position.x, 0, this.canvasMetrics.width - 1 - size.width);\n            position.y = clamp(position.y, 0, this.canvasMetrics.height - 1 - size.height);\n        }\n        this.sprite.setPosition(position.x, position.y);\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/PointerEvents.ts",
    "content": "import { CompositeDisposable } from '../util/disposable/CompositeDisposable';\nimport { equals } from '../util/array';\nimport { clamp } from '../util/math';\nimport { getMobileTouchButton } from './MobileTouchControls';\nimport * as THREE from 'three';\ninterface PointerPosition {\n    x: number;\n    y: number;\n}\ninterface CanvasMetrics {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n    toCanvasPosition(pageX: number, pageY: number): PointerPosition;\n    toCanvasOffset(offsetX: number, offsetY: number): PointerPosition;\n}\ninterface LockModePointer {\n    x: number;\n    y: number;\n}\ninterface Renderer {\n    getCanvas(): HTMLCanvasElement;\n    getScenes(): Scene[];\n}\ninterface Scene {\n    get3DObject(): THREE.Object3D;\n    scene: THREE.Scene;\n    camera: THREE.Camera;\n    viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n}\ninterface TouchStartBuffer {\n    cb: () => void;\n    timeoutId: number;\n}\ninterface FakeMouseEvent extends Partial<MouseEvent> {\n    offsetX: number;\n    offsetY: number;\n    button: number;\n    isTouch: boolean;\n    detail: number;\n    altKey: boolean;\n    ctrlKey: boolean;\n    metaKey: boolean;\n    shiftKey: boolean;\n    timeStamp: number;\n    touchDuration?: number;\n}\ninterface PointerEventData {\n    type: string;\n    target?: THREE.Object3D;\n    pointer: PointerPosition;\n    intersection?: THREE.Intersection;\n    button: number;\n    isTouch: boolean;\n    touchDuration?: number;\n    clicks: number;\n    altKey: boolean;\n    ctrlKey: boolean;\n    metaKey: boolean;\n    shiftKey: boolean;\n    timeStamp: number;\n    wheelDeltaY: number;\n    stopPropagation: () => void;\n}\ninterface EventHandler {\n    callback: (event: PointerEventData) => void;\n    useCapture: boolean;\n}\ninterface EventContext {\n    handlers: Map<string, EventHandler[]>;\n}\nfunction isVisibleInScene(obj: THREE.Object3D, sceneRoot: THREE.Object3D): boolean {\n    return !!obj.visible && (obj === sceneRoot || (!!obj.parent && isVisibleInScene(obj.parent, sceneRoot)));\n}\nexport class PointerEvents {\n    private renderer: Renderer;\n    private lockModePointer: LockModePointer;\n    private document: Document;\n    private canvasMetrics: CanvasMetrics;\n    private disposables: CompositeDisposable;\n    private canvasContext: EventContext;\n    private objectContexts: Map<THREE.Object3D, EventContext>;\n    private intersectionsEnabled: boolean;\n    private clickPaths: Map<number, THREE.Object3D[]>;\n    private touchFingers: number;\n    private currentHoverPath?: THREE.Object3D[];\n    private initialTouchEvent?: TouchEvent;\n    private touchStartBuffer?: TouchStartBuffer;\n    constructor(renderer: Renderer, lockModePointer: LockModePointer, document: Document, canvasMetrics: CanvasMetrics) {\n        this.renderer = renderer;\n        this.lockModePointer = lockModePointer;\n        this.document = document;\n        this.canvasMetrics = canvasMetrics;\n        this.disposables = new CompositeDisposable();\n        this.canvasContext = { handlers: new Map() };\n        this.objectContexts = new Map();\n        this.intersectionsEnabled = true;\n        this.clickPaths = new Map();\n        this.touchFingers = 0;\n        const canvas = renderer.getCanvas();\n        canvas.addEventListener('dblclick', this.onDblClick, false);\n        canvas.addEventListener('mousemove', this.onMouseMove, false);\n        canvas.addEventListener('mousedown', this.onMouseDown, false);\n        canvas.addEventListener('mouseup', this.onMouseUp, false);\n        canvas.addEventListener('touchmove', this.onTouchMove, false);\n        canvas.addEventListener('touchstart', this.onTouchStart, false);\n        canvas.addEventListener('touchend', this.onTouchEnd, false);\n        canvas.addEventListener('wheel', this.onMouseWheel, { passive: true });\n        this.disposables.add(() => {\n            canvas.removeEventListener('dblclick', this.onDblClick, false);\n            canvas.removeEventListener('mousemove', this.onMouseMove, false);\n            canvas.removeEventListener('mousedown', this.onMouseDown, false);\n            canvas.removeEventListener('mouseup', this.onMouseUp, false);\n            canvas.removeEventListener('touchmove', this.onTouchMove, false);\n            canvas.removeEventListener('touchstart', this.onTouchStart, false);\n            canvas.removeEventListener('touchend', this.onTouchEnd, false);\n            canvas.removeEventListener('wheel', this.onMouseWheel, false);\n        });\n    }\n    private onDblClick = (event: MouseEvent): void => {\n        if (event.button === 0) {\n            this.onMouseEvent('dblclick', event);\n        }\n    };\n    private onMouseMove = (event: MouseEvent): void => {\n        const pointerPos = this.getPointerPosition(event);\n        if (this.intersectionsEnabled) {\n            const previousHoverPath = this.currentHoverPath ? [...this.currentHoverPath] : undefined;\n            const previousTarget = previousHoverPath?.[0];\n            const intersection = this.findObjectUnderPointer(pointerPos);\n            const currentTarget = intersection?.object;\n            this.currentHoverPath = undefined;\n            if (currentTarget) {\n                this.currentHoverPath = [currentTarget];\n                currentTarget.traverseAncestors((ancestor) => {\n                    this.currentHoverPath!.push(ancestor);\n                });\n            }\n            if (!equals(this.currentHoverPath ?? [], previousHoverPath ?? [])) {\n                if (previousHoverPath) {\n                    for (const obj of previousHoverPath) {\n                        if (!(this.currentHoverPath && this.currentHoverPath.includes(obj))) {\n                            this.notify('mouseleave', obj, pointerPos, event, undefined, false);\n                        }\n                    }\n                }\n                if (this.currentHoverPath) {\n                    for (const obj of this.currentHoverPath) {\n                        if (!(previousHoverPath && previousHoverPath.includes(obj))) {\n                            this.notify('mouseenter', obj, pointerPos, event, intersection, false);\n                        }\n                    }\n                }\n                if (previousTarget) {\n                    this.notify('mouseout', previousTarget, pointerPos, event);\n                }\n                if (currentTarget) {\n                    this.notify('mouseover', currentTarget, pointerPos, event, intersection);\n                }\n            }\n            if (currentTarget) {\n                this.notify('mousemove', currentTarget, pointerPos, event, intersection);\n            }\n            else {\n                this.renderer.getScenes().forEach((scene) => {\n                    this.notify('mousemove', scene.get3DObject(), pointerPos, event);\n                });\n            }\n        }\n        this.notify('mousemove', 'canvas', pointerPos, event);\n    };\n    private onMouseDown = (event: MouseEvent): void => {\n        this.onMouseEvent('mousedown', event);\n    };\n    private onMouseUp = (event: MouseEvent): void => {\n        this.onMouseEvent('mouseup', event);\n    };\n    private onMouseWheel = (event: WheelEvent): void => {\n        this.onMouseEvent('wheel', event);\n    };\n    private onTouchMove = (event: TouchEvent): void => {\n        event.preventDefault();\n        if (this.initialTouchEvent?.touches) {\n            const initialTouch = this.initialTouchEvent.touches[0];\n            const currentTouch = [...event.changedTouches].find((touch) => initialTouch.identifier === touch.identifier);\n            if (currentTouch) {\n                if (this.touchStartBuffer) {\n                    clearTimeout(this.touchStartBuffer.timeoutId);\n                    this.touchStartBuffer.cb();\n                    this.touchStartBuffer = undefined;\n                }\n                const fakeEvent = this.fakeMouseEventFromTouch(currentTouch, event);\n                this.onMouseMove(fakeEvent as unknown as MouseEvent);\n            }\n        }\n    };\n    private onTouchStart = (event: TouchEvent): void => {\n        event.preventDefault();\n        const touches = event.touches;\n        if (touches.length > 1) {\n            if (this.touchFingers <= 0) {\n                if (touches[0].target === this.renderer.getCanvas() && touches.length === 2) {\n                    if (this.touchStartBuffer) {\n                        clearTimeout(this.touchStartBuffer.timeoutId);\n                        this.touchStartBuffer = undefined;\n                    }\n                    this.touchFingers = 2;\n                    if (!this.initialTouchEvent) {\n                        this.initialTouchEvent = event;\n                    }\n                    const initialTouch = this.initialTouchEvent.touches[0];\n                    const fakeEvent = this.fakeMouseEventFromTouch(initialTouch, event, 2);\n                    this.onMouseEvent('mousedown', fakeEvent as unknown as MouseEvent);\n                }\n            }\n        }\n        else {\n            const callback = () => {\n                this.touchFingers = 1;\n                const fakeEvent = this.fakeMouseEventFromTouch(touches[0], event);\n                this.onMouseEvent('mousedown', fakeEvent as unknown as MouseEvent);\n            };\n            const timeoutId = setTimeout(callback, 50);\n            this.touchStartBuffer = { cb: callback, timeoutId };\n            this.initialTouchEvent = event;\n        }\n    };\n    private onTouchEnd = (event: TouchEvent): void => {\n        event.preventDefault();\n        if (this.initialTouchEvent?.touches) {\n            const initialTouch = this.initialTouchEvent.touches[0];\n            const endTouch = [...event.changedTouches].find((touch) => initialTouch.identifier === touch.identifier);\n            if (endTouch) {\n                if (this.touchStartBuffer) {\n                    clearTimeout(this.touchStartBuffer.timeoutId);\n                    this.touchStartBuffer.cb();\n                    this.touchStartBuffer = undefined;\n                }\n                const button = this.touchFingers === 2 ? 2 : -1;\n                const fakeEvent = this.fakeMouseEventFromTouch(endTouch, event, button);\n                fakeEvent.touchDuration = event.timeStamp - this.initialTouchEvent.timeStamp;\n                this.touchFingers = 0;\n                this.initialTouchEvent = undefined;\n                this.onMouseEvent('mouseup', fakeEvent as unknown as MouseEvent);\n            }\n        }\n    };\n    addEventListener(target: THREE.Object3D | 'canvas', eventType: string, callback: (event: PointerEventData) => void, useCapture: boolean = false): () => void {\n        const context = target === 'canvas'\n            ? this.canvasContext\n            : this.getOrCreateObjectContext(target);\n        let handlers = context.handlers.get(eventType);\n        if (!handlers) {\n            handlers = [];\n            context.handlers.set(eventType, handlers);\n        }\n        handlers.push({ callback, useCapture });\n        return () => this.removeEventListener(target, eventType, callback, useCapture);\n    }\n    removeEventListener(target: THREE.Object3D | 'canvas', eventType: string, callback: (event: PointerEventData) => void, useCapture: boolean = false): void {\n        const context = target === 'canvas'\n            ? this.canvasContext\n            : this.objectContexts.get(target as THREE.Object3D);\n        if (context && context.handlers.has(eventType)) {\n            let handlers = context.handlers.get(eventType)!;\n            handlers = handlers.filter((handler) => !(handler.callback === callback && handler.useCapture === useCapture));\n            if (handlers.length) {\n                context.handlers.set(eventType, handlers);\n            }\n            else {\n                context.handlers.delete(eventType);\n            }\n            if (!context.handlers.size && target !== 'canvas') {\n                this.objectContexts.delete(target as THREE.Object3D);\n            }\n        }\n    }\n    private getOrCreateObjectContext(obj: THREE.Object3D): EventContext {\n        if (!obj) {\n            throw new Error('Undefined Object3D instance.');\n        }\n        let context = this.objectContexts.get(obj);\n        if (!context) {\n            context = { handlers: new Map() };\n            this.objectContexts.set(obj, context);\n        }\n        return context;\n    }\n    private fakeMouseEventFromTouch(touch: Touch, event: TouchEvent, button: number = -1): FakeMouseEvent {\n        const position = this.computeTouchPosition(touch);\n        return {\n            offsetX: position.x,\n            offsetY: position.y,\n            button: button >= 0 ? button : getMobileTouchButton(),\n            isTouch: true,\n            detail: 1,\n            altKey: event.altKey,\n            ctrlKey: event.ctrlKey,\n            metaKey: event.metaKey,\n            shiftKey: event.shiftKey,\n            timeStamp: event.timeStamp,\n        };\n    }\n    private computeTouchPosition(touch: Touch): PointerPosition {\n        let position = this.canvasMetrics.toCanvasPosition(touch.pageX, touch.pageY);\n        position.x = clamp(position.x, 0, this.canvasMetrics.width - 1);\n        position.y = clamp(position.y, 0, this.canvasMetrics.height - 1);\n        return position;\n    }\n    private onMouseEvent(eventType: string, event: MouseEvent | WheelEvent): void {\n        const pointerPos = this.getPointerPosition(event);\n        const intersection = this.findObjectUnderPointer(pointerPos);\n        if (intersection) {\n            this.notify(eventType, intersection.object, pointerPos, event, intersection);\n        }\n        else {\n            this.renderer.getScenes().forEach((scene) => {\n                this.notify(eventType, scene.get3DObject(), pointerPos, event);\n            });\n        }\n        this.notify(eventType, 'canvas', pointerPos, event);\n        if (eventType === 'mousedown' || eventType === 'mouseup') {\n            const targetObj = intersection?.object;\n            let clickPath: THREE.Object3D[] = [];\n            if (targetObj) {\n                clickPath = [targetObj];\n                targetObj.traverseAncestors((ancestor) => {\n                    clickPath.push(ancestor);\n                });\n            }\n            if (eventType === 'mousedown') {\n                this.clickPaths.set((event as MouseEvent).button, clickPath);\n            }\n            else {\n                const downPath = this.clickPaths.get((event as MouseEvent).button);\n                this.clickPaths.delete((event as MouseEvent).button);\n                let clickHandled = false;\n                for (const obj of clickPath) {\n                    if (downPath?.includes(obj)) {\n                        this.notify('click', obj, pointerPos, event, intersection);\n                        clickHandled = true;\n                        break;\n                    }\n                }\n                if (!clickHandled) {\n                    this.renderer.getScenes().forEach((scene) => {\n                        this.notify('click', scene.get3DObject(), pointerPos, event);\n                    });\n                    this.notify('click', 'canvas', pointerPos, event);\n                }\n            }\n        }\n    }\n    private getPointerPosition(event: MouseEvent | WheelEvent): PointerPosition {\n        if (this.document.pointerLockElement) {\n            return this.lockModePointer;\n        }\n        if ((event as unknown as FakeMouseEvent).isTouch) {\n            return { x: (event as MouseEvent).offsetX, y: (event as MouseEvent).offsetY };\n        }\n        return this.canvasMetrics.toCanvasOffset((event as MouseEvent).offsetX, (event as MouseEvent).offsetY);\n    }\n    private findObjectUnderPointer(pointerPos: PointerPosition): THREE.Intersection | undefined {\n        const scenes = this.renderer.getScenes();\n        const objectsByScene = this.groupObjectsByScene();\n        for (let i = scenes.length - 1; i >= 0; i--) {\n            const raycaster = new THREE.Raycaster();\n            const normalizedPointer = this.normalizePointer(pointerPos, scenes[i].viewport);\n            raycaster.setFromCamera(normalizedPointer, scenes[i].camera);\n            raycaster.layers.enable(1);\n            const sceneObjects = objectsByScene\n                .get(scenes[i].scene)!\n                .filter((obj) => isVisibleInScene(obj, scenes[i].get3DObject()));\n            const intersections = raycaster.intersectObjects(sceneObjects, true);\n            if (intersections.length) {\n                if (intersections.length === 1)\n                    return intersections[0];\n                const objectSet = new Set(intersections.map((intersection) => intersection.object));\n                intersections.forEach((intersection) => {\n                    if (objectSet.has(intersection.object)) {\n                        intersection.object.traverseAncestors((ancestor) => {\n                            if (objectSet.has(ancestor)) {\n                                objectSet.delete(ancestor);\n                            }\n                        });\n                    }\n                });\n                return intersections.filter((intersection) => objectSet.has(intersection.object))[0];\n            }\n        }\n        return undefined;\n    }\n    private normalizePointer(pointerPos: PointerPosition, viewport: Scene['viewport']): THREE.Vector2 {\n        return new THREE.Vector2(((pointerPos.x - viewport.x) / viewport.width) * 2 - 1, -((pointerPos.y - viewport.y) / viewport.height) * 2 + 1);\n    }\n    private groupObjectsByScene(): Map<THREE.Scene, THREE.Object3D[]> {\n        const objectsByScene = new Map<THREE.Scene, THREE.Object3D[]>();\n        this.renderer.getScenes().forEach((scene) => {\n            objectsByScene.set(scene.get3DObject() as THREE.Scene, []);\n        });\n        [...this.objectContexts.keys()].forEach((obj) => {\n            if (obj.type !== 'Scene') {\n                let root = obj;\n                while (root.parent) {\n                    root = root.parent;\n                }\n                if (root.type === 'Scene') {\n                    objectsByScene.get(root as THREE.Scene)!.push(obj);\n                }\n            }\n        });\n        return objectsByScene;\n    }\n    private notify(eventType: string, target: THREE.Object3D | 'canvas', pointerPos: PointerPosition, originalEvent: Event, intersection?: THREE.Intersection, bubble: boolean = true): void {\n        const context = target === 'canvas'\n            ? this.canvasContext\n            : this.objectContexts.get(target as THREE.Object3D);\n        const handlers = context?.handlers.get(eventType);\n        if (!(handlers && handlers.length)) {\n            if (target !== 'canvas' && (target as THREE.Object3D).parent && bubble) {\n                this.notify(eventType, (target as THREE.Object3D).parent!, pointerPos, originalEvent, intersection);\n            }\n            return;\n        }\n        handlers.forEach((handler) => {\n            let shouldContinueBubbling = true;\n            const eventData: PointerEventData = {\n                type: eventType,\n                target: target !== 'canvas' ? (target as THREE.Object3D) : undefined,\n                pointer: { ...pointerPos },\n                intersection,\n                button: (originalEvent as MouseEvent).button || 0,\n                isTouch: !!(originalEvent as any).isTouch,\n                touchDuration: (originalEvent as any).touchDuration,\n                clicks: (originalEvent as MouseEvent).detail || 1,\n                altKey: (originalEvent as KeyboardEvent).altKey || false,\n                ctrlKey: (originalEvent as KeyboardEvent).ctrlKey || false,\n                metaKey: (originalEvent as KeyboardEvent).metaKey || false,\n                shiftKey: (originalEvent as KeyboardEvent).shiftKey || false,\n                timeStamp: originalEvent.timeStamp,\n                wheelDeltaY: (originalEvent as WheelEvent).deltaY ?? 0,\n                stopPropagation: () => {\n                    shouldContinueBubbling = false;\n                },\n            };\n            handler.callback(eventData);\n            if (shouldContinueBubbling && target !== 'canvas' && !handler.useCapture &&\n                (target as THREE.Object3D).parent && bubble) {\n                this.notify(eventType, (target as THREE.Object3D).parent!, pointerPos, originalEvent, intersection);\n            }\n        });\n    }\n    dispose(): void {\n        if (this.touchStartBuffer) {\n            clearTimeout(this.touchStartBuffer.timeoutId);\n            this.touchStartBuffer = undefined;\n        }\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/PointerSprite.ts",
    "content": "import { UiObject } from \"./UiObject\";\nimport { ImageUtils } from \"../engine/gfx/ImageUtils\";\nimport { HtmlContainer } from \"./HtmlContainer\";\nimport * as THREE from \"three\";\ninterface Size {\n    width: number;\n    height: number;\n}\nexport class PointerSprite extends UiObject {\n    private images: HTMLCanvasElement;\n    private size: Size;\n    private frameCount: number;\n    private currentFrame: number = 0;\n    private animationRunner?: any;\n    private targetContext?: CanvasRenderingContext2D;\n    static readonly HTML_ZINDEX = 100;\n    static fromShpFile(shpFile: any, palette: any): PointerSprite {\n        return new PointerSprite(ImageUtils.convertShpToCanvas(shpFile, palette), { width: shpFile.width, height: shpFile.height }, shpFile.numImages);\n    }\n    constructor(images: HTMLCanvasElement, size: Size, frameCount: number) {\n        super(new THREE.Object3D(), new HtmlContainer());\n        this.images = images;\n        this.size = size;\n        this.frameCount = frameCount;\n    }\n    setAnimationRunner(animationRunner: any): void {\n        this.animationRunner = animationRunner;\n    }\n    getAnimationRunner(): any {\n        return this.animationRunner;\n    }\n    update(deltaTime: number): void {\n        super.update(deltaTime);\n        if (this.animationRunner) {\n            this.animationRunner.tick(deltaTime);\n            if (this.animationRunner.shouldUpdate()) {\n                this.setFrame(this.animationRunner.getCurrentFrame());\n            }\n        }\n    }\n    getSize(): Size {\n        return this.size;\n    }\n    setFrame(frameIndex: number): void {\n        if (frameIndex !== this.currentFrame) {\n            if (frameIndex < 0 || this.frameCount <= frameIndex) {\n                throw new RangeError(`Pointer frame index out of bounds (index=${frameIndex}, length=${this.frameCount})`);\n            }\n            this.currentFrame = frameIndex;\n            this.drawFrame(frameIndex);\n        }\n    }\n    private drawFrame(frameIndex: number): void {\n        if (this.targetContext) {\n            this.targetContext.clearRect(0, 0, this.size.width, this.size.height);\n            this.targetContext.drawImage(this.images, frameIndex * this.size.width, 0, this.size.width, this.size.height, 0, 0, this.size.width, this.size.height);\n        }\n    }\n    getFrame(): number {\n        return this.currentFrame;\n    }\n    getFrameCount(): number {\n        return this.frameCount;\n    }\n    create3DObject(): void {\n        super.create3DObject();\n        if (!this.targetContext) {\n            const canvas = document.createElement(\"canvas\");\n            const htmlContainer = this.getHtmlContainer();\n            htmlContainer.setTranslateMode(true);\n            const element = htmlContainer.getElement();\n            element.appendChild(canvas);\n            element.style.zIndex = String(PointerSprite.HTML_ZINDEX);\n            const context = canvas.getContext(\"2d\", { alpha: true });\n            if (!context) {\n                throw new Error(\"Couldn't create pointer canvas context\");\n            }\n            this.targetContext = context;\n            this.drawFrame(this.currentFrame);\n        }\n    }\n    destroy(): void {\n        super.destroy();\n    }\n}\n"
  },
  {
    "path": "src/gui/ReactFormat.tsx",
    "content": "import React from 'react';\nconst URL_PATTERN = /(\\[(?:[^\\]]+)\\]\\((?:https?:\\/\\/[^\\s]+|mailto:[^\\s]+)\\))|(https?:\\/\\/[^\\s]+|mailto:[^\\s]+)/g;\nconst MARKDOWN_LINK_PATTERN = /^\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s]+|mailto:[^\\s]+)\\)$/;\nexport class ReactFormat {\n    static formatMultiline(text: string, formatter: (line: string) => React.ReactNode): React.ReactNode[] {\n        return text\n            .split(/\\n/g)\n            .map((line, index) => index ? (<React.Fragment key={index}>\n            <br />\n            {formatter(line)}\n          </React.Fragment>) : formatter(line));\n    }\n    static formatUrls(text: string): React.ReactNode {\n        return (<React.Fragment>\n        {text\n                .split(URL_PATTERN)\n                .filter(Boolean)\n                .map((part, index) => {\n                if (!URL_PATTERN.test(part)) {\n                    return part;\n                }\n                let linkText: string;\n                let href: string;\n                const match = part.match(MARKDOWN_LINK_PATTERN);\n                if (match) {\n                    [, linkText, href] = match;\n                }\n                else {\n                    linkText = href = part;\n                }\n                return (<a key={index} href={href} rel=\"noopener noreferrer\" target=\"_blank\">\n                {linkText}\n              </a>);\n            })}\n      </React.Fragment>);\n    }\n}\n"
  },
  {
    "path": "src/gui/ReplayManager.ts",
    "content": "import { Replay } from \"../network/gamestate/Replay\";\nimport { ReplayExistsError } from \"./replay/ReplayExistsError\";\n\nfunction generateId(): string {\n    if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n        return crypto.randomUUID();\n    }\n    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n        const r = (Math.random() * 16) | 0;\n        const v = c === 'x' ? r : (r & 0x3) | 0x8;\n        return v.toString(16);\n    });\n}\ninterface ReplayManifestEntry {\n    id: string;\n    name: string;\n    keep: boolean;\n    timestamp: number;\n}\ninterface ReplayStorage {\n    getManifest(forceRefresh?: boolean): Promise<ReplayManifestEntry[]>;\n    getReplayData(entry: ReplayManifestEntry): Promise<string | Blob>;\n    hasReplayData(entry: ReplayManifestEntry): Promise<boolean>;\n    saveReplayData(entry: ReplayManifestEntry, data: string): Promise<void>;\n    deleteReplayData(entry: ReplayManifestEntry): Promise<void>;\n    saveManifest(manifest: ReplayManifestEntry[]): Promise<void>;\n}\nexport class ReplayManager {\n    private storage: ReplayStorage;\n    constructor(storage: ReplayStorage) {\n        this.storage = storage;\n    }\n    async loadList(forceRefresh: boolean = false): Promise<ReplayManifestEntry[]> {\n        return await this.storage.getManifest(forceRefresh);\n    }\n    async loadSerializedReplay(entry: ReplayManifestEntry): Promise<string | Blob> {\n        return await this.storage.getReplayData(entry);\n    }\n    async loadReplay(entry: ReplayManifestEntry): Promise<Replay> {\n        const serializedData = await this.loadSerializedReplay(entry);\n        const replay = new Replay();\n        const dataString = typeof serializedData === \"string\"\n            ? serializedData\n            : await serializedData.text();\n        replay.unserialize(dataString, entry);\n        return replay;\n    }\n    async saveReplay(replay: Replay, keep: boolean = false): Promise<string> {\n        const name = replay.name;\n        if (!name) {\n            throw new Error(\"Replay is not initialized\");\n        }\n        const id = generateId();\n        const serializedData = replay.serialize();\n        let entry: ReplayManifestEntry = {\n            id,\n            name,\n            keep,\n            timestamp: replay.timestamp\n        };\n        let counter = 1;\n        while (await this.storage.hasReplayData(entry)) {\n            if (counter > 1) {\n                entry.name = entry.name.replace(/ \\(\\d+\\)$/, \"\");\n            }\n            entry.name += ` (${++counter})`;\n        }\n        let manifest = await this.loadList();\n        const temporaryReplays = manifest.filter((entry) => !entry.keep);\n        if (temporaryReplays.length > 50) {\n            for (const oldReplay of temporaryReplays.slice(50)) {\n                await this.storage.deleteReplayData(oldReplay);\n                manifest.splice(manifest.indexOf(oldReplay), 1);\n            }\n        }\n        manifest.unshift(entry);\n        await this.storage.saveReplayData(entry, serializedData);\n        await this.storage.saveManifest(manifest);\n        return id;\n    }\n    async keepReplay(replayId: string, newName: string): Promise<void> {\n        const manifest = await this.loadList();\n        const existingEntry = manifest.find((entry) => entry.id === replayId);\n        if (existingEntry) {\n            const updatedEntry: ReplayManifestEntry = {\n                ...existingEntry,\n                name: Replay.sanitizeFileName(newName),\n                keep: true,\n            };\n            if (await this.storage.hasReplayData(updatedEntry)) {\n                throw new ReplayExistsError(`A replay with name \"${updatedEntry.name}\" already exists`);\n            }\n            const replayData = await this.storage.getReplayData(existingEntry);\n            const dataString = typeof replayData === \"string\"\n                ? replayData\n                : await replayData.text();\n            await this.storage.deleteReplayData(existingEntry);\n            await this.storage.saveReplayData(updatedEntry, dataString);\n            Object.assign(existingEntry, updatedEntry);\n            await this.storage.saveManifest(manifest);\n        }\n    }\n    async deleteReplay(entry: ReplayManifestEntry): Promise<void> {\n        await this.storage.deleteReplayData(entry);\n        const manifest = await this.loadList();\n        const entryIndex = manifest.findIndex((manifestEntry) => manifestEntry.id === entry.id);\n        if (entryIndex !== -1) {\n            manifest.splice(entryIndex, 1);\n            await this.storage.saveManifest(manifest);\n        }\n    }\n    async importReplay(file: File): Promise<Replay> {\n        return new Promise((resolve, reject) => {\n            const reader = new FileReader();\n            reader.onload = async (event) => {\n                try {\n                    const fileName = file.name.replace(Replay.extension, \"\");\n                    const replay = new Replay();\n                    replay.unserialize(event.target?.result as string, {\n                        name: fileName,\n                        timestamp: file.lastModified,\n                    });\n                    await this.saveReplay(replay, true);\n                    resolve(replay);\n                }\n                catch (error) {\n                    reject(error);\n                }\n            };\n            reader.onerror = () => {\n                reject(reader.error);\n            };\n            reader.readAsText(file, \"utf-8\");\n        });\n    }\n}\n"
  },
  {
    "path": "src/gui/ShpSpriteBatch.ts",
    "content": "import * as THREE from 'three';\nimport { UiObject } from './UiObject';\nimport { HtmlContainer } from './HtmlContainer';\nimport { ShpFile } from '../data/ShpFile';\nimport { BatchShpBuilder } from '../engine/renderable/builder/BatchShpBuilder';\ninterface SpriteProps {\n    image: string | ShpFile;\n    frame?: number;\n    palette: string | any;\n    x?: number;\n    y?: number;\n    zIndex?: number;\n}\ninterface AggregatedShpFile {\n    file: ShpFile;\n    imageIndexes: Map<any, number>;\n}\nexport class ShpSpriteBatch extends UiObject {\n    private spriteProps: SpriteProps[];\n    private getShpFile: (filename: string) => ShpFile;\n    private getPalette: (paletteName: string) => any;\n    private camera: THREE.Camera;\n    private textureCache: Map<any, any>;\n    private batchShpBuilders: BatchShpBuilder[];\n    constructor(spriteProps: SpriteProps[], getShpFile: (filename: string) => ShpFile, getPalette: (paletteName: string) => any, camera: THREE.Camera) {\n        super(new THREE.Object3D(), new HtmlContainer());\n        this.spriteProps = spriteProps;\n        this.getShpFile = getShpFile;\n        this.getPalette = getPalette;\n        this.camera = camera;\n        this.textureCache = new Map();\n        this.batchShpBuilders = [];\n    }\n    create3DObject(): void {\n        super.create3DObject();\n        const aggregatedFile = this.createAggregatedShpFile();\n        this.createObjects(this.get3DObject(), aggregatedFile);\n    }\n    private createAggregatedShpFile(): AggregatedShpFile {\n        let aggregatedShpFile = new ShpFile();\n        aggregatedShpFile.filename = \"agg_unnamed_spritebatch.shp\";\n        let imageIndexes = new Map();\n        let currentIndex = 0;\n        for (const spriteProps of this.spriteProps) {\n            let shpFile = typeof spriteProps.image === \"string\"\n                ? this.getShpFile(spriteProps.image)\n                : spriteProps.image;\n            const image = shpFile.getImage(spriteProps.frame ?? 0);\n            if (!imageIndexes.has(image)) {\n                aggregatedShpFile.addImage(image);\n                imageIndexes.set(image, currentIndex);\n                currentIndex++;\n            }\n        }\n        return { file: aggregatedShpFile, imageIndexes: imageIndexes };\n    }\n    private createObjects(object3D: THREE.Object3D, aggregatedFile: AggregatedShpFile): void {\n        let paletteGroups = new Map<string, SpriteProps[]>();\n        for (const spriteProps of this.spriteProps) {\n            const palette = typeof spriteProps.palette === \"string\"\n                ? this.getPalette(spriteProps.palette)\n                : spriteProps.palette;\n            const paletteHash = palette.hash;\n            const group = paletteGroups.get(paletteHash) ?? [];\n            group.push(spriteProps);\n            paletteGroups.set(paletteHash, group);\n        }\n        for (const spriteGroup of paletteGroups.values()) {\n            const palette = typeof spriteGroup[0].palette === \"string\"\n                ? this.getPalette(spriteGroup[0].palette)\n                : spriteGroup[0].palette;\n            let batchItems: any[] = [];\n            for (const spriteProps of spriteGroup) {\n                let shpFile = typeof spriteProps.image === \"string\"\n                    ? this.getShpFile(spriteProps.image)\n                    : spriteProps.image;\n                const image = shpFile.getImage(spriteProps.frame ?? 0);\n                const frameIndex = aggregatedFile.imageIndexes.get(image);\n                if (frameIndex === undefined) {\n                    throw new Error(\"Missing frame in aggregated sprite shp file\");\n                }\n                const batchItem = {\n                    position: new THREE.Vector3(spriteProps.x ?? 0, spriteProps.y ?? 0, UiObject.zIndexToWorld(spriteProps.zIndex ?? 0)),\n                    shpFile: shpFile,\n                    depth: false,\n                    flat: false,\n                    frameNo: frameIndex,\n                    offset: { x: shpFile.width / 2, y: shpFile.height / 2 },\n                };\n                batchItems.push(batchItem);\n            }\n            if (batchItems.length > 0) {\n                let batchBuilder = new BatchShpBuilder(aggregatedFile.file, palette, this.camera, this.textureCache, undefined, undefined, batchItems.length);\n                batchItems.forEach((item) => batchBuilder.add(item));\n                this.batchShpBuilders.push(batchBuilder);\n                object3D.add(batchBuilder.build());\n            }\n        }\n    }\n    destroy(): void {\n        super.destroy();\n        this.batchShpBuilders.forEach((builder) => builder.dispose());\n        [...this.textureCache.values()].forEach((texture) => texture.dispose());\n        this.textureCache.clear();\n    }\n}\n"
  },
  {
    "path": "src/gui/UiObject.ts",
    "content": "import { EventDispatcher } from '../util/event';\nimport { HtmlContainer } from './HtmlContainer';\nimport { RenderableContainer, Renderable } from '../engine/gfx/RenderableContainer';\nimport * as THREE from 'three';\nexport class UiObject implements Renderable {\n    private rendered: boolean = false;\n    private eventHandlers: Array<{\n        eventName: string;\n        handler: Function;\n        disposer?: Function;\n    }> = [];\n    private _onFrame = new EventDispatcher();\n    private _onDispose = new EventDispatcher();\n    private target?: THREE.Object3D;\n    private htmlContainer?: HtmlContainer;\n    private withPosition: {\n        x: number;\n        y: number;\n        z: number;\n    } = { x: 0, y: 0, z: 0 };\n    private withVisibility: boolean = true;\n    private container: RenderableContainer;\n    private tooltip?: string;\n    private pointerEvents?: any;\n    get onFrame() {\n        return this._onFrame.asEvent();\n    }\n    get onDispose() {\n        return this._onDispose.asEvent();\n    }\n    static zIndexToWorld(zIndex: number): number {\n        return -zIndex;\n    }\n    constructor(target?: THREE.Object3D, htmlContainer?: HtmlContainer) {\n        if (target)\n            this.set3DObject(target);\n        if (htmlContainer)\n            this.setHtmlContainer(htmlContainer);\n        this.container = new RenderableContainer();\n        if (this.target) {\n            this.container.set3DObject(this.target);\n        }\n    }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    set3DObject(target: THREE.Object3D): void {\n        this.target = target;\n        target.matrixAutoUpdate = false;\n    }\n    getRenderableContainer() {\n        return this.container;\n    }\n    getHtmlContainer(): HtmlContainer | undefined {\n        return this.htmlContainer;\n    }\n    setHtmlContainer(htmlContainer: HtmlContainer): void {\n        this.htmlContainer = htmlContainer;\n    }\n    setPosition(x: number, y: number): void {\n        const z = this.withPosition.z || 0;\n        this.withPosition = { x, y, z };\n        if (this.htmlContainer) {\n            this.htmlContainer.setPosition(x, y);\n        }\n        if (this.target) {\n            this.target.position.set(x, y, z);\n            this.target.updateMatrix();\n        }\n    }\n    getPosition(): {\n        x: number;\n        y: number;\n    } {\n        return { x: this.withPosition.x, y: this.withPosition.y };\n    }\n    setZIndex(zIndex: number): void {\n        const { x, y } = this.withPosition;\n        this.withPosition = { x, y, z: UiObject.zIndexToWorld(zIndex) };\n        if (this.target) {\n            this.target.position.set(x, y, this.withPosition.z);\n            this.target.updateMatrix();\n        }\n    }\n    setVisible(visible: boolean): void {\n        this.withVisibility = visible;\n        this.htmlContainer?.setVisible(visible);\n        if (this.target) {\n            this.target.visible = visible;\n        }\n    }\n    isVisible(): boolean {\n        return this.withVisibility;\n    }\n    setTooltip(tooltip: string): void {\n        this.tooltip = tooltip;\n        this.updateTooltip();\n    }\n    updateTooltip(): void {\n        const obj = this.get3DObject();\n        if (obj) {\n            obj.userData.tooltip = this.tooltip;\n        }\n    }\n    setPointerEvents(pointerEvents: any): void {\n        if (this.pointerEvents) {\n            throw new Error('A PointerEvents instance is already set');\n        }\n        this.pointerEvents = pointerEvents;\n    }\n    addEventListener(eventName: string, handler: Function): () => void {\n        this.eventHandlers.push({ eventName, handler });\n        if (this.rendered) {\n            this.setupEventListener(eventName, handler);\n        }\n        return () => this.removeEventListener(eventName, handler);\n    }\n    removeEventListener(eventName: string, handler: Function): void {\n        const index = this.eventHandlers.findIndex((e) => eventName === e.eventName && handler === e.handler);\n        if (index !== -1) {\n            this.eventHandlers[index].disposer?.();\n            this.eventHandlers.splice(index, 1);\n        }\n    }\n    setupEventListener(eventName: string, handler: Function): void {\n        if (!this.pointerEvents) {\n            throw new Error('A PointerEvents object must be provided prior to setting up an event listener');\n        }\n        const disposer = this.pointerEvents.addEventListener(this.get3DObject(), eventName, handler);\n        const eventHandler = this.eventHandlers.find((e) => eventName === e.eventName && handler === e.handler);\n        if (eventHandler) {\n            eventHandler.disposer = disposer;\n        }\n    }\n    create3DObject(): void {\n        if (!this.get3DObject()) {\n            throw new Error('Expecting a THREE.Object3D to have been set by now');\n        }\n        if (!this.rendered) {\n            this.rendered = true;\n            const { x, y, z } = this.withPosition;\n            if (this.target) {\n                this.target.position.set(x, y, z);\n                this.target.visible = this.withVisibility;\n                this.target.updateMatrix();\n            }\n            this.htmlContainer?.render();\n            this.htmlContainer?.setPosition(x, y);\n            this.htmlContainer?.setVisible(this.withVisibility);\n            this.container.set3DObject(this.get3DObject()!);\n            this.container.create3DObject();\n            this.updateTooltip();\n            this.eventHandlers.forEach((e) => this.setupEventListener(e.eventName, e.handler));\n        }\n        else {\n        }\n    }\n    update(deltaTime: number): void {\n        this.container.update(deltaTime);\n        this._onFrame.dispatch(this, deltaTime);\n    }\n    add(...children: UiObject[]): void {\n        this.container.add(...children);\n        children\n            .map((child) => child.getHtmlContainer())\n            .forEach((htmlContainer, index) => {\n            if (htmlContainer) {\n                if (!this.htmlContainer) {\n                    console.error(`[UiObject] Parent has no HTML container but child has one!`);\n                    throw new Error(\"Can't add an UiObject that defines an HTMLContainer to a parent that doesn't provide an HTML container.\");\n                }\n                this.htmlContainer.add(htmlContainer);\n            }\n        });\n    }\n    remove(...children: UiObject[]): void {\n        children\n            .map((child) => child.getHtmlContainer())\n            .forEach((htmlContainer) => {\n            if (htmlContainer) {\n                this.htmlContainer?.remove(htmlContainer);\n            }\n        });\n        this.container.remove(...children);\n    }\n    removeAll(): void {\n        this.container.removeAll();\n    }\n    destroy(): void {\n        this.container.getChildren().forEach((child) => child.destroy?.());\n        this.htmlContainer?.unrender();\n        this.eventHandlers.forEach((e) => e.disposer?.());\n        this.eventHandlers.length = 0;\n        this._onFrame = new EventDispatcher();\n        this._onDispose.dispatch('dispose', undefined);\n        this._onDispose = new EventDispatcher();\n    }\n}\n"
  },
  {
    "path": "src/gui/UiObjectSprite.ts",
    "content": "import { UiObject } from './UiObject';\nimport { ShpFile } from '../data/ShpFile';\nimport { Palette } from '../data/Palette';\nimport { ShpBuilder } from '../engine/renderable/builder/ShpBuilder';\nimport * as THREE from 'three';\nexport class UiObjectSprite extends UiObject {\n    private builder: any;\n    private animationRunner?: any;\n    private initialTransparency?: boolean;\n    private initialOpacity?: number;\n    private initialLightMult?: number;\n    static fromShpFile(shpFile: ShpFile, palette: Palette, camera: THREE.Camera): UiObjectSprite {\n        const builder = new ShpBuilder(shpFile, palette, camera);\n        builder.setBatched(true);\n        builder.setBatchPalettes([palette]);\n        builder.setOffset({\n            x: Math.floor(shpFile.width / 2),\n            y: Math.floor(shpFile.height / 2)\n        });\n        return new UiObjectSprite(builder);\n    }\n    constructor(builder: any) {\n        super();\n        this.builder = builder;\n    }\n    setAnimationRunner(animationRunner: any): void {\n        this.animationRunner = animationRunner;\n    }\n    getAnimationRunner(): any {\n        return this.animationRunner;\n    }\n    update(deltaTime: number): void {\n        super.update(deltaTime);\n        if (this.animationRunner) {\n            this.animationRunner.tick(deltaTime);\n            if (this.animationRunner.shouldUpdate()) {\n                this.setFrame(this.animationRunner.getCurrentFrame());\n            }\n        }\n    }\n    getSize(): {\n        width: number;\n        height: number;\n    } {\n        return this.builder.getSize();\n    }\n    setFrame(frame: number): void {\n        this.builder.setFrame(frame);\n    }\n    getFrame(): number {\n        return this.builder.getFrame();\n    }\n    getFrameCount(): number {\n        return this.builder.frameCount;\n    }\n    setTransparent(transparent: boolean): void {\n        if (this.get3DObject()) {\n            this.builder.setForceTransparent(transparent);\n        }\n        else {\n            this.initialTransparency = transparent;\n        }\n    }\n    setOpacity(opacity: number): void {\n        if (this.get3DObject()) {\n            this.builder.setOpacity(opacity);\n        }\n        else {\n            this.initialOpacity = opacity;\n        }\n    }\n    setLightMult(lightMult: number): void {\n        if (this.get3DObject() && typeof this.builder.setExtraLight === 'function') {\n            this.builder.setExtraLight(new THREE.Vector3().addScalar(-1 + lightMult));\n        }\n        else {\n            this.initialLightMult = lightMult;\n        }\n    }\n    create3DObject(): void {\n        const mesh = this.builder.build();\n        this.set3DObject(mesh);\n        super.create3DObject();\n        if (this.initialTransparency !== undefined) {\n            this.builder.setForceTransparent(this.initialTransparency);\n        }\n        if (this.initialOpacity !== undefined) {\n            this.builder.setOpacity(this.initialOpacity);\n        }\n        if (this.initialLightMult !== undefined) {\n            if (typeof this.builder.setExtraLight === 'function') {\n                this.builder.setExtraLight(new THREE.Vector3().addScalar(this.initialLightMult));\n            }\n        }\n    }\n    destroy(): void {\n        super.destroy();\n        this.builder.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/UiScene.ts",
    "content": "import * as THREE from 'three';\nimport { UiObject } from './UiObject';\nimport { HtmlContainer } from './HtmlContainer';\nimport { MeshBatchManager } from '../engine/gfx/batch/MeshBatchManager';\nexport class UiScene extends UiObject {\n    private scene: THREE.Scene;\n    private camera: THREE.Camera;\n    public viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    private meshBatchManager?: MeshBatchManager;\n    static factory(viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }): UiScene {\n        let scene = new THREE.Scene();\n        scene.matrixAutoUpdate = false;\n        const camera = UiScene.createCamera(viewport);\n        const htmlContainer = new HtmlContainer();\n        return new UiScene(scene, camera, viewport, htmlContainer);\n    }\n    static createCamera(viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }): THREE.Camera {\n        const halfHeight = viewport.height / 2;\n        const aspectRatio = viewport.width / viewport.height;\n        let camera = new THREE.OrthographicCamera(-halfHeight * aspectRatio, halfHeight * aspectRatio, halfHeight, -halfHeight, -1000, 1000);\n        camera.rotation.x = Math.PI;\n        camera.position.x = -viewport.x + viewport.width / 2;\n        camera.position.y = -viewport.y + viewport.height / 2;\n        camera.position.z = -1000;\n        return camera;\n    }\n    constructor(scene: THREE.Scene, camera: THREE.Camera, viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }, htmlContainer: HtmlContainer) {\n        super(scene, htmlContainer);\n        this.scene = scene;\n        this.camera = camera;\n        this.viewport = viewport;\n    }\n    setCamera(camera: THREE.Camera): void {\n        this.camera = camera;\n    }\n    setViewport(viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }): void {\n        this.viewport = viewport;\n    }\n    create3DObject(): void {\n        super.create3DObject();\n        if (!this.meshBatchManager) {\n            const meshBatchManager = this.meshBatchManager = new MeshBatchManager(this.getRenderableContainer());\n            this.getRenderableContainer().add(meshBatchManager);\n            this.scene.matrixAutoUpdate = false;\n        }\n    }\n    update(deltaTime: number): void {\n        super.update(deltaTime);\n        if (this.meshBatchManager) {\n            this.scene.updateMatrixWorld(false);\n            this.meshBatchManager.updateMeshes();\n        }\n    }\n    get menuViewport(): {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    } {\n        const menuWidth = 800;\n        const menuHeight = 600;\n        return {\n            x: Math.max(0, (this.viewport.width - menuWidth) / 2),\n            y: Math.max(0, (this.viewport.height - menuHeight) / 2),\n            width: menuWidth,\n            height: menuHeight,\n        };\n    }\n    getScene(): THREE.Scene {\n        return this.scene;\n    }\n    getCamera(): THREE.Camera {\n        return this.camera;\n    }\n    destroy(): void {\n        super.destroy();\n        this.meshBatchManager?.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/Viewport.ts",
    "content": "export interface ViewportRect {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n    displayWidth?: number;\n    displayHeight?: number;\n    scale?: number;\n    isMobileLayout?: boolean;\n    isPortrait?: boolean;\n}\nexport interface Viewport {\n    value: ViewportRect;\n    rootElement?: HTMLElement;\n}\n"
  },
  {
    "path": "src/gui/chat/ChatHistory.ts",
    "content": "import { ChatRecipientType } from '@/network/chat/ChatMessage';\nimport { RECIPIENT_ALL } from '@/network/gservConfig';\nimport { BoxedVar } from '@/util/BoxedVar';\nimport { EventDispatcher } from '@/util/event';\nexport class ChatHistory {\n    private lastWhisperFrom: BoxedVar<string | undefined>;\n    private lastWhisperTo: BoxedVar<string | undefined>;\n    private lastComposeTarget: BoxedVar<{\n        type: ChatRecipientType;\n        name: string;\n    }>;\n    private messages: any[];\n    private _onNewMessage: EventDispatcher;\n    constructor() {\n        this.lastWhisperFrom = new BoxedVar(undefined);\n        this.lastWhisperTo = new BoxedVar(undefined);\n        this.lastComposeTarget = new BoxedVar({\n            type: ChatRecipientType.Channel,\n            name: RECIPIENT_ALL,\n        });\n        this.messages = [];\n        this._onNewMessage = new EventDispatcher();\n    }\n    get onNewMessage() {\n        return this._onNewMessage.asEvent();\n    }\n    addChatMessage(message: any) {\n        this.messages.push(message);\n        this._onNewMessage.dispatch(this, message);\n    }\n    reset() {\n        this.messages = [];\n    }\n    getAll() {\n        return this.messages;\n    }\n}\n"
  },
  {
    "path": "src/gui/chat/ChatMessageFormat.tsx",
    "content": "import React from 'react';\nimport { ChatRecipientType } from '@/network/chat/ChatMessage';\nimport { RECIPIENT_TEAM } from '@/network/gservConfig';\nimport { ReactFormat } from '@/gui/ReactFormat';\ninterface ChatMessageFormatProps {\n    strings: {\n        get: (key: string, ...args: any[]) => string;\n    };\n    localUsername: string;\n    userColors?: Map<string, string>;\n}\nexport class ChatMessageFormat {\n    private strings: ChatMessageFormatProps['strings'];\n    private localUsername: string;\n    private userColors?: Map<string, string>;\n    constructor(strings: ChatMessageFormatProps['strings'], localUsername: string, userColors?: Map<string, string>) {\n        this.strings = strings;\n        this.localUsername = localUsername;\n        this.userColors = userColors;\n    }\n    formatPrefixPlain(message: {\n        to: {\n            type: ChatRecipientType;\n            name: string;\n        };\n        from: string;\n    }) {\n        let prefix: string;\n        if (message.to.type === ChatRecipientType.Channel) {\n            prefix = message.to.name === RECIPIENT_TEAM\n                ? this.strings.get(\"TS:ChatFromAllies\", message.from)\n                : this.strings.get(\"TS:ChatFrom\", message.from);\n        }\n        else if (message.to.type === ChatRecipientType.Page) {\n            prefix = this.strings.get(\"TS:PageFrom\", message.from);\n        }\n        else {\n            if (message.to.type !== ChatRecipientType.Whisper) {\n                throw new Error(\"Unknown message type \" + message.to.type);\n            }\n            prefix = message.from === this.localUsername\n                ? this.strings.get(\"TS:To\", message.to.name)\n                : this.strings.get(\"TXT_FROM\", message.from);\n        }\n        return prefix;\n    }\n    formatPrefixHtml(message: {\n        to: {\n            type: ChatRecipientType;\n            name: string;\n        };\n        from: string;\n        time: Date;\n    }, onUserClick?: (username: string) => void): React.ReactNode {\n        const displayName = message.to.type === ChatRecipientType.Whisper && message.from === this.localUsername\n            ? message.to.name\n            : message.from;\n        let formattedName: React.ReactNode = displayName;\n        const userPlaceholder = \"{user}\";\n        if (message.to.type !== ChatRecipientType.Page) {\n            const userColor = this.userColors?.get(message.from);\n            if (userColor !== undefined) {\n                formattedName = React.createElement(\"span\", { style: { color: userColor } }, formattedName);\n            }\n            if (onUserClick) {\n                const [prefix, suffix] = this.strings.get(\"TS:ChatUserLink\", userPlaceholder).split(userPlaceholder);\n                formattedName = React.createElement(\"span\", { className: \"user-link\", onClick: () => onUserClick(displayName) }, prefix, formattedName, suffix);\n            }\n        }\n        const timestamp = this.strings.get(\"TS:ChatTimestamp\", message.time.toLocaleTimeString(undefined, { timeStyle: \"short\" })) + \" \";\n        let formatString: string;\n        if (message.to.type === ChatRecipientType.Channel) {\n            formatString = message.to.name === RECIPIENT_TEAM\n                ? this.strings.get(\"TS:ChatFromAllies\", userPlaceholder)\n                : this.strings.get(\"TS:ChatFrom\", userPlaceholder);\n        }\n        else if (message.to.type === ChatRecipientType.Page) {\n            formatString = this.strings.get(\"TS:PageFrom\", userPlaceholder);\n        }\n        else {\n            if (message.to.type !== ChatRecipientType.Whisper) {\n                throw new Error(\"Unknown message type \" + message.to.type);\n            }\n            formatString = message.from === this.localUsername\n                ? this.strings.get(\"TS:To\", userPlaceholder)\n                : this.strings.get(\"TXT_FROM\", userPlaceholder);\n        }\n        const [prefix, suffix] = formatString.split(userPlaceholder);\n        return React.createElement(React.Fragment, null, timestamp, prefix, formattedName, suffix);\n    }\n    formatTextHtml(text: string, formatUrls: boolean): React.ReactNode {\n        return formatUrls ? ReactFormat.formatUrls(text) : text;\n    }\n}\n"
  },
  {
    "path": "src/gui/component/BasicErrorBoxApi.tsx",
    "content": "import { CompositeDisposable } from '../../util/disposable/CompositeDisposable';\nimport { BoxedVar } from '../../util/BoxedVar';\nimport { Strings } from '../../data/Strings';\nimport { HtmlReactElement } from '../HtmlReactElement';\nimport { Dialog } from './Dialog';\nimport { ReactFormat } from '../ReactFormat';\nexport class BasicErrorBoxApi {\n    private viewport: BoxedVar<{\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }>;\n    private strings: Strings;\n    private rootEl: HTMLElement;\n    private disposables: CompositeDisposable;\n    private component?: HtmlReactElement;\n    constructor(viewport: BoxedVar<{\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }>, strings: Strings, rootEl: HTMLElement) {\n        this.viewport = viewport;\n        this.strings = strings;\n        this.rootEl = rootEl;\n        this.disposables = new CompositeDisposable();\n    }\n    async show(message: string, fatal: boolean = false): Promise<void> {\n        return new Promise((resolve) => {\n            this.component = HtmlReactElement.factory(Dialog, {\n                children: ReactFormat.formatMultiline(message, (line) => ReactFormat.formatUrls(line)),\n                className: 'basic-error-box',\n                viewport: this.viewport.value,\n                buttons: fatal\n                    ? []\n                    : [\n                        {\n                            label: this.strings.get('GUI:Ok'),\n                            onClick: () => {\n                                this.destroy();\n                                resolve();\n                            }\n                        }\n                    ]\n            });\n            const handleViewportChange = (viewport: {\n                x: number;\n                y: number;\n                width: number;\n                height: number;\n            }) => {\n                if (this.component) {\n                    this.component.setSize(viewport.width, viewport.height);\n                    this.component.applyOptions((props) => {\n                        props.viewport = viewport;\n                    });\n                }\n            };\n            this.viewport.onChange.subscribe(handleViewportChange);\n            this.component.setSize(this.viewport.value.width, this.viewport.value.height);\n            this.component.render();\n            this.rootEl.appendChild(this.component.getElement()!);\n            this.disposables.add(() => this.viewport.onChange.unsubscribe(handleViewportChange), () => {\n                if (this.component?.getElement() && this.rootEl.contains(this.component.getElement()!)) {\n                    this.rootEl.removeChild(this.component.getElement()!);\n                }\n            }, () => this.component?.unrender(), () => { this.component = undefined; });\n        });\n    }\n    destroy(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/component/BotUploadDialog.tsx",
    "content": "import React from 'react';\nimport { BotUploader } from '@/game/ai/thirdpartbot/BotUploader';\nimport { BotRegistry } from '@/game/ai/thirdpartbot/BotRegistry';\nimport { ThirdPartyBotMeta } from '@/game/ai/thirdpartbot/ThirdPartyBotInterface';\n\ninterface BotUploadDialogProps {\n    strings: any;\n    onClose: () => void;\n    onBotRegistered?: (meta: ThirdPartyBotMeta) => void;\n}\n\ninterface BotUploadDialogState {\n    uploading: boolean;\n    message: string;\n    messageType: 'info' | 'success' | 'error';\n    registeredBots: ThirdPartyBotMeta[];\n}\n\nexport class BotUploadDialog extends React.Component<BotUploadDialogProps, BotUploadDialogState> {\n    private fileInputRef: React.RefObject<HTMLInputElement>;\n\n    constructor(props: BotUploadDialogProps) {\n        super(props);\n        this.fileInputRef = React.createRef();\n        this.state = {\n            uploading: false,\n            message: '',\n            messageType: 'info',\n            registeredBots: BotRegistry.getInstance().getUploadedBots(),\n        };\n    }\n\n    private handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {\n        const file = event.target.files?.[0];\n        if (!file) return;\n\n        this.setState({ uploading: true, message: '', messageType: 'info' });\n\n        try {\n            const result = await BotUploader.processUpload(file);\n\n            if (result.success && result.meta) {\n                this.setState({\n                    uploading: false,\n                    message: this.props.strings.get('GUI:BotUpload:Success') || 'Bot uploaded successfully!',\n                    messageType: 'success',\n                    registeredBots: BotRegistry.getInstance().getUploadedBots(),\n                });\n                this.props.onBotRegistered?.(result.meta);\n            } else {\n                this.setState({\n                    uploading: false,\n                    message: (result.errors || ['Upload failed']).join('\\n'),\n                    messageType: 'error',\n                });\n            }\n        } catch (e) {\n            this.setState({\n                uploading: false,\n                message: `Error: ${(e as Error).message}`,\n                messageType: 'error',\n            });\n        }\n\n        // Reset file input\n        if (this.fileInputRef.current) {\n            this.fileInputRef.current.value = '';\n        }\n    };\n\n    private handleRemoveBot = (botId: string) => {\n        BotRegistry.getInstance().unregister(botId);\n        this.setState({\n            registeredBots: BotRegistry.getInstance().getUploadedBots(),\n        });\n    };\n\n    render() {\n        const { strings, onClose } = this.props;\n        const { uploading, message, messageType, registeredBots } = this.state;\n\n        return (\n            <div className=\"bot-upload-dialog-overlay\" onClick={onClose}>\n                <div\n                    className=\"bot-upload-dialog\"\n                    onClick={(e) => e.stopPropagation()}\n                >\n                    <div className=\"bot-upload-header\">\n                        <h3>{strings.get('GUI:BotUpload:Title') || 'Upload AI Bot Script'}</h3>\n                        <button\n                            className=\"bot-upload-close\"\n                            onClick={onClose}\n                        >\n                            ×\n                        </button>\n                    </div>\n\n                    <div className=\"bot-upload-body\">\n                        <div className=\"bot-upload-section\">\n                            <label className=\"bot-upload-label\">\n                                {strings.get('GUI:BotUpload:Select') || 'Select Bot Zip File'}\n                            </label>\n                            <input\n                                ref={this.fileInputRef as any}\n                                type=\"file\"\n                                accept=\".zip\"\n                                onChange={this.handleFileSelect}\n                                disabled={uploading}\n                                className=\"bot-upload-input\"\n                            />\n                            <div className=\"bot-upload-hint\">\n                                {strings.get('GUI:BotUpload:Hint') || 'Upload a .zip file containing bot.ts or index.ts'}\n                            </div>\n                        </div>\n\n                        {uploading && (\n                            <div className=\"bot-upload-status\">Loading...</div>\n                        )}\n\n                        {message && (\n                            <div className={`bot-upload-message bot-upload-message-${messageType}`}>\n                                {message}\n                            </div>\n                        )}\n\n                        <div className=\"bot-upload-section\">\n                            <h4>{strings.get('GUI:BotUpload:Manage') || 'Manage Bots'}</h4>\n                            {registeredBots.length === 0 ? (\n                                <div className=\"bot-upload-empty\">\n                                    {strings.get('GUI:BotUpload:NoBot') || 'No custom bots uploaded'}\n                                </div>\n                            ) : (\n                                <ul className=\"bot-upload-list\">\n                                    {registeredBots.map((bot) => (\n                                        <li key={bot.id} className=\"bot-upload-item\">\n                                            <div className=\"bot-upload-item-info\">\n                                                <span className=\"bot-upload-item-name\">\n                                                    {bot.displayName}\n                                                </span>\n                                                <span className=\"bot-upload-item-version\">\n                                                    v{bot.version}\n                                                </span>\n                                                <span className=\"bot-upload-item-author\">\n                                                    by {bot.author}\n                                                </span>\n                                            </div>\n                                            <button\n                                                className=\"bot-upload-item-remove\"\n                                                onClick={() => this.handleRemoveBot(bot.id)}\n                                            >\n                                                {strings.get('GUI:BotUpload:Remove') || 'Remove'}\n                                            </button>\n                                        </li>\n                                    ))}\n                                </ul>\n                            )}\n                        </div>\n                    </div>\n\n                    <div className=\"bot-upload-footer\">\n                        <button\n                            className=\"dialog-button\"\n                            onClick={onClose}\n                        >\n                            {strings.get('GUI:Ok') || 'OK'}\n                        </button>\n                    </div>\n                </div>\n            </div>\n        );\n    }\n}\n"
  },
  {
    "path": "src/gui/component/ButtonSelect.tsx",
    "content": "import React, { useState, useRef, useEffect, ReactNode, ReactElement, CSSProperties } from \"react\";\nimport classNames from \"classnames\";\ninterface ButtonSelectProps {\n    initialValue: any;\n    disabled?: boolean;\n    tooltip?: string;\n    className?: string;\n    onSelect: (value: any) => void;\n    labelStyle?: (value: any) => CSSProperties;\n    children: ReactNode;\n}\nconst ButtonSelect: React.FC<ButtonSelectProps> = ({ initialValue, disabled, tooltip, className, onSelect, labelStyle, children, }) => {\n    const [selected, setSelected] = useState(() => initialValue);\n    const [hovered, setHovered] = useState(() => initialValue);\n    const ref = useRef<HTMLDivElement>(null);\n    useEffect(() => {\n        if (selected !== initialValue) {\n            setSelected(initialValue);\n            setHovered(initialValue);\n        }\n    }, [initialValue]);\n    useEffect(() => {\n        setHovered(selected);\n    }, []);\n    return (<div className={classNames(\"button-select\", { disabled }, className)} data-r-tooltip={tooltip} ref={ref}>\n      {React.Children.map(children, (child) => {\n            if (!child)\n                return null;\n            const element = child as ReactElement<any>;\n            const value = element.props.value;\n            const childDisabled = element.props.disabled;\n            return (<div onMouseEnter={() => !childDisabled && setHovered(value)} onMouseLeave={() => hovered === value && setHovered(undefined)}>\n            {React.cloneElement(element, {\n                    selected: value === selected || value === hovered,\n                    disabled: childDisabled || disabled,\n                    labelStyle: labelStyle?.(value),\n                    onClick: () => {\n                        setSelected(value);\n                        setHovered(value);\n                        onSelect(value);\n                    },\n                })}\n          </div>);\n        })}\n    </div>);\n};\nexport default ButtonSelect;\n"
  },
  {
    "path": "src/gui/component/ChannelOpIndicator.tsx",
    "content": "import React from \"react\";\nimport { Image } from \"./Image\";\ninterface ChannelOpIndicatorProps {\n    operator: boolean;\n}\nconst ChannelOpIndicator: React.FC<ChannelOpIndicatorProps> = ({ operator }) => (<div className=\"channel-op-indicator\">\n    {operator ? <Image src=\"woloper.pcx\"/> : null}\n  </div>);\nexport default ChannelOpIndicator;\n"
  },
  {
    "path": "src/gui/component/ChannelUser.tsx",
    "content": "import React from \"react\";\nimport classNames from \"classnames\";\nimport { RankIndicator } from \"@/gui/screen/mainMenu/lobby/component/RankIndicator\";\nimport ChannelOpIndicator from \"./ChannelOpIndicator\";\ninterface ChannelUserProps {\n    user: {\n        name: string;\n        operator?: boolean;\n    };\n    playerProfile?: {\n        rank?: number;\n        rankType?: string;\n    };\n    strings: {\n        get: (key: string) => string;\n    };\n    onClick?: () => void;\n}\nconst RANK_LABELS = RankIndicator.RANK_LABELS || new Map();\nconst ChannelUser: React.FC<ChannelUserProps> = ({ user, playerProfile, strings, onClick, }) => {\n    let tooltip = user.name;\n    if (user.operator) {\n        tooltip += \" : \" + strings.get(\"TXT_OPER\");\n    }\n    tooltip +=\n        playerProfile && playerProfile.rank !== undefined\n            ? \" : \" + strings.get(RANK_LABELS.get(playerProfile.rankType))\n            : \" : \" + strings.get(\"TXT_UNRANKED\");\n    return (<div className={classNames(\"player\", { operator: user.operator })} data-r-tooltip={tooltip} onClick={onClick}>\n      <ChannelOpIndicator operator={!!user.operator}/>\n      <RankIndicator playerProfile={playerProfile} strings={strings}/>\n      {user.name}\n    </div>);\n};\nexport default ChannelUser;\n"
  },
  {
    "path": "src/gui/component/Chat.tsx",
    "content": "import { Component, Fragment } from 'react';\nimport classNames from 'classnames';\nimport { ChatRecipientType } from '@/network/chat/ChatMessage';\nimport { ChatMessageFormat } from '@/gui/chat/ChatMessageFormat';\nimport { ChatInput } from '@/gui/component/ChatInput';\ninterface ChatProps {\n    messages: any[];\n    tooltips?: {\n        output?: string;\n        input?: string;\n        button?: string;\n    };\n    strings: any;\n    chatHistory?: {\n        lastComposeTarget?: {\n            value: {\n                type: ChatRecipientType;\n                name: string;\n            };\n            onChange?: {\n                subscribe: (callback: (value: any) => void) => void;\n                unsubscribe: (callback: (value: any) => void) => void;\n            };\n        };\n        lastWhisperFrom?: {\n            value: string;\n            onChange?: {\n                subscribe: (callback: () => void) => void;\n                unsubscribe: (callback: () => void) => void;\n            };\n        };\n        lastWhisperTo?: {\n            value: string;\n            onChange?: {\n                subscribe: (callback: () => void) => void;\n                unsubscribe: (callback: () => void) => void;\n            };\n        };\n    };\n    channels?: any[];\n    localUsername?: string;\n    userColors?: any;\n    onSendMessage: (message: any) => void;\n    onCancelMessage: () => void;\n}\nconst messageTypeMap = new Map<ChatRecipientType, string>()\n    .set(ChatRecipientType.Channel, \"type-channel\")\n    .set(ChatRecipientType.Page, \"type-page\")\n    .set(ChatRecipientType.Whisper, \"type-whisper\");\nexport class Chat extends Component<ChatProps> {\n    private prevMessageCount = 0;\n    private prevOldestMessage: any;\n    private prevScrollHeight = 0;\n    private messageList?: HTMLDivElement | null;\n    private textBox?: {\n        send: () => void;\n    } | null;\n    render() {\n        const { messages, tooltips, strings, chatHistory, channels } = this.props;\n        return (<div className=\"chat-wrapper\">\n        <div className=\"messages\" ref={el => { this.messageList = el; }} data-r-tooltip={tooltips?.output}>\n          {messages.map((message, index) => this.renderMessage(message, index))}\n        </div>\n        <div className=\"new-message-wrapper\">\n          <ChatInput ref={el => { this.textBox = el; }} chatHistory={chatHistory} channels={channels} className=\"new-message\" tooltip={tooltips?.input} strings={strings} onSubmit={this.props.onSendMessage} onCancel={this.props.onCancelMessage}/>\n          <button className=\"icon-button send-message-button\" data-r-tooltip={tooltips?.button} onClick={() => this.textBox?.send()}/>\n        </div>\n      </div>);\n    }\n    componentDidUpdate(prevProps: ChatProps) {\n        if (this.props.messages[0] === this.prevOldestMessage &&\n            this.props.messages.length === this.prevMessageCount) {\n            return;\n        }\n        this.prevMessageCount = this.props.messages.length;\n        this.prevOldestMessage = this.props.messages[0];\n        if (!this.messageList) {\n            return;\n        }\n        const scrollHeight = this.messageList.scrollHeight;\n        const clientHeight = this.messageList.clientHeight;\n        if (scrollHeight !== this.prevScrollHeight &&\n            (!this.prevScrollHeight ||\n                Math.abs(this.messageList.scrollTop - (this.prevScrollHeight - clientHeight)) <= 1)) {\n            this.messageList.scrollTop = scrollHeight - clientHeight;\n        }\n        this.prevScrollHeight = scrollHeight;\n    }\n    private renderMessage(message: any, index: number) {\n        const formatter = new ChatMessageFormat(this.props.strings, this.props.localUsername, this.props.userColors);\n        const classes = [\"message\"];\n        let prefix: React.ReactNode;\n        if (message.from !== undefined) {\n            prefix = formatter.formatPrefixHtml(message, (name: string) => {\n                if (this.props.chatHistory &&\n                    message.to &&\n                    message.to.type !== ChatRecipientType.Page &&\n                    this.props.chatHistory.lastComposeTarget) {\n                    this.props.chatHistory.lastComposeTarget.value = {\n                        type: ChatRecipientType.Whisper,\n                        name\n                    };\n                }\n            });\n            const messageTypeClass = messageTypeMap.get(message.to.type);\n            if (messageTypeClass) {\n                classes.push(messageTypeClass);\n            }\n            if (message.operator) {\n                classes.push(\"operator-message\");\n            }\n        }\n        const isSystemMessage = message.from === undefined;\n        const text = formatter.formatTextHtml(message.text, isSystemMessage);\n        return (<div key={index} className={classNames(classes)}>\n        {prefix ? (<Fragment>\n            <span>{prefix}</span> {text}\n          </Fragment>) : text}\n      </div>);\n    }\n}\n"
  },
  {
    "path": "src/gui/component/ChatInput.tsx",
    "content": "import React, { useRef, useState, useEffect, useImperativeHandle, forwardRef } from 'react';\nimport { ChatRecipientType } from '@/network/chat/ChatMessage';\nimport { RECIPIENT_TEAM, RECIPIENT_ALL } from '@/network/gservConfig';\nconst IMPLICIT_CHANNEL_NAME = '';\ninterface ChatInputProps {\n    chatHistory?: {\n        lastComposeTarget?: {\n            value: {\n                type: ChatRecipientType;\n                name: string;\n            };\n            onChange?: {\n                subscribe: (callback: (value: any) => void) => void;\n                unsubscribe: (callback: (value: any) => void) => void;\n            };\n        };\n        lastWhisperFrom?: {\n            value: string;\n            onChange?: {\n                subscribe: (callback: () => void) => void;\n                unsubscribe: (callback: () => void) => void;\n            };\n        };\n        lastWhisperTo?: {\n            value: string;\n            onChange?: {\n                subscribe: (callback: () => void) => void;\n                unsubscribe: (callback: () => void) => void;\n            };\n        };\n    };\n    channels?: string[];\n    strings: {\n        get: (key: string, ...args: any[]) => string;\n    };\n    className?: string;\n    tooltip?: string;\n    forceColor?: string;\n    noCycleHint?: boolean;\n    submitEmpty?: boolean;\n    onKeyDown?: (e: React.KeyboardEvent) => void;\n    onKeyUp?: (e: React.KeyboardEvent) => void;\n    onBlur?: () => void;\n    onCancel?: (isEmpty: boolean) => void;\n    onSubmit: (data: {\n        recipient: {\n            type: ChatRecipientType;\n            name: string;\n        };\n        value: string;\n    }) => void;\n}\nexport const ChatInput = forwardRef<{\n    send: () => void;\n}, ChatInputProps>(({ chatHistory: s, channels: r = [], strings: t, className: e, tooltip: i, forceColor: a, noCycleHint: n, submitEmpty: o, onKeyDown: l, onKeyUp: c, onBlur: h, onCancel: u, onSubmit: d, }, g) => {\n    const p = useRef<HTMLInputElement>(null);\n    const [m, f] = useState(() => M());\n    const [y, T] = useState(() => {\n        const e = s?.lastComposeTarget?.value;\n        return A(e) ? e : { type: ChatRecipientType.Channel, name: r[0] ?? IMPLICIT_CHANNEL_NAME };\n    });\n    const [v, b] = useState<string>();\n    const [S, w] = useState<string>();\n    const [C, E] = useState(false);\n    const [x, O] = useState(false);\n    function M() {\n        const e = (r.length ? r : [IMPLICIT_CHANNEL_NAME]).map((e) => ({\n            type: ChatRecipientType.Channel,\n            name: e,\n        }));\n        let t: string | undefined, i: string | undefined;\n        if (s) {\n            t = s.lastWhisperFrom?.value;\n            i = s.lastWhisperTo?.value;\n            if (t)\n                e.push({ type: ChatRecipientType.Whisper, name: t });\n            if (i && i !== t)\n                e.push({ type: ChatRecipientType.Whisper, name: i });\n        }\n        return e;\n    }\n    function A(e: {\n        type: ChatRecipientType;\n        name: string;\n    } | undefined) {\n        return e && (e.type !== ChatRecipientType.Channel || r.includes(e.name));\n    }\n    function R(e: {\n        type: ChatRecipientType;\n        name: string;\n    }) {\n        if (s?.lastComposeTarget)\n            s.lastComposeTarget.value = e;\n        T(e);\n    }\n    useEffect(() => {\n        p.current?.focus();\n    }, []);\n    useEffect(() => {\n        if (!A(y)) {\n            T({ type: ChatRecipientType.Channel, name: r[0] ?? IMPLICIT_CHANNEL_NAME });\n        }\n    }, [r]);\n    useEffect(() => {\n        if (s) {\n            const e = (e: any) => {\n                if (y !== e && A(e)) {\n                    T(e);\n                    p.current?.focus();\n                }\n            };\n            const t = () => {\n                f(M());\n            };\n            s.lastComposeTarget?.onChange.subscribe(e);\n            s.lastWhisperFrom?.onChange.subscribe(t);\n            s.lastWhisperTo?.onChange.subscribe(t);\n            return () => {\n                s.lastComposeTarget?.onChange.unsubscribe(e);\n                s.lastWhisperFrom?.onChange.unsubscribe(t);\n                s.lastWhisperTo?.onChange.unsubscribe(t);\n            };\n        }\n    }, [y, s, r]);\n    useImperativeHandle(g, () => ({\n        send() {\n            const e = p.current;\n            if (!e)\n                return;\n            const t = e.value;\n            if (t.length) {\n                d({ recipient: y, value: t });\n                e.value = '';\n                e.focus();\n                w(t);\n            }\n        },\n    }), [y]);\n    const P = (function (e: {\n        type: ChatRecipientType;\n        name: string;\n    }) {\n        if (e.type === ChatRecipientType.Channel) {\n            if (e.name === RECIPIENT_TEAM)\n                return t.get(\"TS:ToAllies\");\n            if (e.name === RECIPIENT_ALL)\n                return t.get(\"TS:ToAll\");\n            return '';\n        }\n        if (e.type === ChatRecipientType.Whisper) {\n            return t.get(\"TS:To\", e.name);\n        }\n        throw new Error(`Recipient type ${e.type} not implemented`);\n    })(y);\n    const I = !n && C && !x && (m.length > 1 || y.type === ChatRecipientType.Whisper)\n        ? t.get(\"TS:ChatCycleHint\", \"Tab\")\n        : undefined;\n    return (<div className={e}>\n      {P && <label style={{ color: a }}>{P}</label>}\n      <input type=\"text\" autoComplete=\"off\" spellCheck={false} ref={p} maxLength={128} data-r-tooltip={i} placeholder={I} style={{ color: a }} onKeyDown={(e) => {\n            if (e.key === \"Tab\")\n                e.preventDefault();\n            if (!e.repeat)\n                b(e.key);\n            l?.(e);\n        }} onKeyUp={(e) => {\n            const t = e.target as HTMLInputElement;\n            if (e.key === \"Enter\" && v === \"Enter\") {\n                const i = t.value;\n                if (i.length || o) {\n                    d({ recipient: y, value: i });\n                    if (i.length) {\n                        t.value = '';\n                        w(i);\n                    }\n                }\n            }\n            else if (e.key === \"Tab\" && v === \"Tab\") {\n                if (m.length !== 1 || m[0].name !== y.name) {\n                    let e = m.findIndex((e) => e.type === y.type && e.name === y.name);\n                    e = e === -1 ? 0 : (e + 1) % m.length;\n                    const i = m[e];\n                    O(true);\n                    R(i);\n                }\n            }\n            else if (e.key === \"ArrowUp\" && S) {\n                t.value = S;\n            }\n            else if (e.key === \"Escape\" && v !== \"Process\") {\n                u?.(t.value.length === 0);\n                t.value = '';\n            }\n            c?.(e);\n        }} onChange={(e) => {\n            const t = e.target.value;\n            const i = t.match(/^\\/(?:page|whisper|w|msg|m) ([A-Za-z0-9-_']+) /i);\n            if (i) {\n                R({ type: ChatRecipientType.Whisper, name: i[1] });\n                e.target.value = '';\n            }\n            const r = t.match(/^\\/r(eply)? /i);\n            if (r) {\n                if (s?.lastWhisperFrom.value !== undefined) {\n                    R({\n                        type: ChatRecipientType.Whisper,\n                        name: s.lastWhisperFrom.value,\n                    });\n                }\n                e.target.value = '';\n            }\n            if (!i && !r && I !== undefined) {\n                O(true);\n            }\n        }} onFocus={() => E(true)} onBlur={() => {\n            E(false);\n            h?.();\n        }}/>\n    </div>);\n});\n"
  },
  {
    "path": "src/gui/component/ColorSelect.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport classNames from 'classnames';\nimport { Select } from './Select';\nimport { Option } from './Option';\ninterface ColorSelectProps {\n    color?: string;\n    disabled?: boolean;\n    availableColors: string[];\n    onSelect?: (color: string) => void;\n    strings: {\n        get: (key: string, ...args: any[]) => string;\n    };\n}\nexport const ColorSelect: React.FC<ColorSelectProps> = ({ color, disabled, availableColors, onSelect, strings, }) => {\n    const [selectedColor, setSelectedColor] = useState(() => color);\n    useEffect(() => {\n        if (selectedColor !== color) {\n            setSelectedColor(color);\n        }\n    }, [color]);\n    return (<Select className={classNames('player-color-select', {\n            'bg-color': !!selectedColor,\n        })} tooltip={strings.get(\"STT:HostComboColor\")} initialValue={selectedColor || \"random\"} disabled={disabled} labelStyle={(value) => ({\n            backgroundColor: value !== \"random\" ? value : \"transparent\",\n        })} onSelect={(value) => {\n            const newColor = value === \"random\" ? \"\" : value;\n            setSelectedColor(newColor);\n            onSelect?.(newColor);\n        }}>\n      {(disabled ? [selectedColor] : availableColors).map((color) => (<Option key={color} value={color || \"random\"} label={color ? \"\" : strings.get(\"GUI:RandomAsSymbols\")} className={classNames({ 'bg-color': !!color })}/>))}\n    </Select>);\n};\n"
  },
  {
    "path": "src/gui/component/CountryIcon.tsx",
    "content": "import React from 'react';\nimport { RANDOM_COUNTRY_NAME, OBS_COUNTRY_NAME } from '@/game/gameopts/constants';\nimport { Image } from '@/gui/component/Image';\nconst countryIcons = new Map<string, string>()\n    .set(\"Americans\", \"usai.pcx\")\n    .set(\"French\", \"frai.pcx\")\n    .set(\"Germans\", \"geri.pcx\")\n    .set(\"British\", \"gbri.pcx\")\n    .set(\"Russians\", \"rusi.pcx\")\n    .set(\"Confederation\", \"lati.pcx\")\n    .set(\"Africans\", \"djbi.pcx\")\n    .set(\"Arabs\", \"arbi.pcx\")\n    .set(\"Alliance\", \"japi.pcx\")\n    .set(RANDOM_COUNTRY_NAME, \"rani.pcx\")\n    .set(OBS_COUNTRY_NAME, \"obsi.pcx\");\ninterface CountryIconProps {\n    country: any;\n}\nexport const CountryIcon: React.FC<CountryIconProps> = ({ country }) => {\n    const countryName = typeof country === 'string' ? country : country?.name;\n    const iconSrc = countryIcons.get(countryName);\n    return (<div className=\"player-country-icon\">\n      {iconSrc && <Image src={iconSrc}/>}\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/component/CountrySelect.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { CountryIcon } from './CountryIcon';\nimport { Select } from './Select';\nimport { Option } from './Option';\ninterface CountrySelectProps {\n    country: string;\n    availableCountries: string[];\n    onlyIcon?: boolean;\n    disabled?: boolean;\n    strings: {\n        get: (key: string, ...args: any[]) => string;\n    };\n    countryUiNames: Map<string, string>;\n    countryUiTooltips: Map<string, string>;\n    onSelect?: (country: string) => void;\n}\nexport const CountrySelect: React.FC<CountrySelectProps> = ({ country, availableCountries, onlyIcon, disabled, strings, countryUiNames, countryUiTooltips, onSelect, }) => {\n    const [selectedCountry, setSelectedCountry] = useState(() => country);\n    useEffect(() => {\n        if (selectedCountry !== country) {\n            setSelectedCountry(country);\n        }\n    }, [country]);\n    return (<div className=\"country-select\">\n      <div className=\"player-country-icon\" data-r-tooltip={strings.get(\"STT:HostPictureFlag\")}>\n        <CountryIcon country={selectedCountry}/>\n      </div>\n      {!onlyIcon && (<Select className=\"player-country-select\" tooltip={strings.get(\"STT:HostComboCountry\")} initialValue={selectedCountry} disabled={disabled} onSelect={(value) => {\n                setSelectedCountry(value);\n                onSelect?.(value);\n            }}>\n          {(disabled ? [selectedCountry] : availableCountries).map((country) => {\n                const label = strings.get(countryUiNames.get(country) || country);\n                const tooltip = countryUiTooltips.has(country)\n                    ? strings.get(countryUiTooltips.get(country)!)\n                    : undefined;\n                return (<Option key={country} value={country} label={label} tooltip={tooltip}/>);\n            })}\n        </Select>)}\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/component/Dialog.tsx",
    "content": "import React from 'react';\ninterface DialogViewport {\n    x: number;\n    y: number;\n    width: number | string;\n    height: number | string;\n}\nexport interface ButtonConfig {\n    label: string;\n    onClick?: () => void;\n}\nexport interface DialogProps {\n    children?: React.ReactNode;\n    className?: string;\n    hidden?: boolean;\n    buttons: ButtonConfig[];\n    viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    zIndex?: number;\n}\nexport class Dialog extends React.Component<DialogProps> {\n    render(): React.ReactNode {\n        if (this.props.hidden) {\n            return null;\n        }\n        const contentChildren = React.Children.toArray(this.props.children);\n        return React.createElement('div', { style: this.getWrapperStyle() }, React.createElement('div', {\n            className: 'message-box ' + (this.props.className || '')\n        }, React.createElement('div', { className: 'message-box-content' }, contentChildren), React.createElement('div', { className: 'message-box-footer' }, this.props.buttons.map((button, index) => this.renderButton(button, index)))));\n    }\n    private renderButton(button: ButtonConfig, index: number): React.ReactElement {\n        return React.createElement('button', {\n            key: index,\n            className: 'dialog-button',\n            onClick: button.onClick\n        }, button.label);\n    }\n    private getWrapperStyle(): React.CSSProperties {\n        const viewport = this.props.viewport;\n        return {\n            position: 'absolute',\n            top: viewport.y,\n            left: viewport.x,\n            width: viewport.width,\n            height: viewport.height,\n            zIndex: this.props.zIndex\n        };\n    }\n}\n"
  },
  {
    "path": "src/gui/component/GameResBoxApi.tsx",
    "content": "import React from 'react';\nimport classNames from 'classnames';\nimport { HtmlReactElement } from '../HtmlReactElement';\nimport { Dialog, type DialogProps } from './Dialog';\nimport { GameResForm, type GameResFormProps } from './GameResForm';\nimport { FileSystemUtil } from '../../engine/gameRes/FileSystemUtil';\nimport type { Viewport } from '../Viewport';\nimport type { Strings } from '../../data/Strings';\ninterface FsAccessLibraryShim {\n    polyfillDataTransferItem: () => Promise<void>;\n    showDirectoryPicker: (options?: any) => Promise<FileSystemDirectoryHandle>;\n}\nexport type GameResSourceSelection = FileSystemDirectoryHandle | FileSystemFileHandle | URL | undefined;\nexport class GameResBoxApi {\n    private viewport: Viewport;\n    private strings: Strings;\n    private rootEl: HTMLElement;\n    private fsAccessLib: FsAccessLibraryShim;\n    constructor(viewport: Viewport, strings: Strings, rootEl: HTMLElement, fsAccessLib: FsAccessLibraryShim) {\n        this.viewport = viewport;\n        this.strings = strings;\n        this.rootEl = rootEl;\n        this.fsAccessLib = fsAccessLib;\n    }\n    async promptForGameRes(defaultArchiveUrl?: string, closable?: boolean): Promise<GameResSourceSelection> {\n        console.log('[GameResBoxApi] promptForGameRes called with:', { defaultArchiveUrl, closable });\n        await this.fsAccessLib.polyfillDataTransferItem();\n        return new Promise<GameResSourceSelection>((resolve) => {\n            let dialogElement: HtmlReactElement<DialogProps> | undefined;\n            const handleResolve = (selection: GameResSourceSelection) => {\n                console.log('[GameResBoxApi] Resolving with selection:', selection);\n                cleanup();\n                resolve(selection);\n            };\n            const dialogProps: DialogProps = {\n                className: classNames(\"game-res-box\"),\n                buttons: [] as any[],\n                children: React.createElement(GameResForm, {\n                    defaultArchiveUrl: defaultArchiveUrl,\n                    closable: closable,\n                    strings: this.strings,\n                    onDrop: async (dataTransfer: DataTransfer) => {\n                        console.log('[GameResBoxApi] onDrop called');\n                        if (dataTransfer.items && dataTransfer.items.length > 0) {\n                            try {\n                                const handle = await (dataTransfer.items[0] as any).getAsFileSystemHandle();\n                                if (!handle)\n                                    return;\n                                handleResolve(handle as FileSystemDirectoryHandle | FileSystemFileHandle);\n                            }\n                            catch (e) {\n                                console.error(\"Error getting handle from drop:\", e);\n                            }\n                        }\n                    },\n                    onBrowseFolder: async () => {\n                        console.log('[GameResBoxApi] onBrowseFolder called');\n                        try {\n                            const handle = await this.fsAccessLib.showDirectoryPicker({ _preferPolyfill: true });\n                            handleResolve(handle);\n                        }\n                        catch (e) {\n                            console.error(\"Error browsing folder:\", e);\n                        }\n                    },\n                    onBrowseArchive: async () => {\n                        console.log('[GameResBoxApi] onBrowseArchive called');\n                        try {\n                            const handle = await FileSystemUtil.showArchivePicker(this.fsAccessLib as any);\n                            handleResolve(handle as FileSystemFileHandle);\n                        }\n                        catch (e) {\n                            console.error(\"Error browsing archive:\", e);\n                        }\n                    },\n                    onDownloadArchive: async (url: URL) => {\n                        console.log('[GameResBoxApi] onDownloadArchive called with:', url);\n                        handleResolve(url);\n                    },\n                    onClose: () => {\n                        console.log('[GameResBoxApi] onClose called');\n                        handleResolve(undefined);\n                    },\n                } as GameResFormProps),\n                viewport: this.viewport.value,\n                zIndex: 101,\n            };\n            console.log('[GameResBoxApi] Creating dialog element with props:', dialogProps);\n            dialogElement = HtmlReactElement.factory(Dialog, dialogProps);\n            const cleanup = () => {\n                console.log('[GameResBoxApi] Cleanup called');\n                if (dialogElement) {\n                    const element = dialogElement.getElement();\n                    if (element && this.rootEl.contains(element)) {\n                        this.rootEl.removeChild(element);\n                    }\n                    dialogElement.unrender();\n                    dialogElement = undefined;\n                }\n            };\n            if (dialogElement) {\n                console.log('[GameResBoxApi] Rendering dialog element');\n                const viewportValue = this.viewport.value;\n                dialogElement.setSize(viewportValue.width, viewportValue.height);\n                dialogElement.render();\n                const elementToAppend = dialogElement.getElement();\n                if (elementToAppend) {\n                    console.log('[GameResBoxApi] Appending dialog element to root:', elementToAppend);\n                    this.rootEl.appendChild(elementToAppend);\n                }\n                else {\n                    console.error(\"GameResBoxApi: Dialog element not created for appending.\");\n                    handleResolve(undefined);\n                }\n            }\n            else {\n                console.error(\"GameResBoxApi: Dialog could not be created.\");\n                handleResolve(undefined);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/gui/component/GameResForm.tsx",
    "content": "import React, { useState, useRef, useEffect, useCallback, DragEvent, FormEvent } from 'react';\nimport classNames from 'classnames';\nimport type { Strings } from '../../data/Strings';\nexport interface GameResFormProps {\n    closable?: boolean;\n    strings: Strings;\n    defaultArchiveUrl?: string;\n    onDownloadArchive: (url: URL) => Promise<void> | void;\n    onBrowseFolder: () => Promise<void> | void;\n    onBrowseArchive: () => Promise<void> | void;\n    onDrop: (dataTransfer: DataTransfer) => Promise<void> | void;\n    onClose?: () => void;\n}\nexport const GameResForm: React.FC<GameResFormProps> = ({ closable, strings, defaultArchiveUrl, onDownloadArchive, onBrowseFolder, onBrowseArchive, onDrop, onClose, }) => {\n    const [dragTarget, setDragTarget] = useState<EventTarget | null | undefined>(null);\n    const [archiveUrl, setArchiveUrl] = useState<string>(defaultArchiveUrl || '');\n    const urlInputRef = useRef<HTMLInputElement>(null);\n    const handleDragLeave = useCallback((event: DragEvent<HTMLDivElement>) => {\n        if (event.target === dragTarget) {\n            setDragTarget(null);\n        }\n    }, [dragTarget]);\n    useEffect(() => {\n        urlInputRef.current?.focus();\n    }, []);\n    useEffect(() => {\n        const preventDefault = (event: Event) => event.preventDefault();\n        globalThis.addEventListener(\"drop\", preventDefault);\n        globalThis.addEventListener(\"dragover\", preventDefault);\n        return () => {\n            globalThis.removeEventListener(\"drop\", preventDefault);\n            globalThis.removeEventListener(\"dragover\", preventDefault);\n        };\n    }, []);\n    const handleSubmit = (event: FormEvent<HTMLFormElement>) => {\n        event.preventDefault();\n        if (archiveUrl) {\n            try {\n                const url = new URL(archiveUrl.trim());\n                if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n                    alert(strings.get(\"ts:gameres_invalid_url\"));\n                }\n                else if (url.protocol === \"http:\" && window.location.protocol === \"https:\") {\n                    alert(strings.get(\"ts:gameres_insecure_url\"));\n                }\n                else {\n                    onDownloadArchive(url);\n                }\n            }\n            catch (e) {\n                alert(strings.get(\"ts:gameres_invalid_url\"));\n            }\n        }\n    };\n    return (<div>\n            {closable && (<div className=\"close-button\" onClick={onClose} role=\"button\" tabIndex={0} aria-label=\"Close\">\n                    \n                </div>)}\n            <div className=\"title\">{strings.get(\"ts:gameres_locate_title\")}</div>\n            <div className=\"browse-container\">\n                <p>{strings.get(\"ts:gameres_import_desc\")}</p>\n                <form className=\"link-container\" onSubmit={handleSubmit}>\n                    <p className=\"link-field\">\n                        <label htmlFor=\"archiveUrlInput\">{strings.get(\"ts:gameres_download_url\")}</label>\n                        <input id=\"archiveUrlInput\" type=\"url\" ref={urlInputRef} value={archiveUrl} onChange={(e) => setArchiveUrl(e.currentTarget.value)} placeholder=\"https://\"/>\n                    </p>\n                    <p className=\"download-button\">\n                        <button type=\"submit\" className=\"dialog-button\" disabled={!archiveUrl?.trim()}>\n                            {strings.get(\"ts:gameres_download_button\")}\n                        </button>\n                    </p>\n                </form>\n                <div className={classNames(\"drop-container\", { \"dropzone-active\": !!dragTarget })} onDragOver={(e) => e.preventDefault()} onDragEnter={(e: DragEvent<HTMLDivElement>) => {\n            if (Array.from(e.dataTransfer.items).every(item => item.kind === 'file')) {\n                setDragTarget(e.target);\n            }\n        }} onDragLeave={handleDragLeave} onDrop={(e: DragEvent<HTMLDivElement>) => {\n            e.preventDefault();\n            setDragTarget(null);\n            if (Array.from(e.dataTransfer.items).every(item => item.kind === 'file')) {\n                onDrop(e.dataTransfer);\n            }\n        }}>\n                    <p className=\"drop-figures\">\n                        <img src=\"res/img/drag-archive.png\" width=\"98\" height=\"133\" alt=\"Archive File Example\"/>\n                        {strings.get(\"ts:gameres_or\")}\n                        <img src=\"res/img/drag-folder.png\" width=\"99\" height=\"153\" alt=\"Folder Example\"/>\n                    </p>\n                    <p className=\"desc\">\n                        {strings.get(\"ts:gameres_drop_desc\")}\n                        <br />\n                        {strings.get(\"ts:gameres_or\")}\n                    </p>\n                    <p className=\"browse-buttons\">\n                        <button type=\"button\" className=\"dialog-button\" onClick={onBrowseFolder}>\n                            {strings.get(\"ts:gameres_browse_folder\")}\n                        </button>\n                        <button type=\"button\" className=\"dialog-button\" onClick={onBrowseArchive}>\n                            {strings.get(\"ts:gameres_browse_archive\")}\n                        </button>\n                    </p>\n                    <p className=\"archive-formats\">\n                        <em>{strings.get(\"ts:gameres_supported_archive_formats\")}</em>\n                    </p>\n                </div>\n            </div>\n        </div>);\n};\n"
  },
  {
    "path": "src/gui/component/GameResourcesViewer.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { Engine } from '../../engine/Engine';\nimport { browserFileSystemAccess } from '../../engine/gameRes/browserFileSystemAccess';\nimport { StorageFileExplorer } from './fileExplorer/StorageFileExplorer';\nimport AppLogger from '../../util/logger';\n\nconst GameResourcesViewer: React.FC = () => {\n    const [error, setError] = useState<string | null>(null);\n    const [message, setMessage] = useState<string | null>(null);\n    const [storageDirHandle, setStorageDirHandle] = useState<FileSystemDirectoryHandle | null>(null);\n    const [fileSystemChanged, setFileSystemChanged] = useState(false);\n    const [showExplorer, setShowExplorer] = useState(false);\n\n    useEffect(() => {\n        try {\n            if (Engine.rfs) {\n                const rootDirHandle = Engine.rfs.getRootDirectoryHandle();\n                if (rootDirHandle) {\n                    setStorageDirHandle(rootDirHandle);\n                    AppLogger.info('[GameResourcesViewer] Storage directory handle obtained from Engine.rfs');\n                    return;\n                }\n                AppLogger.warn('[GameResourcesViewer] Engine.rfs.getRootDirectoryHandle() returned null');\n                setError('No storage directory handle available');\n                return;\n            }\n            AppLogger.warn('[GameResourcesViewer] Engine.rfs not available');\n            setError('Real File System (RFS) not initialized');\n        }\n        catch (loadError: any) {\n            AppLogger.error('[GameResourcesViewer] Error getting storage directory handle:', loadError);\n            setError(`Failed to get storage handle: ${loadError.message}`);\n        }\n    }, []);\n\n    const isSystemFile = (path: string): boolean => {\n        const systemPatterns: (string | RegExp)[] = [\n            /^\\/[^\\/]*\\.mix$/i,\n            /^\\/[^\\/]*\\.bag$/i,\n            /^\\/[^\\/]*\\.idx$/i,\n            /^\\/[^\\/]*\\.ini$/i,\n            /^\\/[^\\/]*\\.csf$/i,\n        ];\n        return systemPatterns.some(pattern => typeof pattern === 'string'\n            ? path.toLowerCase() === pattern.toLowerCase()\n            : pattern.test(path));\n    };\n\n    const getSystemStatus = () => {\n        const vfsStatus = Engine.vfs ? '✅ 已初始化' : '❌ 未初始化';\n        const rfsStatus = Engine.rfs ? '✅ 已初始化' : '❌ 未初始化';\n        const vfsArchiveCount = Engine.vfs ? Engine.vfs.listArchives().length : 0;\n        const storageReady = !!storageDirHandle;\n        const fsAccessReady = !!browserFileSystemAccess.adapters.indexeddb;\n        return { vfsStatus, rfsStatus, vfsArchiveCount, storageReady, fsAccessReady };\n    };\n\n    const { vfsStatus, rfsStatus, vfsArchiveCount, storageReady, fsAccessReady } = getSystemStatus();\n\n    return (\n        <div style={{\n            height: '100vh',\n            overflow: 'auto',\n            padding: '20px',\n            fontFamily: 'Arial, sans-serif',\n            boxSizing: 'border-box'\n        }}>\n            <h1>RA2 Web - 游戏资源存储浏览器</h1>\n\n            <div style={{ marginBottom: '20px' }}>\n                <h2>系统状态</h2>\n                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '10px' }}>\n                    <div style={{ padding: '10px', border: '1px solid #ccc', borderRadius: '5px' }}>\n                        <strong>虚拟文件系统 (VFS)</strong>\n                        <div>状态: {vfsStatus}</div>\n                        <div>归档数量: {vfsArchiveCount}</div>\n                    </div>\n                    <div style={{ padding: '10px', border: '1px solid #ccc', borderRadius: '5px' }}>\n                        <strong>真实文件系统 (RFS)</strong>\n                        <div>状态: {rfsStatus}</div>\n                        <div>存储句柄: {storageReady ? '✅ 就绪' : '❌ 未就绪'}</div>\n                    </div>\n                    <div style={{ padding: '10px', border: '1px solid #ccc', borderRadius: '5px' }}>\n                        <strong>ESM 模块</strong>\n                        <div>FileSystemAccess: {fsAccessReady ? '✅ 已接入' : '❌ 不可用'}</div>\n                        <div>File Explorer: ✅ TypeScript 组件</div>\n                    </div>\n                </div>\n            </div>\n\n            <div style={{ marginBottom: '20px' }}>\n                <h2>存储浏览器控制</h2>\n                <div style={{ marginBottom: '10px' }}>\n                    <button\n                        onClick={() => {\n                            setError(null);\n                            setShowExplorer(true);\n                        }}\n                        disabled={!storageReady}\n                        style={{\n                            marginRight: '10px',\n                            padding: '10px 20px',\n                            backgroundColor: storageReady ? '#007cba' : '#ccc',\n                            color: 'white',\n                            border: '1px solid #ccc',\n                            borderRadius: '5px',\n                            cursor: storageReady ? 'pointer' : 'not-allowed'\n                        }}\n                    >\n                        打开存储浏览器\n                    </button>\n                    <button\n                        onClick={() => location.reload()}\n                        disabled={!fileSystemChanged}\n                        style={{\n                            padding: '10px 20px',\n                            backgroundColor: fileSystemChanged ? '#dc3545' : '#ccc',\n                            color: 'white',\n                            border: '1px solid #ccc',\n                            borderRadius: '5px',\n                            cursor: fileSystemChanged ? 'pointer' : 'not-allowed'\n                        }}\n                    >\n                        {fileSystemChanged ? '退出并重新加载' : '重新加载（未修改）'}\n                    </button>\n                </div>\n            </div>\n\n            {error ? (\n                <div style={{\n                    padding: '10px',\n                    backgroundColor: '#ffebee',\n                    color: '#c62828',\n                    borderRadius: '5px',\n                    marginBottom: '10px',\n                    border: '1px solid #ffcdd2'\n                }}>\n                    错误: {error}\n                </div>\n            ) : null}\n\n            {message ? (\n                <div style={{\n                    padding: '10px',\n                    backgroundColor: '#e8f5e8',\n                    color: '#2e7d32',\n                    borderRadius: '5px',\n                    marginBottom: '10px',\n                    border: '1px solid #c8e6c9'\n                }}>\n                    {message}\n                </div>\n            ) : null}\n\n            {fileSystemChanged ? (\n                <div style={{\n                    padding: '10px',\n                    backgroundColor: '#fff3cd',\n                    color: '#856404',\n                    borderRadius: '5px',\n                    marginBottom: '10px',\n                    border: '1px solid #ffeaa7'\n                }}>\n                    ⚠️ 文件系统已修改。建议重新加载应用以确保更改生效。\n                </div>\n            ) : null}\n\n            <div style={{\n                marginTop: '20px',\n                border: '2px solid #ccc',\n                borderRadius: '5px',\n                minHeight: '500px',\n                backgroundColor: '#f9f9f9',\n                overflow: 'hidden'\n            }}>\n                {!showExplorer ? (\n                    <div style={{ padding: '20px', textAlign: 'center' }}>\n                        <p>点击“打开存储浏览器”开始浏览游戏资源文件。</p>\n                        <p>这里显示的是浏览器存储中持久化的游戏文件和目录。</p>\n                    </div>\n                ) : storageDirHandle ? (\n                    <StorageFileExplorer\n                        rootHandle={storageDirHandle}\n                        rootLabel=\"Game Storage\"\n                        isSystemFile={isSystemFile}\n                        onFileSystemChange={() => setFileSystemChanged(true)}\n                        onFileOpen={(path, entry) => setMessage(`打开文件: ${entry.name} (路径: ${path})`)}\n                        onInfo={(info) => setMessage(info)}\n                        promptForText={async (promptText) => {\n                            const value = window.prompt(promptText);\n                            return value === null ? undefined : value;\n                        }}\n                        confirmAction={async (confirmText) => window.confirm(confirmText)}\n                        showAlert={async (alertText, title) => window.alert(title ? `${title}\\n\\n${alertText}` : alertText)}\n                    />\n                ) : (\n                    <div style={{ padding: '20px', textAlign: 'center' }}>\n                        <p>等待存储系统就绪...</p>\n                        <p>请确保游戏资源已导入且 RFS 系统正常初始化。</p>\n                    </div>\n                )}\n            </div>\n\n            <div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f0f0f0', borderRadius: '5px' }}>\n                <h3>使用说明</h3>\n                <ul>\n                    <li><strong>存储浏览器</strong>: 浏览浏览器存储中的游戏资源文件</li>\n                    <li><strong>系统文件</strong>: .mix、.bag、.ini 等核心游戏文件受保护，删除前会警告</li>\n                    <li><strong>文件操作</strong>: 支持上传、删除、新建文件夹等操作</li>\n                    <li><strong>调试工具</strong>: 此组件用于调试 mix 文件读取问题和资源管理</li>\n                    <li><strong>ESM 迁移</strong>: 浏览器不再依赖 public 下的旧版 file-explorer.js</li>\n                </ul>\n            </div>\n        </div>\n    );\n};\n\nexport default GameResourcesViewer;\n"
  },
  {
    "path": "src/gui/component/Image.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { Palette } from '../../data/Palette';\nimport { PcxFile } from '../../data/PcxFile';\nimport { ShpFile } from '../../data/ShpFile';\nimport { ImageUtils } from '../../engine/gfx/ImageUtils';\nimport { ImageContext, ImageContextClass } from './ImageContext';\ninterface ImageProps {\n    src: string;\n    palette?: string;\n}\nexport const Image: React.FC<ImageProps> = (props) => {\n    const context = ImageContext;\n    const [imageUrl, setImageUrl] = useState(\"\");\n    useEffect(() => {\n        let url: string;\n        let cleanup: (() => void) | undefined;\n        if (ImageContextClass.imageUrlCache.has(props.src)) {\n            url = ImageContextClass.imageUrlCache.get(props.src)!;\n        }\n        else if (context.vfs?.fileExists(props.src)) {\n            const extension = props.src.split(\".\").pop();\n            if (extension === \"shp\") {\n                const palette = props.palette;\n                if (palette && context.vfs.fileExists(palette)) {\n                    const shpFile = new ShpFile(context.vfs.openFile(props.src));\n                    const paletteFile = new Palette(context.vfs.openFile(palette));\n                    url = ImageUtils.convertShpToCanvas(shpFile, paletteFile).toDataURL();\n                }\n                else {\n                    console.warn(`Palette \"${palette}\" not found in VFS\"`);\n                    url = \"\";\n                }\n            }\n            else if (extension === \"pcx\") {\n                const pcxFile = new PcxFile(context.vfs.openFile(props.src));\n                url = pcxFile.toDataUrl();\n            }\n            else if (extension === \"png\") {\n                const stream = context.vfs.openFile(props.src).stream;\n                const blob = new Blob([new Uint8Array(stream.buffer, stream.byteOffset, stream.byteLength)], { type: \"image/png\" });\n                url = URL.createObjectURL(blob);\n                cleanup = () => {\n                    URL.revokeObjectURL(url);\n                    ImageContextClass.imageUrlCache.delete(props.src);\n                };\n            }\n            else {\n                console.warn(`Unknown image format \"${extension}\"`);\n                url = \"\";\n            }\n            ImageContextClass.imageUrlCache.set(props.src, url);\n        }\n        else {\n            url = context.cdnBaseUrl\n                ? context.cdnBaseUrl + props.src.substring(0, props.src.lastIndexOf(\".\")) + \".png\"\n                : (console.warn(`Image \"${props.src}\" not found in VFS`), \"\");\n        }\n        setImageUrl(url);\n        return cleanup;\n    }, [props.src]);\n    return imageUrl ? <img src={imageUrl}/> : null;\n};\n"
  },
  {
    "path": "src/gui/component/ImageContext.tsx",
    "content": "export class ImageContextClass {\n    static imageUrlCache = new Map<string, string>();\n    vfs?: any;\n    cdnBaseUrl?: string;\n}\nexport const ImageContext = new ImageContextClass();\n"
  },
  {
    "path": "src/gui/component/List.tsx",
    "content": "import React, { ReactNode, Ref } from \"react\";\nimport classNames from \"classnames\";\ninterface ListProps {\n    children?: ReactNode;\n    className?: string;\n    title?: ReactNode;\n    innerRef?: Ref<HTMLDivElement>;\n    tooltip?: string;\n}\nexport const List: React.FC<ListProps> = ({ children, className, title, innerRef, tooltip, }) => (<>\n    {title && <div className=\"list-title\">{title}</div>}\n    <div ref={innerRef} className={classNames(\"list\", className)} data-r-tooltip={tooltip}>\n      {children}\n    </div>\n  </>);\ninterface ListItemProps extends React.HTMLAttributes<HTMLDivElement> {\n    children?: ReactNode;\n    selected?: boolean;\n    disabled?: boolean;\n    tooltip?: string;\n    className?: string;\n    innerRef?: Ref<HTMLDivElement>;\n}\nexport const ListItem: React.FC<ListItemProps> = ({ children, selected, disabled, tooltip, className, innerRef, ...rest }) => (<div ref={innerRef} className={classNames(\"list-item\", { selected, disabled }, className)} data-r-tooltip={tooltip} {...rest}>\n    {children}\n  </div>);\ninterface ListHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n    children?: ReactNode;\n    tooltip?: string;\n    className?: string;\n    innerRef?: Ref<HTMLDivElement>;\n}\nexport const ListHeader: React.FC<ListHeaderProps> = ({ children, tooltip, className, innerRef, ...rest }) => (<div ref={innerRef} className={classNames(\"list-header\", className)} data-r-tooltip={tooltip} {...rest}>\n    {children}\n  </div>);\n"
  },
  {
    "path": "src/gui/component/MenuButton.tsx",
    "content": "import React from \"react\";\ninterface ButtonConfig {\n    label: string;\n    disabled?: boolean;\n    tooltip?: string;\n}\ninterface Box {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface MenuButtonProps {\n    buttonConfig: ButtonConfig;\n    box: Box;\n    onMouseDown?: (event: React.MouseEvent) => void;\n    onMouseUp?: (event: React.MouseEvent) => void;\n    onClick?: (event: React.MouseEvent) => void;\n}\nexport class MenuButton extends React.Component<MenuButtonProps> {\n    render() {\n        const { buttonConfig } = this.props;\n        if (!buttonConfig) {\n            return null;\n        }\n        return React.createElement(\"div\", {\n            className: this.getClassName(buttonConfig),\n            style: this.getStyle(),\n            onMouseDown: (event) => this.onMouseDown(event),\n            onMouseUp: (event) => this.onMouseUp(event),\n            onClick: (event) => this.onClick(event),\n            \"data-r-tooltip\": buttonConfig.tooltip,\n        }, buttonConfig.label);\n    }\n    getClassName(buttonConfig: ButtonConfig): string {\n        let classes = [\"menu-button\"];\n        if (buttonConfig.disabled) {\n            classes.push(\"disabled\");\n        }\n        return classes.join(\" \");\n    }\n    getStyle(): React.CSSProperties {\n        const { box } = this.props;\n        return {\n            position: \"absolute\",\n            left: box.x,\n            top: box.y,\n            width: box.width,\n            height: box.height,\n            lineHeight: box.height + 1 + \"px\",\n        };\n    }\n    onMouseDown(event: React.MouseEvent): void {\n        if (!this.props.buttonConfig.disabled && this.props.onMouseDown) {\n            this.props.onMouseDown(event);\n        }\n    }\n    onMouseUp(event: React.MouseEvent): void {\n        if (!this.props.buttonConfig.disabled && this.props.onMouseUp) {\n            this.props.onMouseUp(event);\n        }\n    }\n    onClick(event: React.MouseEvent): void {\n        if (!this.props.buttonConfig.disabled && this.props.onClick) {\n            this.props.onClick(event);\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/component/MenuVideo.tsx",
    "content": "import React, { useEffect, useRef } from 'react';\ninterface MenuVideoProps {\n    src?: string | File;\n    className?: string;\n}\nconst MenuVideo: React.FC<MenuVideoProps> = ({ src, className = 'video-wrapper' }) => {\n    const videoRef = useRef<HTMLVideoElement>(null);\n    const logoRef = useRef<HTMLDivElement>(null);\n    useEffect(() => {\n        if (!src)\n            return;\n        const video = videoRef.current;\n        const logo = logoRef.current;\n        if (!video || !logo)\n            return;\n        let videoUrl: string;\n        let cleanup: (() => void) | undefined;\n        if (typeof src === 'string') {\n            videoUrl = src;\n        }\n        else {\n            videoUrl = URL.createObjectURL(src);\n            cleanup = () => URL.revokeObjectURL(videoUrl);\n        }\n        const source = video.querySelector('source');\n        if (source) {\n            source.src = videoUrl;\n            const extension = videoUrl.split('.').pop()?.toLowerCase();\n            source.type = extension === 'mp4' ? 'video/mp4' : 'video/webm';\n        }\n        const handleLoadedData = () => {\n            logo.style.opacity = '1';\n        };\n        video.addEventListener('loadeddata', handleLoadedData);\n        video.load();\n        return () => {\n            video.removeEventListener('loadeddata', handleLoadedData);\n            cleanup?.();\n        };\n    }, [src]);\n    if (!src) {\n        return (<div className={className}>\n        <div style={{\n                width: '100%',\n                height: '100%',\n                backgroundColor: '#000',\n                display: 'flex',\n                alignItems: 'center',\n                justifyContent: 'center',\n                color: '#ffff00',\n                fontSize: '24px',\n                fontWeight: 'bold'\n            }}>\n          RED ALERT 2 WEB\n        </div>\n        \n      </div>);\n    }\n    return (<div className={className}>\n      <video ref={videoRef} style={{ outline: 'none', width: '100%', height: '100%' }} loop playsInline muted autoPlay>\n        <source src=\"\" type=\"video/webm\"/>\n      </video>\n      \n    </div>);\n};\nexport default MenuVideo;\n"
  },
  {
    "path": "src/gui/component/MessageBoxApi.tsx",
    "content": "import React from 'react';\nimport { jsx } from '../jsx/jsx';\nimport { HtmlView } from '../jsx/HtmlView';\nimport { CompositeDisposable } from '../../util/disposable/CompositeDisposable';\nimport { Dialog } from './Dialog';\nimport { PromptDialog } from './PromptDialog';\nexport interface ButtonConfig {\n    label: string;\n    onClick?: () => void;\n}\nexport interface MessageBoxApiProps {\n    viewport: any;\n    uiScene: any;\n    jsxRenderer: any;\n}\nexport class MessageBoxApi {\n    private viewport: any;\n    private uiScene: any;\n    private jsxRenderer: any;\n    private disposables: CompositeDisposable;\n    private component: any;\n    constructor(viewport: any, uiScene: any, jsxRenderer: any) {\n        this.viewport = viewport;\n        this.uiScene = uiScene;\n        this.jsxRenderer = jsxRenderer;\n        this.disposables = new CompositeDisposable();\n    }\n    show(message: string | React.ReactNode, buttons: string | ButtonConfig[], callback?: (() => void) | {\n        className?: string;\n    }) {\n        this.destroy();\n        const options = typeof callback === 'function' ? undefined : callback;\n        let [element] = this.jsxRenderer.render(jsx(HtmlView, {\n            innerRef: (ref: any) => (this.component = ref),\n            component: Dialog,\n            props: {\n                children: typeof message === 'string' ? this.splitNewLines(message) : message,\n                className: options?.className,\n                viewport: this.viewport.value || this.viewport,\n                zIndex: 101,\n                buttons: typeof buttons === 'string'\n                    ? [{\n                            label: buttons,\n                            onClick: () => {\n                                this.disposables.dispose();\n                                typeof callback === 'function' && callback();\n                            }\n                        }]\n                    : (buttons ?? []).map(btn => ({\n                        label: btn.label,\n                        onClick: () => {\n                            this.disposables.dispose();\n                            btn.onClick?.();\n                        }\n                    }))\n            }\n        }));\n        this.uiScene.add(element);\n        this.disposables.add(element, () => this.uiScene.remove(element), () => (this.component = undefined));\n    }\n    splitNewLines(text: string): React.ReactNode[] {\n        return text.split(/\\n/g).map((line, index) => index ? (<React.Fragment key={index}>\n          <br />\n          <span>{line}</span>\n        </React.Fragment>) : (<span key={index}>{line}</span>));\n    }\n    async confirm(message: string, confirmLabel: string, cancelLabel: string): Promise<boolean> {\n        return new Promise((resolve) => {\n            this.show(message, [\n                { label: confirmLabel, onClick: () => resolve(true) },\n                { label: cancelLabel, onClick: () => resolve(false) }\n            ]);\n        });\n    }\n    async alert(message: string, buttonLabel: string): Promise<void> {\n        return new Promise((resolve) => this.show(message, buttonLabel, resolve));\n    }\n    async prompt(promptText: string, submitLabel: string, cancelLabel: string, inputProps?: any): Promise<string | undefined> {\n        this.destroy();\n        return new Promise((resolve) => {\n            let [element] = this.jsxRenderer.render(jsx(HtmlView, {\n                innerRef: (ref: any) => (this.component = ref),\n                component: PromptDialog,\n                props: {\n                    promptText,\n                    submitLabel,\n                    cancelLabel,\n                    inputProps,\n                    onSubmit: (value: string) => {\n                        resolve(value);\n                        element.destroy();\n                    },\n                    onDismiss: () => {\n                        resolve(undefined);\n                        element.destroy();\n                    },\n                    viewport: this.viewport.value || this.viewport\n                }\n            }));\n            this.uiScene.add(element);\n            this.disposables.add(element, () => this.uiScene.remove(element), () => (this.component = undefined));\n        });\n    }\n    updateViewport(viewport: any): void {\n        this.viewport = viewport;\n        this.component?.applyOptions((props: any) => (props.viewport = viewport.value || viewport));\n    }\n    updateText(text: string | React.ReactNode): void {\n        this.component?.applyOptions((props: any) => {\n            if (props.promptText) {\n                props.promptText = text;\n            }\n            else {\n                props.children = typeof text === 'string' ? this.splitNewLines(text) : text;\n            }\n        });\n    }\n    destroy(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/component/Option.tsx",
    "content": "import React from 'react';\nimport classNames from 'classnames';\ninterface OptionProps {\n    selected?: boolean;\n    disabled?: boolean;\n    value?: string | number;\n    label: string;\n    style?: React.CSSProperties;\n    labelStyle?: React.CSSProperties;\n    className?: string;\n    tooltip?: string;\n    onClick?: () => void;\n}\nexport const Option: React.FC<OptionProps> = ({ selected, disabled, value, label, style, labelStyle, className, tooltip, onClick, }) => (<div className={classNames('option', { selected, disabled }, className)} style={style} onClick={disabled ? undefined : onClick} data-r-tooltip={tooltip}>\n    <div style={labelStyle}>{label}</div>\n  </div>);\n"
  },
  {
    "path": "src/gui/component/PingIndicator.tsx",
    "content": "import React from 'react';\nimport { Image } from './Image';\nenum PingQuality {\n    Good = 1,\n    Average = 2,\n    Bad = 3\n}\ninterface PingIndicatorProps {\n    ping?: number;\n    strings: {\n        get: (key: string, ...args: any[]) => string;\n    };\n}\nconst getPingQuality = (ping: number): PingQuality => {\n    if (ping <= 100)\n        return PingQuality.Good;\n    if (ping <= 250)\n        return PingQuality.Average;\n    return PingQuality.Bad;\n};\nconst pingImageMap = new Map<PingQuality, string>()\n    .set(PingQuality.Bad, \"pingr\")\n    .set(PingQuality.Average, \"pingy\")\n    .set(PingQuality.Good, \"pingg\");\nexport const PingIndicator: React.FC<PingIndicatorProps> = ({ ping, strings }) => {\n    const tooltip = ping !== undefined ? strings.get(\"Msg:PingInfo\", ping) : undefined;\n    return (<div className=\"ping-indicator\" data-r-tooltip={tooltip} title={tooltip}>\n      {ping !== undefined && (<Image src={pingImageMap.get(getPingQuality(ping)) + \".pcx\"}/>)}\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/component/PromptDialog.tsx",
    "content": "import React, { useRef, useState, useEffect } from 'react';\nimport { Dialog } from './Dialog';\nexport interface PromptDialogProps {\n    viewport: any;\n    promptText: string;\n    submitLabel: string;\n    cancelLabel: string;\n    inputProps?: any;\n    onSubmit: (value: string) => void;\n    onDismiss?: () => void;\n}\nexport const PromptDialog: React.FC<PromptDialogProps> = ({ viewport, promptText, submitLabel, cancelLabel, inputProps, onSubmit, onDismiss, }) => {\n    const inputRef = useRef<HTMLInputElement>(null);\n    const [hidden, setHidden] = useState(false);\n    useEffect(() => {\n        setTimeout(() => {\n            inputRef.current?.focus();\n        }, 50);\n    }, []);\n    const handleSubmit = (e?: React.FormEvent) => {\n        e?.preventDefault();\n        setHidden(true);\n        onSubmit(inputRef.current?.value || '');\n    };\n    return (<Dialog className=\"prompt-box\" hidden={hidden} viewport={viewport} zIndex={100} buttons={[\n            { label: submitLabel, onClick: handleSubmit },\n            {\n                label: cancelLabel,\n                onClick: () => {\n                    setHidden(true);\n                    onDismiss?.();\n                },\n            },\n        ]}>\n      <form onSubmit={handleSubmit} autoComplete=\"off\">\n        <div className=\"field\">\n          <label>\n            {promptText.split(/\\r?\\n/).map((line, index) => (<React.Fragment key={index}>\n                {index ? <br /> : null}\n                {line}\n              </React.Fragment>))}\n          </label>\n          <input name=\"promptvalue\" type=\"text\" autoComplete=\"off\" data-lpignore=\"true\" ref={inputRef} {...inputProps}/>\n        </div>\n        <button type=\"submit\" style={{ visibility: 'hidden' }}/>\n      </form>\n    </Dialog>);\n};\n"
  },
  {
    "path": "src/gui/component/Select.tsx",
    "content": "import React, { useState, useEffect, useRef } from 'react';\nimport classNames from 'classnames';\nimport { contains } from '@/util/dom';\ninterface SelectProps {\n    initialValue: any;\n    disabled?: boolean;\n    tooltip?: string;\n    className?: string;\n    onSelect?: (value: any) => void;\n    labelStyle?: (value: any) => React.CSSProperties;\n    children?: React.ReactNode;\n}\nexport const Select: React.FC<SelectProps> = ({ initialValue, disabled, tooltip, className, onSelect, labelStyle, children, }) => {\n    const [value, setValue] = useState(() => initialValue);\n    const [hoverValue, setHoverValue] = useState(() => initialValue);\n    const [isOpen, setIsOpen] = useState(false);\n    const containerRef = useRef<HTMLDivElement>(null);\n    useEffect(() => {\n        if (value !== initialValue) {\n            setValue(initialValue);\n            setHoverValue(initialValue);\n        }\n    }, [initialValue]);\n    useEffect(() => {\n        if (isOpen) {\n            setHoverValue(value);\n            const handleClickOutside = (e: MouseEvent) => {\n                const target = e.target instanceof Element ? e.target : null;\n                if (containerRef.current && !contains(containerRef.current, target)) {\n                    setIsOpen(false);\n                }\n            };\n            document.addEventListener('click', handleClickOutside);\n            return () => {\n                document.removeEventListener('click', handleClickOutside);\n            };\n        }\n    }, [isOpen]);\n    const selectedChild = React.Children.toArray(children).find((child) => React.isValidElement(child) && (child.props as any).value === value);\n    return (<div style={{ display: 'inline-block', verticalAlign: 'middle' }} className={className}>\n      <div className={classNames('select', { disabled })} data-r-tooltip={tooltip} ref={containerRef}>\n        <div className=\"select-value\" onClick={() => !disabled && setIsOpen(!isOpen)}>\n          <div style={labelStyle?.(value)}>\n            {React.isValidElement(selectedChild) ? (selectedChild.props as any).label : ''}\n          </div>\n        </div>\n        {isOpen && (<div className=\"select-layer\">\n            {React.Children.map(children, (child) => {\n                if (!React.isValidElement(child))\n                    return null;\n                const optionChild = child as React.ReactElement<any>;\n                const childValue = optionChild.props.value;\n                const isDisabled = optionChild.props.disabled;\n                return (<div onMouseEnter={() => !isDisabled && setHoverValue(childValue)}>\n                  {React.cloneElement(optionChild, {\n                        selected: childValue === hoverValue,\n                        labelStyle: labelStyle?.(childValue),\n                        onClick: () => {\n                            setValue(childValue);\n                            setHoverValue(childValue);\n                            onSelect?.(childValue);\n                            setIsOpen(false);\n                        },\n                    })}\n                </div>);\n            })}\n          </div>)}\n      </div>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/component/Slider.tsx",
    "content": "import React, { useState, useEffect } from 'react';\ninterface SliderProps extends React.InputHTMLAttributes<HTMLInputElement> {\n    getLabel?: (value: string | number) => string | number;\n}\nexport const Slider: React.FC<SliderProps> = ({ getLabel, ...props }) => {\n    const [value, setValue] = useState(() => props.value);\n    const normalizedValue = Array.isArray(value) ? value[0] ?? '' : value ?? '';\n    useEffect(() => {\n        if (value !== props.value) {\n            setValue(props.value);\n        }\n    }, [props.value]);\n    return (<div style={{ display: 'inline-block', verticalAlign: 'middle' }}>\n      <input type=\"range\" {...props} value={normalizedValue} onChange={(e) => {\n            setValue(e.target.value);\n            props.onChange?.(e);\n        }}/>\n      <input type=\"text\" disabled={true} readOnly={true} value={getLabel?.(normalizedValue) ?? normalizedValue}/>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/component/SplashScreen.tsx",
    "content": "import React, { useEffect, useRef, useState, MutableRefObject } from 'react';\nexport interface SplashScreenProps {\n    width: number;\n    height: number;\n    parentElement: HTMLElement | null;\n    backgroundImage?: string;\n    loadingText?: string;\n    copyrightText?: string;\n    disclaimerText?: string;\n    onRender?: () => void;\n}\nconst SplashScreen: React.FC<SplashScreenProps> = ({ width, height, parentElement, backgroundImage, loadingText, copyrightText, disclaimerText, onRender, }) => {\n    const [rendered, setRendered] = useState(false);\n    const elRef = useRef<HTMLDivElement | null>(null) as MutableRefObject<HTMLDivElement | null>;\n    const loadingElRef = useRef<HTMLDivElement | null>(null) as MutableRefObject<HTMLDivElement | null>;\n    const copyrightElRef = useRef<HTMLDivElement | null>(null) as MutableRefObject<HTMLDivElement | null>;\n    const disclaimerElRef = useRef<HTMLDivElement | null>(null) as MutableRefObject<HTMLDivElement | null>;\n    useEffect(() => {\n        if (parentElement && !rendered) {\n            const div = document.createElement('div');\n            elRef.current = div;\n            div.style.backgroundColor = 'black';\n            div.style.color = 'white';\n            div.style.padding = '10px';\n            div.style.boxSizing = 'border-box';\n            div.style.backgroundRepeat = 'no-repeat';\n            div.style.backgroundPosition = '50% 50%';\n            div.style.textShadow = '1px 1px black';\n            div.style.position = 'relative';\n            const loadingDiv = document.createElement('div');\n            loadingElRef.current = loadingDiv;\n            div.appendChild(loadingDiv);\n            const copyrightDiv = document.createElement('div');\n            copyrightDiv.style.position = 'absolute';\n            copyrightDiv.style.bottom = '10px';\n            copyrightDiv.style.right = '10px';\n            copyrightDiv.style.textAlign = 'right';\n            copyrightElRef.current = copyrightDiv;\n            div.appendChild(copyrightDiv);\n            const disclaimerDiv = document.createElement('div');\n            disclaimerDiv.style.position = 'absolute';\n            disclaimerDiv.style.bottom = '10px';\n            disclaimerDiv.style.left = '10px';\n            disclaimerElRef.current = disclaimerDiv;\n            div.appendChild(disclaimerDiv);\n            parentElement.appendChild(div);\n            setRendered(true);\n            if (onRender) {\n                onRender();\n            }\n        }\n    }, [parentElement, rendered, onRender]);\n    useEffect(() => {\n        if (elRef.current) {\n            elRef.current.style.width = `${width}px`;\n            elRef.current.style.height = `${height}px`;\n        }\n    }, [width, height]);\n    useEffect(() => {\n        if (elRef.current) {\n            if (backgroundImage === \"\") {\n                elRef.current.style.backgroundImage = 'none';\n            }\n            else if (backgroundImage) {\n                elRef.current.style.backgroundImage = `url(${backgroundImage})`;\n            }\n        }\n    }, [backgroundImage]);\n    useEffect(() => {\n        if (loadingElRef.current && loadingText !== undefined) {\n            console.log('[SplashScreen] Setting loadingText to:', loadingText);\n            loadingElRef.current.innerHTML = loadingText;\n        }\n    }, [loadingText]);\n    useEffect(() => {\n        if (copyrightElRef.current && copyrightText !== undefined) {\n            copyrightElRef.current.innerHTML = copyrightText.replace(/\\n/g, '<br />');\n        }\n    }, [copyrightText]);\n    useEffect(() => {\n        if (disclaimerElRef.current && disclaimerText !== undefined) {\n            disclaimerElRef.current.innerHTML = disclaimerText.replace(/\\n/g, '<br />');\n        }\n    }, [disclaimerText]);\n    useEffect(() => {\n        return () => {\n            if (elRef.current && elRef.current.parentElement) {\n                elRef.current.parentElement.removeChild(elRef.current);\n            }\n            setRendered(false);\n        };\n    }, []);\n    return null;\n};\nexport default SplashScreen;\n"
  },
  {
    "path": "src/gui/component/StartPosSelect.tsx",
    "content": "import React from 'react';\nimport { Select } from './Select';\nimport { RANDOM_START_POS } from '@/game/gameopts/constants';\nimport { Option } from './Option';\ninterface StartPosSelectProps {\n    startPos: number;\n    disabled?: boolean;\n    availableStartPositions: number[];\n    onSelect?: (pos: number) => void;\n    strings: {\n        get: (key: string, ...args: any[]) => string;\n    };\n}\nexport const StartPosSelect: React.FC<StartPosSelectProps> = ({ startPos, disabled, availableStartPositions, onSelect, strings, }) => {\n    const positions = [...new Set([startPos, ...availableStartPositions]).values()].sort();\n    return (<Select className=\"player-start-pos-select\" initialValue={String(startPos)} disabled={disabled} tooltip={strings.get(\"STT:HostComboStart\")} onSelect={(value) => {\n            onSelect?.(Number(value));\n        }}>\n      {positions.map((pos) => (<Option key={pos} value={String(pos)} label={pos === RANDOM_START_POS\n                ? strings.get(\"GUI:RandomAsSymbols\")\n                : String(pos + 1)}/>))}\n    </Select>);\n};\n"
  },
  {
    "path": "src/gui/component/TeamSelect.tsx",
    "content": "import React from 'react';\nimport { Select } from './Select';\nimport { Option } from './Option';\nimport { NO_TEAM_ID, OBS_TEAM_ID } from '@/game/gameopts/constants';\nexport const formatTeamId = (id: number): string => String.fromCharCode('A'.charCodeAt(0) + id);\ninterface TeamSelectProps {\n    teamId: number;\n    required?: boolean;\n    disabled?: boolean;\n    maxTeams: number;\n    showObserver?: boolean;\n    onSelect?: (teamId: number) => void;\n    strings: {\n        get: (key: string, ...args: any[]) => string;\n    };\n}\nexport const TeamSelect: React.FC<TeamSelectProps> = ({ teamId, required, disabled, maxTeams, showObserver, onSelect, strings, }) => {\n    const teams = new Array(maxTeams).fill(0).map((_, index) => index);\n    return (<Select className=\"player-team-select\" initialValue={String(teamId)} disabled={disabled} tooltip={strings.get(\"STT:HostComboTeam\")} onSelect={(value) => {\n            onSelect?.(Number(value));\n        }}>\n      {!required && (<Option value={String(NO_TEAM_ID)} label={strings.get(\"GUI:NoneAsSymbols\")}/>)}\n      {teams.map((team) => (<Option key={team} value={String(team)} label={formatTeamId(team)}/>))}\n      {showObserver && (<Option value={String(OBS_TEAM_ID)} label={strings.get(\"GUI:Observer\")}/>)}\n    </Select>);\n};\n"
  },
  {
    "path": "src/gui/component/ToastApi.tsx",
    "content": "import { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { Toasts } from \"@/gui/component/Toasts\";\nimport { jsx } from \"@/gui/jsx/jsx\";\ninterface ToastMessage {\n    text: string;\n    timestamp: number;\n}\ninterface Viewport {\n    value: any;\n    onChange: {\n        subscribe: (fn: (v: any) => void) => void;\n        unsubscribe: (fn: (v: any) => void) => void;\n    };\n}\ninterface UiScene {\n    add: (el: any) => void;\n    remove: (el: any) => void;\n}\ninterface JsxRenderer {\n    render: (el: any) => [\n        any\n    ];\n}\nexport class ToastApi {\n    private viewport: Viewport;\n    private uiScene: UiScene;\n    private jsxRenderer: JsxRenderer;\n    private messages: ToastMessage[];\n    private disposables: CompositeDisposable;\n    private handleViewportChange: (v: any) => void;\n    private innerComponent?: any;\n    private uiToasts?: any;\n    private updateTimeoutId?: any;\n    constructor(viewport: Viewport, uiScene: UiScene, jsxRenderer: JsxRenderer) {\n        this.viewport = viewport;\n        this.uiScene = uiScene;\n        this.jsxRenderer = jsxRenderer;\n        this.messages = [];\n        this.disposables = new CompositeDisposable();\n        this.handleViewportChange = (v) => {\n            if (this.innerComponent) {\n                this.innerComponent.applyOptions((opts: any) => (opts.viewport = v));\n            }\n        };\n    }\n    push(text: string) {\n        const timestamp = Date.now();\n        this.messages.push({ text, timestamp });\n        if (this.updateTimeoutId) {\n            clearTimeout(this.updateTimeoutId);\n            this.updateTimeoutId = void 0;\n        }\n        this.update();\n    }\n    update() {\n        this.messages = this.messages.filter((msg) => msg.timestamp > Date.now() - 5000);\n        this.messages = this.messages.slice(-5);\n        if (this.messages.length) {\n            const texts = this.messages.map((msg) => msg.text);\n            if (this.uiToasts) {\n                this.innerComponent?.applyOptions((opts: any) => (opts.messages = texts));\n            }\n            else {\n                const [ui] = this.jsxRenderer.render(jsx(HtmlView, {\n                    innerRef: (ref: any) => (this.innerComponent = ref),\n                    component: Toasts,\n                    props: {\n                        messages: texts,\n                        viewport: this.viewport.value,\n                        zIndex: 101,\n                    },\n                }));\n                this.uiToasts = ui;\n                this.uiScene.add(ui);\n                this.viewport.onChange.subscribe(this.handleViewportChange);\n                this.disposables.add(ui, () => this.uiScene.remove(ui), () => this.viewport.onChange.unsubscribe(this.handleViewportChange), () => (this.innerComponent = void 0), () => (this.uiToasts = void 0));\n            }\n            this.updateTimeoutId = setTimeout(() => this.update(), 5000);\n        }\n        else {\n            this.destroy();\n        }\n    }\n    destroy() {\n        if (this.updateTimeoutId) {\n            clearTimeout(this.updateTimeoutId);\n            this.updateTimeoutId = void 0;\n        }\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/component/Toasts.tsx",
    "content": "import React from \"react\";\ninterface ToastsProps {\n    messages: string[];\n    viewport: {\n        x: number;\n        y: number;\n        width: number;\n    };\n    zIndex?: number;\n}\nexport const Toasts: React.FC<ToastsProps> = ({ messages, viewport, zIndex }) => {\n    return (<div style={{\n            position: \"absolute\",\n            top: viewport.x,\n            left: viewport.y,\n            width: viewport.width,\n            zIndex: zIndex,\n        }}>\n      <div className=\"toasts\">\n        {messages.map((msg, idx) => (<div key={idx} className=\"toast\">\n            {msg}\n          </div>))}\n      </div>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/component/UiText.tsx",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { SpriteUtils } from \"@/engine/gfx/SpriteUtils\";\nimport { CanvasUtils } from \"@/engine/gfx/CanvasUtils\";\nexport type UiTextProps = UiComponentProps & {\n    value: string;\n    textAlign?: CanvasTextAlign;\n    textColor: string;\n    width: number;\n    height: number;\n    zIndex?: number;\n    onClick?: () => void;\n    x?: number;\n    y?: number;\n};\nexport class UiText extends UiComponent<UiTextProps> {\n    declare ctx: CanvasRenderingContext2D | null;\n    declare texture: THREE.Texture;\n    declare mesh: THREE.Mesh;\n    declare value: string;\n    declare textAlign?: CanvasTextAlign;\n    constructor(props: UiTextProps) {\n        super(props);\n        this.value = props.value;\n        this.textAlign = props.textAlign;\n    }\n    createUiObject(): UiObject {\n        const obj = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        obj.setPosition(this.props.x || 0, this.props.y || 0);\n        const width = this.props.width;\n        const height = this.props.height;\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = width;\n        canvas.height = height;\n        this.ctx = canvas.getContext(\"2d\", { alpha: true });\n        this.texture = this.createTexture(canvas);\n        this.updateTexture(this.value, this.textAlign, this.props.textColor);\n        this.mesh = this.createMesh(width, height);\n        return obj;\n    }\n    createTexture(canvas: HTMLCanvasElement): THREE.Texture {\n        const texture = new THREE.Texture(canvas);\n        texture.needsUpdate = true;\n        texture.flipY = false;\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        return texture;\n    }\n    createMesh(width: number, height: number): THREE.Mesh {\n        const geometry = SpriteUtils.createRectGeometry(width, height);\n        SpriteUtils.addRectUvs(geometry, { x: 0, y: 0, width, height }, { width, height });\n        geometry.translate(width / 2, height / 2, 0);\n        const material = new THREE.MeshBasicMaterial({\n            map: this.texture,\n            side: THREE.DoubleSide,\n            transparent: true,\n        });\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.frustumCulled = false;\n        return mesh;\n    }\n    defineChildren() {\n        return jsx(\"mesh\", { zIndex: this.props.zIndex, onClick: this.props.onClick }, this.mesh);\n    }\n    updateTexture(value: string, textAlign: CanvasTextAlign | undefined, textColor: string) {\n        if (!this.ctx)\n            return;\n        this.ctx.clearRect(0, 0, this.props.width, this.props.height);\n        CanvasUtils.drawText(this.ctx, value, 0, 0, {\n            color: textColor,\n            fontFamily: \"'Fira Sans Condensed', Arial, sans-serif\",\n            fontSize: 12,\n            fontWeight: \"500\",\n            paddingTop: 6,\n            textAlign: textAlign ?? \"center\",\n            width: this.props.width,\n            height: this.props.height,\n        });\n        this.texture.needsUpdate = true;\n    }\n    setValue(value: string) {\n        if (this.value !== value) {\n            this.value = value;\n            this.updateTexture(value, this.textAlign, this.props.textColor);\n        }\n    }\n    setTextAlign(textAlign: CanvasTextAlign) {\n        if (textAlign !== this.textAlign) {\n            this.textAlign = textAlign;\n            this.updateTexture(this.value, textAlign, this.props.textColor);\n        }\n    }\n    onDispose() {\n        this.mesh.geometry.dispose();\n        (this.mesh.material as THREE.Material).dispose();\n        this.texture.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/component/fileExplorer/StorageFileExplorer.css",
    "content": "@font-face {\n    font-family: 'fe_fileexplorer_actions';\n    src: url('./assets/fileexplorer_actions.woff') format('woff');\n    font-weight: normal;\n    font-style: normal;\n    font-display: block;\n}\n\n.fe_fileexplorer_disabled {\n    filter: grayscale(95%);\n    opacity: 0.6;\n}\n\n.fe_fileexplorer_open_icon {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    width: 24px;\n    height: 24px;\n    background-position: -48px -96px;\n    image-rendering: pixelated;\n}\n\n.fe_fileexplorer_wrap {\n    position: relative;\n    font-size: 1em;\n    user-select: none;\n    cursor: default;\n    height: 100%;\n    min-height: 9em;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_operation_in_progress {\n    cursor: progress;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap {\n    border: 1px solid #aaaaaa;\n    color: #000000;\n    background-color: #ffffff;\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n    box-sizing: border-box;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_inner_wrap.fe_fileexplorer_inner_wrap_focused {\n    border-color: #0063b1;\n}\n\n.fe_fileexplorer_wrap button {\n    padding: 0;\n    border: 0 none;\n    box-sizing: border-box;\n    background-color: transparent;\n    outline: none;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_toolbar {\n    display: flex;\n    margin-top: 0.4em;\n    align-items: center;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtools {\n    display: flex;\n    margin-left: 5px;\n    margin-right: 0.1em;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtools button {\n    height: 24px;\n    background-repeat: no-repeat;\n    image-rendering: pixelated;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_back {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    width: 32px;\n    background-position: 0 0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_forward {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    width: 32px;\n    background-position: -64px 0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_history {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    width: 18px;\n    background-position: -84px -24px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_up {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    width: 24px;\n    background-position: 0 -24px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_back:not(.fe_fileexplorer_disabled):hover,\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_back:not(.fe_fileexplorer_disabled):focus {\n    background-position: -32px 0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_forward:not(.fe_fileexplorer_disabled):hover,\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_forward:not(.fe_fileexplorer_disabled):focus {\n    background-position: -96px 0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_history:not(.fe_fileexplorer_disabled):hover,\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_history:not(.fe_fileexplorer_disabled):focus {\n    background-position: -102px -24px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_up:not(.fe_fileexplorer_disabled):hover,\n.fe_fileexplorer_wrap .fe_fileexplorer_navtool_up:not(.fe_fileexplorer_disabled):focus {\n    background-position: -24px -24px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_wrap {\n    display: flex;\n    flex: 1;\n    align-items: center;\n    overflow: hidden;\n    border: 1px solid #d9d9d9;\n    margin-right: 12px;\n    min-height: 26px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_icon {\n    height: 24px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_icon_inner {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    width: 24px;\n    height: 24px;\n    margin-left: 2px;\n    margin-right: 4px;\n    background-position: -72px -96px;\n    image-rendering: pixelated;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segments_scroll_wrap {\n    flex: 1;\n    overflow-x: auto;\n    scrollbar-width: none;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segments_scroll_wrap::-webkit-scrollbar {\n    height: 0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segments_wrap {\n    display: flex;\n    align-items: center;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segments_wrap::after {\n    content: '';\n    padding-left: 10%;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap {\n    display: flex;\n    border: 1px solid transparent;\n    outline: none;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap:hover {\n    border-color: #cce8ff;\n    background-color: #e5f3ff;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap:hover .fe_fileexplorer_path_opts {\n    border-left: 1px solid #cce8ff;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap.fe_fileexplorer_drag_hover {\n    border-color: #99d1ff;\n    background-color: #cce8ff;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_segment_wrap.fe_fileexplorer_drag_hover .fe_fileexplorer_path_opts {\n    border-left: 1px solid #99d1ff;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_name {\n    padding: 0.5em;\n    border: 1px solid transparent;\n    line-height: 1;\n    font-size: 0.75em;\n    white-space: nowrap;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_opts {\n    width: 18px;\n    padding: 0;\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-repeat: no-repeat;\n    background-position: -48px -24px;\n    image-rendering: pixelated;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_opts:hover,\n.fe_fileexplorer_wrap .fe_fileexplorer_path_opts:focus {\n    background-position: -84px -24px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_path_name:hover,\n.fe_fileexplorer_wrap .fe_fileexplorer_path_name:focus {\n    background-color: transparent;\n    border-color: transparent;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_body_wrap_outer {\n    flex: 1;\n    display: flex;\n    margin-top: 0.3em;\n    overflow: hidden;\n    position: relative;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_body_wrap {\n    display: flex;\n    align-items: stretch;\n    overflow: hidden;\n    width: 100%;\n    height: 100%;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools_scroll_wrap {\n    padding: 0.4em 10px;\n    border-right: 1px solid #cce8ff;\n    overflow-y: auto;\n    box-sizing: border-box;\n    scrollbar-width: none;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools_scroll_wrap::-webkit-scrollbar {\n    width: 0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools {\n    display: flex;\n    flex-direction: column;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools button {\n    margin-bottom: 0.3em;\n    border: 1px solid transparent;\n    padding: 4px;\n    width: 34px;\n    height: 34px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools button::before {\n    display: block;\n    width: 24px;\n    height: 24px;\n    content: '';\n    background-repeat: no-repeat;\n    image-rendering: pixelated;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools button:not(.fe_fileexplorer_disabled):hover,\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tools button:not(.fe_fileexplorer_disabled):focus {\n    border-color: #99d1ff;\n    background-color: #e5f3ff;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_separator {\n    margin: 0 -0.1em 0.3em;\n    border-top: 1px solid #dfe7f0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_new_folder::before {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -24px -144px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_upload::before {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -96px -144px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_download::before {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -96px -120px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_copy::before {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -24px -120px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_paste::before {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -48px -144px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_cut::before {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -48px -120px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_delete::before {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -72px -120px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_folder_tool_item_checkboxes::before {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -72px -144px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_show_item_checkboxes .fe_fileexplorer_folder_tool_item_checkboxes::before {\n    background-position: 0 -120px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_scroll_wrap {\n    flex: 1;\n    overflow-y: auto;\n    box-sizing: border-box;\n    outline: none;\n    position: relative;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_scroll_wrap.fe_fileexplorer_items_scroll_wrap_drag_active {\n    background-color: #f4fbff;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_scroll_wrap_inner {\n    position: relative;\n    min-height: 100%;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_drop_message {\n    position: sticky;\n    top: 10px;\n    margin: 10px 14px 0;\n    padding: 12px;\n    border: 2px dashed #99d1ff;\n    background-color: rgba(229, 243, 255, 0.92);\n    color: #0f4c81;\n    text-align: center;\n    font-size: 0.8em;\n    z-index: 2;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_select_box {\n    position: absolute;\n    box-sizing: border-box;\n    border: 1px solid #0078d7;\n    background-color: rgba(0, 120, 215, 0.22);\n    pointer-events: none;\n    z-index: 1;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_wrap {\n    position: absolute;\n    left: 53px;\n    top: 0;\n    width: calc(100% - 53px);\n    height: 200px;\n    max-height: 75%;\n    z-index: 2;\n    outline: none;\n    pointer-events: none;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_inner_wrap {\n    position: relative;\n    height: 100%;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_text_wrap {\n    position: absolute;\n    inset: 0;\n    box-sizing: border-box;\n    background-color: rgba(255, 255, 255, 0.95);\n    border: 2px dashed #aaaaaa;\n    box-shadow: 2px 3px 5px 0 rgba(0, 0, 0, 0.15);\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_wrap:hover .fe_fileexplorer_items_clipboard_overlay_paste_text_wrap,\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_wrap:focus .fe_fileexplorer_items_clipboard_overlay_paste_text_wrap {\n    border-color: #3298fe;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_text {\n    position: absolute;\n    left: 50%;\n    top: 50%;\n    transform: translate(-50%, -50%);\n    text-align: center;\n    color: #888888;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_text_big {\n    font-size: 1.55em;\n    margin-bottom: 0.35em;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_clipboard_overlay_paste_text_small {\n    font-size: 0.75em;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_message_wrap {\n    padding: 1.5em 1em 1em;\n    color: #6d6d6d;\n    font-size: 0.75em;\n    text-align: center;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_items_wrap {\n    display: flex;\n    flex-wrap: wrap;\n    padding: 0.3em 12px 0.2em 4px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_wrap {\n    margin-left: 0.56em;\n    margin-bottom: 1px;\n    width: 4.7em;\n    box-sizing: border-box;\n    text-align: center;\n    overflow: hidden;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_wrap_inner {\n    position: relative;\n    border: 1px solid transparent;\n    padding: 0.1em 0.3em;\n    outline: none;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_wrap_inner:hover {\n    background-color: #e5f3ff;\n    border-color: #e5f3ff;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_selected .fe_fileexplorer_item_wrap_inner {\n    background-color: #cde8ff;\n    border-color: #99d1ff;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_wrap.fe_fileexplorer_drag_hover .fe_fileexplorer_item_wrap_inner {\n    background-color: #cde8ff;\n    border-color: #99d1ff;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_checkbox {\n    position: absolute;\n    left: 0;\n    top: 0;\n    margin: 2px;\n    z-index: 1;\n    display: none;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_show_item_checkboxes .fe_fileexplorer_item_checkbox,\n.fe_fileexplorer_wrap .fe_fileexplorer_item_selected .fe_fileexplorer_item_checkbox {\n    display: block;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon {\n    width: 48px;\n    height: 48px;\n    margin-left: auto;\n    margin-right: auto;\n    background-repeat: no-repeat;\n    position: relative;\n    image-rendering: pixelated;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_text {\n    margin-top: 0.1em;\n    font-size: 0.75em;\n    text-overflow: ellipsis;\n    display: -webkit-box;\n    -webkit-line-clamp: 4;\n    -webkit-box-orient: vertical;\n    word-wrap: break-word;\n    overflow: hidden;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_folder {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -48px -48px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: 0 -48px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_file:not(.fe_fileexplorer_item_icon_file_no_ext)::after {\n    position: absolute;\n    bottom: 10px;\n    left: 0;\n    box-sizing: border-box;\n    content: attr(data-ext);\n    color: #ffffff;\n    font-size: 11px;\n    padding: 1px 3px;\n    width: 36px;\n    overflow: hidden;\n    white-space: nowrap;\n    background-color: #888888;\n    text-transform: uppercase;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_a::after {\n    background-color: #f03c3c;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_b::after {\n    background-color: #f05a3c;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_c::after {\n    background-color: #f0783c;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_d::after {\n    background-color: #f0963c;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_e::after {\n    background-color: #e0862b;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_f::after {\n    background-color: #dca12b;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_g::after {\n    background-color: #c7ab1e;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_h::after {\n    background-color: #c7c71e;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_i::after {\n    background-color: #abc71e;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_j::after {\n    background-color: #8fc71e;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_k::after {\n    background-color: #72c71e;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_l::after {\n    background-color: #56c71e;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_m::after {\n    background-color: #3ac71e;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_n::after {\n    background-color: #1ec71e;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_o::after {\n    background-color: #1ec73a;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_p::after {\n    background-color: #1ec756;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_q::after {\n    background-color: #1ec78f;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_r::after {\n    background-color: #1ec7ab;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_s::after {\n    background-color: #1ec7c7;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_t::after {\n    background-color: #1eabc7;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_u::after {\n    background-color: #1e8fc7;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_v::after {\n    background-color: #1e72c7;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_w::after {\n    background-color: #3c78f0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_x::after {\n    background-color: #3c5af0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_y::after {\n    background-color: #3c3cf0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_z::after {\n    background-color: #5a3cf0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_0::after {\n    background-color: #783cf0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_1::after {\n    background-color: #963cf0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_2::after {\n    background-color: #b43cf0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_3::after {\n    background-color: #d23cf0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_4::after {\n    background-color: #f03cf0;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_5::after {\n    background-color: #f03cd2;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_6::after {\n    background-color: #f03cb4;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_7::after {\n    background-color: #f03c96;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_8::after {\n    background-color: #f03c78;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_item_icon_ext_9::after {\n    background-color: #f03c5a;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_wrap {\n    display: flex;\n    white-space: nowrap;\n    font-size: 0.75em;\n    color: #14273e;\n    border-top: 1px solid #e4e7ec;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_wrap.fe_fileexplorer_statusbar_wrap_multiline {\n    display: block;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_text_wrap {\n    display: flex;\n    margin-left: 15px;\n    margin-right: 12px;\n    padding-top: 0.3em;\n    padding-bottom: 0.3em;\n    overflow: hidden;\n    flex: 1;\n    line-height: 1.1;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_text_segment_wrap {\n    padding-right: 1em;\n    border-right: 1px solid #f0f0f0;\n    margin-right: 1em;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_statusbar_text_segment_wrap_last {\n    padding-right: 0;\n    border-right: 0 none;\n    margin-right: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_action_wrap {\n    display: flex;\n    align-items: center;\n    padding-right: 10px;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_action_wrap button {\n    border: 1px solid transparent;\n}\n\n.fe_fileexplorer_wrap .fe_fileexplorer_action_wrap button:hover,\n.fe_fileexplorer_wrap .fe_fileexplorer_action_wrap button:focus {\n    border-color: #99d1ff;\n    background-color: #e5f3ff;\n}\n\n.fe_fileexplorer_popup_wrap {\n    position: fixed;\n    max-height: 33vh;\n    overflow-y: auto;\n    border: 1px solid #a0a0a0;\n    background-color: #f2f2f2;\n    min-width: 11em;\n    max-width: 18em;\n    z-index: 100;\n    box-shadow: 5px 5px 4px -3px rgba(0, 0, 0, 0.57);\n    font-size: 1em;\n    user-select: none;\n    cursor: default;\n    outline: none;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_inner_wrap {\n    position: relative;\n    padding: 2px;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_split {\n    margin-left: 34px;\n    margin-top: 0.1em;\n    border-top: 1px solid #d7d7d7;\n    padding-top: 0.1em;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_wrap {\n    display: flex;\n    align-items: center;\n    box-sizing: border-box;\n    outline: none;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_wrap:hover,\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_wrap_focus {\n    background-color: #c3def5;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon {\n    height: 24px;\n    image-rendering: pixelated;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_inner {\n    width: 24px;\n    height: 24px;\n    margin-left: 5px;\n    margin-right: 5px;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_text {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    font-size: 0.75em;\n    line-height: 1;\n    white-space: nowrap;\n    padding: 0.5em 0.3em;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_text.fe_fileexplorer_popup_item_active {\n    font-weight: bold;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_disabled {\n    color: #6d6d6d;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_disabled:hover,\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_disabled.fe_fileexplorer_popup_item_wrap_focus {\n    background-color: #e5e5e5;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_back {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -96px -48px;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_forward {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -24px -96px;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_check {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: 0 -96px;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_folder {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -96px -96px;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_file {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: 0 -48px;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_download {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -96px -120px;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_copy {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -24px -120px;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_paste {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -48px -144px;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_cut {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -48px -120px;\n}\n\n.fe_fileexplorer_popup_wrap .fe_fileexplorer_popup_item_icon_delete {\n    background-image: url('./assets/fileexplorer_sprites.png');\n    background-position: -72px -120px;\n}\n\n.fe_fileexplorer_floating_drag_icon_wrap {\n    position: fixed;\n    left: -9999px;\n    top: -9999px;\n    padding: 1.5em;\n    pointer-events: none;\n    border: 1px solid rgba(151, 220, 252, 0.4);\n    background-image: linear-gradient(rgba(227, 245, 252, 0.4), rgba(189, 231, 252, 0.4));\n    z-index: 100;\n    box-shadow: 5px 5px 4px -3px rgba(0, 0, 0, 0.3);\n}\n\n.fe_fileexplorer_floating_drag_icon_wrap .fe_fileexplorer_floating_drag_icon_wrap_inner {\n    position: relative;\n    width: 48px;\n    height: 48px;\n    overflow: hidden;\n}\n\n.fe_fileexplorer_floating_drag_icon_wrap .fe_fileexplorer_item_icon {\n    width: 48px;\n    height: 48px;\n    background-repeat: no-repeat;\n    opacity: 0.82;\n}\n\n.fe_fileexplorer_floating_drag_icon_wrap .fe_fileexplorer_floating_drag_icon_wrap_inner[data-numitems]::after {\n    position: absolute;\n    left: 50%;\n    top: 50%;\n    transform: translate(-50%, -30%);\n    padding: 0.1em 0.3em;\n    font-size: 0.75em;\n    background-color: #0074cc;\n    border: 1px solid #ffffff;\n    color: #ffffff;\n    content: attr(data-numitems);\n}\n\n@media (pointer: coarse) {\n    .fe_fileexplorer_wrap .fe_fileexplorer_item_wrap {\n        margin-top: 0.1em;\n        margin-bottom: 0.1em;\n    }\n\n    .fe_fileexplorer_wrap .fe_fileexplorer_show_item_checkboxes .fe_fileexplorer_item_checkbox {\n        display: block;\n    }\n}\n"
  },
  {
    "path": "src/gui/component/fileExplorer/StorageFileExplorer.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react';\nimport AppLogger from '../../../util/logger';\nimport { Zip } from '../../../data/zip/Zip';\nimport './StorageFileExplorer.css';\n\ninterface ExplorerEntry {\n    id: string;\n    name: string;\n    type: 'folder' | 'file';\n    size?: number;\n    canModify: boolean;\n}\n\ninterface StorageFileExplorerProps {\n    rootHandle: FileSystemDirectoryHandle;\n    rootLabel: string;\n    startIn?: string;\n    isSystemFile?: (path: string) => boolean;\n    isUploadAllowed?: (path: string) => boolean;\n    shouldLowerCaseFile?: (path: string) => boolean;\n    onFileSystemChange?: () => void;\n    onFileOpen?: (path: string, entry: ExplorerEntry) => void;\n    onInfo?: (message: string) => void;\n    promptForText?: (message: string) => Promise<string | undefined>;\n    confirmAction?: (message: string, confirmLabel?: string, cancelLabel?: string) => Promise<boolean>;\n    showAlert?: (message: string, title?: string) => Promise<void> | void;\n    downloadMultiple?: (currentPath: string, items: ExplorerEntry[]) => Promise<void>;\n    canCreateFolder?: (path: string, segments: string[]) => boolean;\n    validateNewFolderName?: (name: string, path: string, segments: string[]) => string | undefined;\n    emptyState?: React.ReactNode;\n    loadingLabel?: string;\n}\n\ninterface ClipboardEntryRef {\n    id: string;\n    type: 'folder' | 'file';\n}\n\ninterface ExplorerClipboard {\n    mode: 'copy' | 'cut';\n    sourceSegments: string[];\n    items: ClipboardEntryRef[];\n}\n\ninterface PopupMenuItem {\n    id: string;\n    label: string;\n    iconClass?: string;\n    disabled?: boolean;\n    active?: boolean;\n    separatorBefore?: boolean;\n    onSelect: () => void | Promise<void>;\n}\n\ninterface PopupMenuState {\n    x: number;\n    y: number;\n    items: PopupMenuItem[];\n    focusIndex: number;\n}\n\ninterface InternalDragPayload {\n    sourceSegments: string[];\n    items: ClipboardEntryRef[];\n}\n\ninterface PathSegmentEntry {\n    label: string;\n    index: number;\n    segments: string[];\n}\n\ninterface HistoryEntryState {\n    segments: string[];\n    lastSelectedId: string | null;\n}\n\ninterface SelectionBoxRect {\n    left: number;\n    top: number;\n    width: number;\n    height: number;\n}\n\nfunction normalizeSegments(path?: string): string[] {\n    return (path ?? '')\n        .split('/')\n        .map((segment) => segment.trim())\n        .filter(Boolean);\n}\n\nfunction buildPath(segments: string[], name?: string): string {\n    const parts = [...segments];\n    if (name) {\n        parts.push(name);\n    }\n    return parts.length ? `/${parts.join('/')}` : '/';\n}\n\nasync function navigateToPath(rootHandle: FileSystemDirectoryHandle, segments: string[]): Promise<FileSystemDirectoryHandle> {\n    let currentHandle = rootHandle;\n    for (const segment of segments) {\n        currentHandle = await currentHandle.getDirectoryHandle(segment);\n    }\n    return currentHandle;\n}\n\nasync function readEntriesFromDirectory(\n    dirHandle: FileSystemDirectoryHandle,\n    currentPath: string,\n    isSystemFile?: (path: string) => boolean,\n): Promise<ExplorerEntry[]> {\n    const entries: ExplorerEntry[] = [];\n    for await (const [name, handle] of dirHandle.entries()) {\n        const fullPath = currentPath === '/' ? `/${name}` : `${currentPath}/${name}`;\n        const canModify = isSystemFile ? !isSystemFile(fullPath) : true;\n        if (handle.kind === 'directory') {\n            entries.push({\n                id: name,\n                name,\n                type: 'folder',\n                canModify,\n            });\n        }\n        else {\n            const file = await (handle as FileSystemFileHandle).getFile();\n            entries.push({\n                id: name,\n                name,\n                type: 'file',\n                size: file.size,\n                canModify,\n            });\n        }\n    }\n    return entries.sort((left, right) => {\n        if (left.type !== right.type) {\n            return left.type === 'folder' ? -1 : 1;\n        }\n        return left.name.localeCompare(right.name);\n    });\n}\n\nasync function downloadSingleFile(file: File) {\n    if ('showSaveFilePicker' in window && window.showSaveFilePicker) {\n        const saveFileHandle = await window.showSaveFilePicker({\n            suggestedName: file.name,\n        });\n        const writable = await saveFileHandle.createWritable();\n        try {\n            await writable.write(file);\n            await writable.close();\n        }\n        catch (error) {\n            if (typeof (writable as any).abort === 'function') {\n                await (writable as any).abort();\n            }\n            throw error;\n        }\n        return;\n    }\n    const url = URL.createObjectURL(file);\n    const anchor = document.createElement('a');\n    anchor.href = url;\n    anchor.download = file.name;\n    document.body.appendChild(anchor);\n    anchor.click();\n    document.body.removeChild(anchor);\n    URL.revokeObjectURL(url);\n}\n\nfunction formatFileSize(bytes?: number): string {\n    if (bytes === undefined) {\n        return '-';\n    }\n    if (bytes < 1024) {\n        return `${bytes} B`;\n    }\n    if (bytes < 1024 * 1024) {\n        return `${(bytes / 1024).toFixed(2)} KB`;\n    }\n    if (bytes < 1024 * 1024 * 1024) {\n        return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;\n    }\n    return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;\n}\n\nfunction getFileExtension(name: string): string {\n    const lastDotIndex = name.lastIndexOf('.');\n    if (lastDotIndex <= 0 || lastDotIndex === name.length - 1) {\n        return '';\n    }\n    return name.slice(lastDotIndex + 1).toLowerCase();\n}\n\nfunction getFileIconClass(entry: ExplorerEntry): string {\n    if (entry.type === 'folder') {\n        return 'fe_fileexplorer_item_icon_folder';\n    }\n    const extension = getFileExtension(entry.name);\n    if (!extension) {\n        return 'fe_fileexplorer_item_icon_file fe_fileexplorer_item_icon_file_no_ext';\n    }\n    const extLead = extension.charAt(0).toLowerCase().replace(/[^a-z0-9]/g, 'a');\n    return `fe_fileexplorer_item_icon_file fe_fileexplorer_item_icon_ext_${extLead}`;\n}\n\nfunction getFileExtLabel(entry: ExplorerEntry): string {\n    if (entry.type === 'folder') {\n        return '';\n    }\n    return getFileExtension(entry.name).slice(0, 4).toUpperCase();\n}\n\nasync function entryExists(directoryHandle: FileSystemDirectoryHandle, name: string): Promise<boolean> {\n    try {\n        await directoryHandle.getFileHandle(name);\n        return true;\n    }\n    catch {\n    }\n    try {\n        await directoryHandle.getDirectoryHandle(name);\n        return true;\n    }\n    catch {\n    }\n    return false;\n}\n\nfunction getCopyName(name: string, attempt: number): string {\n    const lastDotIndex = name.lastIndexOf('.');\n    const hasExt = lastDotIndex > 0;\n    const baseName = hasExt ? name.slice(0, lastDotIndex) : name;\n    const extension = hasExt ? name.slice(lastDotIndex) : '';\n    const suffix = attempt === 1 ? ' - copy' : ` - copy ${attempt}`;\n    return `${baseName}${suffix}${extension}`;\n}\n\nasync function getAvailableCopyName(directoryHandle: FileSystemDirectoryHandle, name: string): Promise<string> {\n    let attempt = 1;\n    let candidate = getCopyName(name, attempt);\n    while (await entryExists(directoryHandle, candidate)) {\n        attempt += 1;\n        candidate = getCopyName(name, attempt);\n    }\n    return candidate;\n}\n\nasync function cloneHandleToDirectory(\n    sourceHandle: FileSystemHandle,\n    targetDirectoryHandle: FileSystemDirectoryHandle,\n    targetName: string,\n): Promise<void> {\n    if (sourceHandle.kind === 'file') {\n        const file = await (sourceHandle as FileSystemFileHandle).getFile();\n        const targetFileHandle = await targetDirectoryHandle.getFileHandle(targetName, { create: true });\n        const writable = await targetFileHandle.createWritable();\n        try {\n            await writable.write(file);\n            await writable.close();\n        }\n        catch (error) {\n            if (typeof (writable as any).abort === 'function') {\n                await (writable as any).abort();\n            }\n            throw error;\n        }\n        return;\n    }\n    const targetChildDirectory = await targetDirectoryHandle.getDirectoryHandle(targetName, { create: true });\n    for await (const [childName, childHandle] of (sourceHandle as FileSystemDirectoryHandle).entries()) {\n        await cloneHandleToDirectory(childHandle, targetChildDirectory, childName);\n    }\n}\n\nasync function downloadBlob(blob: Blob, suggestedName: string) {\n    if ('showSaveFilePicker' in window && window.showSaveFilePicker) {\n        const saveFileHandle = await window.showSaveFilePicker({ suggestedName });\n        const writable = await saveFileHandle.createWritable();\n        try {\n            await writable.write(blob);\n            await writable.close();\n        }\n        catch (error) {\n            if (typeof (writable as any).abort === 'function') {\n                await (writable as any).abort();\n            }\n            throw error;\n        }\n        return;\n    }\n    const url = URL.createObjectURL(blob);\n    const anchor = document.createElement('a');\n    anchor.href = url;\n    anchor.download = suggestedName;\n    document.body.appendChild(anchor);\n    anchor.click();\n    document.body.removeChild(anchor);\n    URL.revokeObjectURL(url);\n}\n\nfunction parseInternalDragPayload(dataTransfer: DataTransfer): InternalDragPayload | null {\n    const raw = dataTransfer.getData('application/x-ra2-fileexplorer-items');\n    if (!raw) {\n        return null;\n    }\n    try {\n        const parsed = JSON.parse(raw) as InternalDragPayload;\n        if (!Array.isArray(parsed.sourceSegments) || !Array.isArray(parsed.items)) {\n            return null;\n        }\n        return parsed;\n    }\n    catch {\n        return null;\n    }\n}\n\nfunction parseClipboardPayload(dataTransfer: DataTransfer | null): ExplorerClipboard | null {\n    if (!dataTransfer) {\n        return null;\n    }\n    const raw = dataTransfer.getData('application/x-ra2-fileexplorer-clipboard');\n    if (raw) {\n        try {\n            const parsed = JSON.parse(raw) as ExplorerClipboard;\n            if (parsed?.mode && Array.isArray(parsed.sourceSegments) && Array.isArray(parsed.items)) {\n                return parsed;\n            }\n        }\n        catch {\n        }\n    }\n    const textPlain = dataTransfer.getData('text/plain');\n    if (!textPlain) {\n        return null;\n    }\n    try {\n        const parsed = JSON.parse(textPlain) as { 'application/x-ra2-fileexplorer-clipboard'?: ExplorerClipboard };\n        const payload = parsed?.['application/x-ra2-fileexplorer-clipboard'];\n        if (payload?.mode && Array.isArray(payload.sourceSegments) && Array.isArray(payload.items)) {\n            return payload;\n        }\n    }\n    catch {\n    }\n    return null;\n}\n\nexport const StorageFileExplorer: React.FC<StorageFileExplorerProps> = ({\n    rootHandle,\n    rootLabel,\n    startIn,\n    isSystemFile,\n    isUploadAllowed,\n    shouldLowerCaseFile,\n    onFileSystemChange,\n    onFileOpen,\n    onInfo,\n    promptForText,\n    confirmAction,\n    showAlert,\n    downloadMultiple,\n    canCreateFolder,\n    validateNewFolderName,\n    emptyState,\n    loadingLabel,\n}) => {\n    const initialSegments = normalizeSegments(startIn);\n    const [currentSegments, setCurrentSegments] = useState<string[]>(initialSegments);\n    const [entries, setEntries] = useState<ExplorerEntry[]>([]);\n    const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());\n    const [error, setError] = useState<string | null>(null);\n    const [loading, setLoading] = useState(false);\n    const [statusMessage, setStatusMessage] = useState('');\n    const [showCheckboxes, setShowCheckboxes] = useState(false);\n    const [historyEntries, setHistoryEntries] = useState<HistoryEntryState[]>([{\n        segments: initialSegments,\n        lastSelectedId: null,\n    }]);\n    const [historyIndex, setHistoryIndex] = useState(0);\n    const [focusedId, setFocusedId] = useState<string | null>(null);\n    const [clipboard, setClipboard] = useState<ExplorerClipboard | null>(null);\n    const [showPasteOverlay, setShowPasteOverlay] = useState(false);\n    const [dragActive, setDragActive] = useState(false);\n    const [dragFolderHoverId, setDragFolderHoverId] = useState<string | null>(null);\n    const [dragPathHoverIndex, setDragPathHoverIndex] = useState<number | null>(null);\n    const [selectionBoxRect, setSelectionBoxRect] = useState<SelectionBoxRect | null>(null);\n    const [popupMenu, setPopupMenu] = useState<PopupMenuState | null>(null);\n    const uploadInputRef = useRef<HTMLInputElement>(null);\n    const rootRef = useRef<HTMLDivElement>(null);\n    const navHistoryRef = useRef<HTMLButtonElement>(null);\n    const popupRef = useRef<HTMLDivElement>(null);\n    const itemsScrollRef = useRef<HTMLDivElement>(null);\n    const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());\n    const pathNameRefs = useRef<Map<number, HTMLButtonElement>>(new Map());\n    const pathOptionRefs = useRef<Map<number, HTMLButtonElement>>(new Map());\n    const dragImageRef = useRef<HTMLDivElement | null>(null);\n    const historyEntriesRef = useRef(historyEntries);\n    const historyIndexRef = useRef(historyIndex);\n    const browserCaptureIdRef = useRef(`fileexplorer-${Math.random().toString(36).slice(2)}`);\n    const browserCaptureRefs = useRef(0);\n    const browserCaptureActive = useRef(false);\n    const browserScrollRestoreRef = useRef<string | undefined>(undefined);\n    const selectionDragRef = useRef<{\n        anchorClientX: number;\n        anchorClientY: number;\n        anchorContentX: number;\n        anchorContentY: number;\n        additive: boolean;\n        baseSelectedIds: Set<string>;\n        moved: boolean;\n    } | null>(null);\n    const selectionAutoScrollRef = useRef<{\n        timerId: number | null;\n        lastClientX: number;\n        lastClientY: number;\n    }>({\n        timerId: null,\n        lastClientX: 0,\n        lastClientY: 0,\n    });\n    const currentPath = buildPath(currentSegments);\n\n    historyEntriesRef.current = historyEntries;\n    historyIndexRef.current = historyIndex;\n\n    useEffect(() => {\n        const nextSegments = normalizeSegments(startIn);\n        clearSelectionAutoScroll();\n        setCurrentSegments(nextSegments);\n        setSelectedIds(new Set());\n        setHistoryEntries([{\n            segments: nextSegments,\n            lastSelectedId: null,\n        }]);\n        setHistoryIndex(0);\n        setStatusMessage('');\n        setFocusedId(null);\n        setClipboard(null);\n        setShowPasteOverlay(false);\n        setPopupMenu(null);\n        setDragPathHoverIndex(null);\n        setSelectionBoxRect(null);\n        selectionDragRef.current = null;\n    }, [rootHandle, startIn]);\n\n    useEffect(() => {\n        let cancelled = false;\n        const run = async () => {\n            setLoading(true);\n            setError(null);\n            try {\n                const currentDirHandle = await navigateToPath(rootHandle, currentSegments);\n                const nextEntries = await readEntriesFromDirectory(currentDirHandle, currentPath, isSystemFile);\n                if (!cancelled) {\n                    clearSelectionAutoScroll();\n                    setEntries(nextEntries);\n                    setSelectedIds(new Set());\n                    setFocusedId(null);\n                    setPopupMenu(null);\n                    setDragPathHoverIndex(null);\n                    setSelectionBoxRect(null);\n                    selectionDragRef.current = null;\n                }\n            }\n            catch (loadError: any) {\n                AppLogger.error('[StorageFileExplorer] Failed to read directory:', loadError);\n                if (!cancelled) {\n                    clearSelectionAutoScroll();\n                    setEntries([]);\n                    setSelectedIds(new Set());\n                    setFocusedId(null);\n                    setPopupMenu(null);\n                    setDragPathHoverIndex(null);\n                    setSelectionBoxRect(null);\n                    selectionDragRef.current = null;\n                    setError(loadError?.message ?? 'Failed to read directory');\n                }\n            }\n            finally {\n                if (!cancelled) {\n                    setLoading(false);\n                }\n            }\n        };\n        void run();\n        return () => {\n            cancelled = true;\n        };\n    }, [rootHandle, currentPath, isSystemFile]);\n\n    useEffect(() => {\n        if (!popupMenu) {\n            return undefined;\n        }\n        const handlePointerDown = (event: MouseEvent) => {\n            const target = event.target as Node | null;\n            if (target && popupRef.current?.contains(target)) {\n                return;\n            }\n            setPopupMenu(null);\n        };\n        const handleWindowBlur = () => {\n            setPopupMenu(null);\n        };\n        window.addEventListener('mousedown', handlePointerDown, true);\n        window.addEventListener('blur', handleWindowBlur);\n        return () => {\n            window.removeEventListener('mousedown', handlePointerDown, true);\n            window.removeEventListener('blur', handleWindowBlur);\n        };\n    }, [popupMenu]);\n\n    useEffect(() => {\n        if (!popupMenu) {\n            return;\n        }\n        popupRef.current?.focus();\n    }, [popupMenu]);\n\n    useEffect(() => () => {\n        dragImageRef.current?.remove();\n        dragImageRef.current = null;\n    }, []);\n\n    useEffect(() => () => {\n        browserCaptureRefs.current = 1;\n        stopBrowserCapture();\n    }, []);\n\n    useEffect(() => () => {\n        clearSelectionAutoScroll();\n    }, []);\n\n    const selectedEntries = entries.filter((entry) => selectedIds.has(entry.id));\n\n    useEffect(() => {\n        const lastSelectedId = historyEntries[historyIndex]?.lastSelectedId;\n        if (!lastSelectedId || selectedIds.size || !entries.some((entry) => entry.id === lastSelectedId)) {\n            return;\n        }\n        setSelectedIds(new Set([lastSelectedId]));\n        focusEntry(lastSelectedId);\n    }, [entries, historyEntries, historyIndex, selectedIds.size]);\n\n    useEffect(() => {\n        if (selectedIds.size === 0) {\n            return;\n        }\n        const lastSelectedId = selectedIds.size === 1 ? Array.from(selectedIds)[0] : null;\n        setHistoryEntries((prev) => prev.map((entry, index) => index === historyIndex\n            ? { ...entry, lastSelectedId }\n            : entry));\n    }, [selectedIds, historyIndex]);\n\n    const emitInfo = (message: string) => {\n        setStatusMessage(message);\n        onInfo?.(message);\n    };\n\n    const requestText = async (message: string) => {\n        if (promptForText) {\n            return promptForText(message);\n        }\n        const value = window.prompt(message);\n        return value === null ? undefined : value;\n    };\n\n    const requestConfirm = async (message: string, confirmLabel?: string, cancelLabel?: string) => {\n        if (confirmAction) {\n            return confirmAction(message, confirmLabel, cancelLabel);\n        }\n        return window.confirm(message);\n    };\n\n    const showMessage = async (message: string, title?: string) => {\n        if (showAlert) {\n            await Promise.resolve(showAlert(message, title));\n            return;\n        }\n        window.alert(title ? `${title}\\n\\n${message}` : message);\n    };\n\n    const focusEntry = (entryId: string | null) => {\n        if (!entryId) {\n            return;\n        }\n        setFocusedId(entryId);\n        requestAnimationFrame(() => {\n            const node = itemRefs.current.get(entryId);\n            node?.focus();\n            node?.scrollIntoView({ block: 'nearest', inline: 'nearest' });\n        });\n    };\n\n    const focusPathSegmentByIndex = (segmentIndex: number, target: 'name' | 'opts' = 'name') => {\n        requestAnimationFrame(() => {\n            const node = target === 'opts'\n                ? pathOptionRefs.current.get(segmentIndex) ?? pathNameRefs.current.get(segmentIndex)\n                : pathNameRefs.current.get(segmentIndex) ?? pathOptionRefs.current.get(segmentIndex);\n            node?.focus();\n            node?.scrollIntoView({ block: 'nearest', inline: 'nearest' });\n        });\n    };\n\n    const clearSelectionAutoScroll = () => {\n        if (selectionAutoScrollRef.current.timerId !== null) {\n            window.clearInterval(selectionAutoScrollRef.current.timerId);\n            selectionAutoScrollRef.current.timerId = null;\n        }\n    };\n\n    const syncSelectionAutoScroll = (clientX: number, clientY: number) => {\n        selectionAutoScrollRef.current.lastClientX = clientX;\n        selectionAutoScrollRef.current.lastClientY = clientY;\n        const itemsScroll = itemsScrollRef.current;\n        if (!selectionDragRef.current || !itemsScroll) {\n            clearSelectionAutoScroll();\n            return;\n        }\n        const itemsRect = itemsScroll.getBoundingClientRect();\n        const topOverflow = Math.max(0, itemsRect.top - clientY);\n        const bottomOverflow = Math.max(0, clientY - itemsRect.bottom);\n        if (!topOverflow && !bottomOverflow) {\n            clearSelectionAutoScroll();\n            return;\n        }\n        const applySelectionAutoScroll = () => {\n            const currentItemsScroll = itemsScrollRef.current;\n            if (!selectionDragRef.current || !currentItemsScroll) {\n                clearSelectionAutoScroll();\n                return;\n            }\n            const currentRect = currentItemsScroll.getBoundingClientRect();\n            const currentTopOverflow = Math.max(0, currentRect.top - selectionAutoScrollRef.current.lastClientY);\n            const currentBottomOverflow = Math.max(0, selectionAutoScrollRef.current.lastClientY - currentRect.bottom);\n            const scrollDelta = currentTopOverflow\n                ? -Math.max(1, Math.floor(currentTopOverflow / 8) + 1)\n                : currentBottomOverflow\n                    ? Math.max(1, Math.floor(currentBottomOverflow / 8) + 1)\n                    : 0;\n            if (!scrollDelta) {\n                clearSelectionAutoScroll();\n                return;\n            }\n            const maxScrollTop = currentItemsScroll.scrollHeight - currentItemsScroll.clientHeight;\n            const nextScrollTop = Math.max(0, Math.min(maxScrollTop, currentItemsScroll.scrollTop + scrollDelta));\n            if (nextScrollTop === currentItemsScroll.scrollTop) {\n                clearSelectionAutoScroll();\n                return;\n            }\n            currentItemsScroll.scrollTop = nextScrollTop;\n            updateSelectionBox(selectionAutoScrollRef.current.lastClientX, selectionAutoScrollRef.current.lastClientY);\n        };\n        if (selectionAutoScrollRef.current.timerId === null) {\n            selectionAutoScrollRef.current.timerId = window.setInterval(applySelectionAutoScroll, 16);\n        }\n        applySelectionAutoScroll();\n    };\n\n    const openPopupMenu = (items: PopupMenuItem[], x: number, y: number) => {\n        const focusIndex = Math.max(0, items.findIndex((item) => !item.disabled));\n        setPopupMenu({\n            x,\n            y,\n            items,\n            focusIndex: focusIndex === -1 ? 0 : focusIndex,\n        });\n    };\n\n    const activatePopupItem = async (index: number) => {\n        if (!popupMenu) {\n            return;\n        }\n        const target = popupMenu.items[index];\n        if (!target || target.disabled) {\n            return;\n        }\n        setPopupMenu(null);\n        await target.onSelect();\n    };\n\n    const navigateToSegments = (segments: string[], pushHistory = true) => {\n        const nextSegments = [...segments];\n        selectionDragRef.current = null;\n        clearSelectionAutoScroll();\n        setSelectionBoxRect(null);\n        setCurrentSegments(nextSegments);\n        setSelectedIds(new Set());\n        setFocusedId(null);\n        setStatusMessage('');\n        if (!pushHistory) {\n            return;\n        }\n        const baseHistory = historyEntries.slice(0, historyIndex + 1);\n        setHistoryEntries([...baseHistory, {\n            segments: nextSegments,\n            lastSelectedId: null,\n        }]);\n        setHistoryIndex(baseHistory.length);\n    };\n\n    const refresh = async () => {\n        setLoading(true);\n        setError(null);\n        try {\n            const currentDirHandle = await navigateToPath(rootHandle, currentSegments);\n            const nextEntries = await readEntriesFromDirectory(currentDirHandle, currentPath, isSystemFile);\n            clearSelectionAutoScroll();\n            setEntries(nextEntries);\n            setSelectedIds(new Set());\n            setFocusedId(null);\n            setStatusMessage('已刷新');\n            setDragPathHoverIndex(null);\n            setSelectionBoxRect(null);\n            selectionDragRef.current = null;\n        }\n        catch (refreshError: any) {\n            AppLogger.error('[StorageFileExplorer] Failed to refresh directory:', refreshError);\n            setEntries([]);\n            setSelectedIds(new Set());\n            setError(refreshError?.message ?? 'Failed to refresh directory');\n        }\n        finally {\n            setLoading(false);\n        }\n    };\n\n    const handleSelect = (entryId: string, additive: boolean, extendRange: boolean = false) => {\n        const entryIndex = entries.findIndex((entry) => entry.id === entryId);\n        if (extendRange && focusedId) {\n            const focusedIndex = entries.findIndex((entry) => entry.id === focusedId);\n            if (focusedIndex !== -1 && entryIndex !== -1) {\n                const start = Math.min(focusedIndex, entryIndex);\n                const end = Math.max(focusedIndex, entryIndex);\n                setSelectedIds(new Set(entries.slice(start, end + 1).map((entry) => entry.id)));\n                setFocusedId(entryId);\n                return;\n            }\n        }\n        setSelectedIds((prev) => {\n            if (!additive) {\n                return new Set([entryId]);\n            }\n            const next = new Set(prev);\n            if (next.has(entryId)) {\n                next.delete(entryId);\n            }\n            else {\n                next.add(entryId);\n            }\n            return next;\n        });\n        setFocusedId(entryId);\n    };\n\n    const openEntry = async (entry: ExplorerEntry) => {\n        if (entry.type === 'folder') {\n            navigateToSegments([...currentSegments, entry.id]);\n            return;\n        }\n        const nextPath = buildPath(currentSegments, entry.id);\n        if (onFileOpen) {\n            onFileOpen(nextPath, entry);\n            emitInfo(`打开文件: ${entry.name}`);\n            return;\n        }\n        try {\n            const currentDirHandle = await navigateToPath(rootHandle, currentSegments);\n            const fileHandle = await currentDirHandle.getFileHandle(entry.id);\n            const file = await fileHandle.getFile();\n            await downloadSingleFile(file);\n            emitInfo(`已下载文件: ${entry.name}`);\n        }\n        catch (openError: any) {\n            AppLogger.error('[StorageFileExplorer] Failed to open file:', openError);\n            setError(openError?.message ?? '打开文件失败');\n        }\n    };\n\n    const uploadFiles = async (files: File[], targetSegments: string[] = currentSegments) => {\n        if (!files.length) {\n            return;\n        }\n        setLoading(true);\n        setError(null);\n        try {\n            const targetDirHandle = await navigateToPath(rootHandle, targetSegments);\n            const skippedFiles: string[] = [];\n            let modified = false;\n            for (const file of files) {\n                const originalPath = buildPath(targetSegments, file.name);\n                if (isUploadAllowed && !isUploadAllowed(originalPath)) {\n                    skippedFiles.push(file.name);\n                    continue;\n                }\n                let fileName = file.name;\n                if (shouldLowerCaseFile?.(originalPath)) {\n                    fileName = fileName.toLowerCase();\n                }\n                let shouldOverwrite = true;\n                try {\n                    await targetDirHandle.getFileHandle(fileName);\n                    shouldOverwrite = await requestConfirm(`文件 \"${fileName}\" 已存在。是否覆盖？`, '覆盖', '取消');\n                }\n                catch {\n                }\n                if (!shouldOverwrite) {\n                    continue;\n                }\n                const fileHandle = await targetDirHandle.getFileHandle(fileName, { create: true });\n                const writable = await fileHandle.createWritable();\n                try {\n                    await writable.write(file);\n                    await writable.close();\n                    modified = true;\n                }\n                catch (writeError) {\n                    if (typeof (writable as any).abort === 'function') {\n                        await (writable as any).abort();\n                    }\n                    throw writeError;\n                }\n            }\n            if (skippedFiles.length) {\n                await showMessage(`以下文件不允许上传到当前目录:\\n${skippedFiles.join('\\n')}`, '上传已跳过');\n            }\n            if (modified) {\n                emitInfo(`已上传 ${files.length - skippedFiles.length} 个文件`);\n                onFileSystemChange?.();\n                await refresh();\n            }\n        }\n        catch (uploadError: any) {\n            AppLogger.error('[StorageFileExplorer] Failed to upload files:', uploadError);\n            setError(uploadError?.message ?? '上传失败');\n        }\n        finally {\n            setLoading(false);\n        }\n    };\n\n    const handleRename = async (entry?: ExplorerEntry) => {\n        const targetEntry = entry ?? selectedEntries[0];\n        if (!targetEntry) {\n            return;\n        }\n        if (!targetEntry.canModify) {\n            await showMessage('当前项目不允许重命名。');\n            return;\n        }\n        const originalPath = buildPath(currentSegments, targetEntry.id);\n        const nextNameInput = (await requestText(`输入 \"${targetEntry.name}\" 的新名称:`))?.trim();\n        if (!nextNameInput || nextNameInput === targetEntry.id) {\n            return;\n        }\n        const nextName = shouldLowerCaseFile?.(buildPath(currentSegments, nextNameInput))\n            ? nextNameInput.toLowerCase()\n            : nextNameInput;\n        if (targetEntry.type === 'folder') {\n            const validationError = validateNewFolderName?.(nextName, currentPath, currentSegments);\n            if (validationError) {\n                await showMessage(validationError);\n                return;\n            }\n        }\n        try {\n            const currentDirHandle = await navigateToPath(rootHandle, currentSegments);\n            if (await entryExists(currentDirHandle, nextName)) {\n                await showMessage(`\"${nextName}\" 已存在。`);\n                return;\n            }\n            const sourceHandle = targetEntry.type === 'folder'\n                ? await currentDirHandle.getDirectoryHandle(targetEntry.id)\n                : await currentDirHandle.getFileHandle(targetEntry.id);\n            await cloneHandleToDirectory(sourceHandle, currentDirHandle, nextName);\n            await currentDirHandle.removeEntry(targetEntry.id, { recursive: true });\n            emitInfo(`已重命名: ${targetEntry.name} -> ${nextName}`);\n            onFileSystemChange?.();\n            await refresh();\n            focusEntry(nextName);\n        }\n        catch (renameError: any) {\n            AppLogger.error('[StorageFileExplorer] Failed to rename entry:', renameError);\n            setError(renameError?.message ?? '重命名失败');\n        }\n    };\n\n    const handleCreateFolder = async () => {\n        if (canCreateFolder && !canCreateFolder(currentPath, currentSegments)) {\n            await showMessage('当前目录不允许新建文件夹。');\n            return;\n        }\n        const folderName = (await requestText('输入新文件夹名称:'))?.trim();\n        if (!folderName) {\n            return;\n        }\n        const validationError = validateNewFolderName?.(folderName, currentPath, currentSegments);\n        if (validationError) {\n            await showMessage(validationError);\n            return;\n        }\n        try {\n            const currentDirHandle = await navigateToPath(rootHandle, currentSegments);\n            await currentDirHandle.getDirectoryHandle(folderName, { create: true });\n            emitInfo(`已创建文件夹: ${folderName}`);\n            onFileSystemChange?.();\n            await refresh();\n        }\n        catch (createError: any) {\n            AppLogger.error('[StorageFileExplorer] Failed to create folder:', createError);\n            setError(createError?.message ?? '创建文件夹失败');\n        }\n    };\n\n    const handleDelete = async (entriesToDelete: ExplorerEntry[] = selectedEntries) => {\n        if (!entriesToDelete.length) {\n            return;\n        }\n        const systemFiles = entriesToDelete\n            .map((entry) => buildPath(currentSegments, entry.id))\n            .filter((path) => isSystemFile?.(path));\n        if (systemFiles.length) {\n            const confirmedSystemDelete = await requestConfirm(\n                `文件 \"${systemFiles.map((path) => path.split('/').pop()).join(', ')}\" 是系统文件。删除它们可能导致游戏无法正常工作。\\n\\n您确定要继续吗？`,\n                '删除',\n                '取消',\n            );\n            if (!confirmedSystemDelete) {\n                return;\n            }\n        }\n        const confirmedDelete = await requestConfirm(\n            `您确定要永久删除这 ${entriesToDelete.length} 个项目吗？`,\n            '删除',\n            '取消',\n        );\n        if (!confirmedDelete) {\n            return;\n        }\n        try {\n            const currentDirHandle = await navigateToPath(rootHandle, currentSegments);\n            for (const entry of entriesToDelete) {\n                await currentDirHandle.removeEntry(entry.id, { recursive: true });\n            }\n            emitInfo(`已删除 ${entriesToDelete.length} 个项目`);\n            onFileSystemChange?.();\n            await refresh();\n        }\n        catch (deleteError: any) {\n            AppLogger.error('[StorageFileExplorer] Failed to delete entries:', deleteError);\n            setError(deleteError?.message ?? '删除失败');\n        }\n    };\n\n    const handleDownload = async (entriesToDownload: ExplorerEntry[] = selectedEntries) => {\n        if (!entriesToDownload.length) {\n            return;\n        }\n        try {\n            const currentDirHandle = await navigateToPath(rootHandle, currentSegments);\n            if (entriesToDownload.length > 1 || entriesToDownload[0]?.type === 'folder') {\n                if (downloadMultiple) {\n                    await downloadMultiple(currentPath, entriesToDownload);\n                    return;\n                }\n                const zip = new Zip();\n                const appendHandle = async (handle: FileSystemHandle, zipPath: string) => {\n                    if (handle.kind === 'directory') {\n                        for await (const [childName, childHandle] of (handle as FileSystemDirectoryHandle).entries()) {\n                            await appendHandle(childHandle, `${zipPath}/${childName}`);\n                        }\n                        return;\n                    }\n                    const file = await (handle as FileSystemFileHandle).getFile();\n                    zip.startFile(zipPath, new Date(file.lastModified));\n                    const reader = file.stream().getReader();\n                    for (;;) {\n                        const { done, value } = await reader.read();\n                        if (done) {\n                            break;\n                        }\n                        zip.appendData(value);\n                    }\n                    zip.endFile();\n                };\n                for (const entry of entriesToDownload) {\n                    const sourceHandle = entry.type === 'folder'\n                        ? await currentDirHandle.getDirectoryHandle(entry.id)\n                        : await currentDirHandle.getFileHandle(entry.id);\n                    await appendHandle(sourceHandle, entry.id);\n                }\n                zip.finish();\n                const reader = zip.getOutputStream().getReader();\n                const chunks: Uint8Array[] = [];\n                for (;;) {\n                    const { done, value } = await reader.read();\n                    if (done) {\n                        break;\n                    }\n                    chunks.push(value);\n                }\n                await downloadBlob(new Blob(chunks as any, { type: 'application/zip' }), 'cdexport.zip');\n                emitInfo(`已打包下载 ${entriesToDownload.length} 个项目`);\n                return;\n            }\n            const entry = entriesToDownload[0];\n            const fileHandle = await currentDirHandle.getFileHandle(entry.id);\n            const file = await fileHandle.getFile();\n            await downloadSingleFile(file);\n            emitInfo(`已下载文件: ${entry.name}`);\n        }\n        catch (downloadError: any) {\n            AppLogger.error('[StorageFileExplorer] Failed to download entries:', downloadError);\n            if (downloadError?.name !== 'AbortError') {\n                setError(downloadError?.message ?? '下载失败');\n            }\n        }\n    };\n\n    const handleUploadClick = () => {\n        uploadInputRef.current?.click();\n    };\n\n    const handleUploadChange = async (event: React.ChangeEvent<HTMLInputElement>) => {\n        const files = Array.from(event.target.files ?? []);\n        if (!files.length) {\n            return;\n        }\n        await uploadFiles(files);\n        event.target.value = '';\n    };\n\n    const navigateToHistoryIndex = (nextIndex: number) => {\n        const nextEntry = historyEntriesRef.current[nextIndex];\n        if (!nextEntry) {\n            return;\n        }\n        selectionDragRef.current = null;\n        clearSelectionAutoScroll();\n        setSelectionBoxRect(null);\n        setHistoryIndex(nextIndex);\n        setCurrentSegments([...nextEntry.segments]);\n        setSelectedIds(new Set());\n        setFocusedId(null);\n        setStatusMessage('');\n    };\n\n    const handleGoBack = () => {\n        const nextIndex = historyIndexRef.current - 1;\n        if (nextIndex < 0) {\n            return;\n        }\n        navigateToHistoryIndex(nextIndex);\n    };\n\n    const handleGoForward = () => {\n        const nextIndex = historyIndexRef.current + 1;\n        if (nextIndex >= historyEntriesRef.current.length) {\n            return;\n        }\n        navigateToHistoryIndex(nextIndex);\n    };\n\n    const handleBrowserPopState = (event: PopStateEvent) => {\n        const state = event.state as { _fileExplorerCapture?: string; _fileExplorerDirection?: 'back' | 'main' | 'forward' } | null;\n        if (!state || state._fileExplorerCapture !== browserCaptureIdRef.current) {\n            return;\n        }\n        if (state._fileExplorerDirection === 'back') {\n            window.history.forward();\n            handleGoBack();\n            itemsScrollRef.current?.focus();\n        }\n        else if (state._fileExplorerDirection === 'forward') {\n            window.history.back();\n            handleGoForward();\n            itemsScrollRef.current?.focus();\n        }\n    };\n\n    const startBrowserCapture = () => {\n        browserCaptureRefs.current += 1;\n        if (browserCaptureActive.current) {\n            return;\n        }\n        browserCaptureActive.current = true;\n        browserScrollRestoreRef.current = window.history.scrollRestoration;\n        window.history.scrollRestoration = 'manual';\n        window.history.pushState({\n            _fileExplorerCapture: browserCaptureIdRef.current,\n            _fileExplorerDirection: 'back',\n        }, document.title);\n        window.history.scrollRestoration = 'manual';\n        window.history.pushState({\n            _fileExplorerCapture: browserCaptureIdRef.current,\n            _fileExplorerDirection: 'main',\n        }, document.title);\n        window.history.scrollRestoration = 'manual';\n        window.history.pushState({\n            _fileExplorerCapture: browserCaptureIdRef.current,\n            _fileExplorerDirection: 'forward',\n        }, document.title);\n        window.history.scrollRestoration = 'manual';\n        window.history.back();\n        window.addEventListener('popstate', handleBrowserPopState, true);\n    };\n\n    const stopBrowserCapture = () => {\n        if (browserCaptureRefs.current > 0) {\n            browserCaptureRefs.current -= 1;\n        }\n        if (browserCaptureRefs.current > 0 || !browserCaptureActive.current) {\n            return;\n        }\n        browserCaptureActive.current = false;\n        window.removeEventListener('popstate', handleBrowserPopState, true);\n        const state = window.history.state as { _fileExplorerCapture?: string } | null;\n        if (state?._fileExplorerCapture === browserCaptureIdRef.current) {\n            window.history.back();\n        }\n        if (browserScrollRestoreRef.current) {\n            window.history.scrollRestoration = browserScrollRestoreRef.current as ScrollRestoration;\n        }\n    };\n\n    const handleClipboardStage = (mode: 'copy' | 'cut', entriesToStage: ExplorerEntry[] = selectedEntries) => {\n        if (!entriesToStage.length) {\n            return;\n        }\n        setClipboard({\n            mode,\n            sourceSegments: [...currentSegments],\n            items: entriesToStage.map((entry) => ({ id: entry.id, type: entry.type })),\n        });\n        setShowPasteOverlay(true);\n        emitInfo(`${mode === 'copy' ? '已复制' : '已剪切'} ${entriesToStage.length} 个项目`);\n    };\n\n    const transferEntries = async (payload: InternalDragPayload, mode: 'copy' | 'cut', targetSegments: string[]) => {\n        const sourcePath = buildPath(payload.sourceSegments);\n        const targetPath = buildPath(targetSegments);\n        if (mode === 'cut' && sourcePath === targetPath) {\n            emitInfo('源目录与目标目录相同，未执行移动');\n            return;\n        }\n        try {\n            setLoading(true);\n            setError(null);\n            const sourceDirHandle = await navigateToPath(rootHandle, payload.sourceSegments);\n            const targetDirHandle = await navigateToPath(rootHandle, targetSegments);\n            let modified = false;\n            for (const item of payload.items) {\n                const sourceEntryPath = buildPath(payload.sourceSegments, item.id);\n                if (item.type === 'folder' && targetPath.startsWith(`${sourceEntryPath}/`)) {\n                    continue;\n                }\n                const sourceHandle = item.type === 'folder'\n                    ? await sourceDirHandle.getDirectoryHandle(item.id)\n                    : await sourceDirHandle.getFileHandle(item.id);\n                let targetName = item.id;\n                if (mode === 'copy' && sourcePath === targetPath) {\n                    targetName = await getAvailableCopyName(targetDirHandle, item.id);\n                }\n                else if (await entryExists(targetDirHandle, targetName)) {\n                    const overwrite = await requestConfirm(`\"${targetName}\" 已存在。是否覆盖？`, '覆盖', '取消');\n                    if (!overwrite) {\n                        continue;\n                    }\n                    await targetDirHandle.removeEntry(targetName, { recursive: true });\n                }\n                await cloneHandleToDirectory(sourceHandle, targetDirHandle, targetName);\n                if (mode === 'cut') {\n                    await sourceDirHandle.removeEntry(item.id, { recursive: true });\n                }\n                modified = true;\n            }\n            if (modified) {\n                emitInfo(`${mode === 'copy' ? '已粘贴' : '已移动'} ${payload.items.length} 个项目`);\n                onFileSystemChange?.();\n                if (mode === 'cut' && clipboard && buildPath(clipboard.sourceSegments) === sourcePath) {\n                    setClipboard(null);\n                }\n                await refresh();\n            }\n        }\n        catch (pasteError: any) {\n            AppLogger.error('[StorageFileExplorer] Failed to paste entries:', pasteError);\n            setError(pasteError?.message ?? '粘贴失败');\n        }\n        finally {\n            setLoading(false);\n        }\n    };\n\n    const handlePaste = async () => {\n        if (!clipboard?.items.length) {\n            return;\n        }\n        setShowPasteOverlay(false);\n        await transferEntries({\n            sourceSegments: clipboard.sourceSegments,\n            items: clipboard.items,\n        }, clipboard.mode, currentSegments);\n    };\n\n    const handleItemsKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n        if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'a') {\n            event.preventDefault();\n            setSelectedIds(new Set(entries.map((entry) => entry.id)));\n            focusEntry(entries[0]?.id ?? null);\n            return;\n        }\n        if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'c') {\n            event.preventDefault();\n            handleClipboardStage('copy');\n            return;\n        }\n        if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'x') {\n            event.preventDefault();\n            handleClipboardStage('cut');\n            return;\n        }\n        if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'v') {\n            event.preventDefault();\n            void handlePaste();\n            return;\n        }\n        if (event.key === 'Delete' && selectedEntries.length) {\n            event.preventDefault();\n            void handleDelete();\n            return;\n        }\n        if (event.key === 'F2' && selectedEntries.length === 1) {\n            event.preventDefault();\n            void handleRename();\n            return;\n        }\n        if (event.key === 'Enter' && focusedId) {\n            event.preventDefault();\n            const targetEntry = entries.find((entry) => entry.id === focusedId);\n            if (targetEntry) {\n                void openEntry(targetEntry);\n            }\n            return;\n        }\n        const focusedIndex = entries.findIndex((entry) => entry.id === focusedId);\n        const fallbackIndex = selectedEntries.length ? entries.findIndex((entry) => entry.id === selectedEntries[0].id) : 0;\n        const activeIndex = focusedIndex === -1 ? Math.max(0, fallbackIndex) : focusedIndex;\n        const itemNode = entries[activeIndex] ? itemRefs.current.get(entries[activeIndex].id) : null;\n        const itemWidth = itemNode?.offsetWidth || 76;\n        const containerWidth = itemsScrollRef.current?.clientWidth || itemWidth;\n        const columns = Math.max(1, Math.floor(containerWidth / itemWidth));\n        let nextIndex = activeIndex;\n        if (event.key === 'ArrowLeft') {\n            nextIndex = Math.max(0, activeIndex - 1);\n        }\n        else if (event.key === 'ArrowRight') {\n            nextIndex = Math.min(entries.length - 1, activeIndex + 1);\n        }\n        else if (event.key === 'ArrowUp') {\n            nextIndex = Math.max(0, activeIndex - columns);\n        }\n        else if (event.key === 'ArrowDown') {\n            nextIndex = Math.min(entries.length - 1, activeIndex + columns);\n        }\n        else if (event.key === 'Home') {\n            nextIndex = 0;\n        }\n        else if (event.key === 'End') {\n            nextIndex = Math.max(0, entries.length - 1);\n        }\n        if (nextIndex !== activeIndex || ['Home', 'End'].includes(event.key)) {\n            event.preventDefault();\n            const nextEntry = entries[nextIndex];\n            if (nextEntry) {\n                handleSelect(nextEntry.id, event.metaKey || event.ctrlKey, event.shiftKey);\n                focusEntry(nextEntry.id);\n            }\n        }\n    };\n\n    const handlePopupKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n        if (!popupMenu) {\n            return;\n        }\n        if (event.key === 'Escape' || event.key === 'Tab') {\n            event.preventDefault();\n            setPopupMenu(null);\n            return;\n        }\n        if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n            event.preventDefault();\n            const direction = event.key === 'ArrowDown' ? 1 : -1;\n            let nextIndex = popupMenu.focusIndex;\n            for (let count = 0; count < popupMenu.items.length; count += 1) {\n                nextIndex = (nextIndex + direction + popupMenu.items.length) % popupMenu.items.length;\n                if (!popupMenu.items[nextIndex]?.disabled) {\n                    setPopupMenu({ ...popupMenu, focusIndex: nextIndex });\n                    break;\n                }\n            }\n            return;\n        }\n        if (event.key === 'Enter') {\n            event.preventDefault();\n            void activatePopupItem(popupMenu.focusIndex);\n        }\n    };\n\n    const updateSelectionBox = (clientX: number, clientY: number) => {\n        const dragState = selectionDragRef.current;\n        const itemsScroll = itemsScrollRef.current;\n        if (!dragState || !itemsScroll) {\n            return;\n        }\n        const scrollRect = itemsScroll.getBoundingClientRect();\n        const nextClientX = Math.max(scrollRect.left, Math.min(clientX, scrollRect.right));\n        const nextClientY = Math.max(scrollRect.top, Math.min(clientY, scrollRect.bottom));\n        const nextContentX = nextClientX - scrollRect.left + itemsScroll.scrollLeft;\n        const nextContentY = nextClientY - scrollRect.top + itemsScroll.scrollTop;\n        const rectLeft = Math.min(dragState.anchorContentX, nextContentX);\n        const rectTop = Math.min(dragState.anchorContentY, nextContentY);\n        const rectWidth = Math.abs(nextContentX - dragState.anchorContentX);\n        const rectHeight = Math.abs(nextContentY - dragState.anchorContentY);\n        setSelectionBoxRect({\n            left: rectLeft,\n            top: rectTop,\n            width: rectWidth,\n            height: rectHeight,\n        });\n        const clientRect = {\n            left: Math.min(dragState.anchorClientX, nextClientX),\n            right: Math.max(dragState.anchorClientX, nextClientX),\n            top: Math.min(dragState.anchorClientY, nextClientY),\n            bottom: Math.max(dragState.anchorClientY, nextClientY),\n        };\n        const nextSelectedIds = dragState.additive ? new Set(dragState.baseSelectedIds) : new Set<string>();\n        for (const entry of entries) {\n            const node = itemRefs.current.get(entry.id);\n            if (!node) {\n                continue;\n            }\n            const nodeRect = node.getBoundingClientRect();\n            const intersects = clientRect.left <= nodeRect.right &&\n                clientRect.right >= nodeRect.left &&\n                clientRect.top <= nodeRect.bottom &&\n                clientRect.bottom >= nodeRect.top;\n            if (intersects) {\n                nextSelectedIds.add(entry.id);\n            }\n        }\n        setSelectedIds(nextSelectedIds);\n        setFocusedId(nextSelectedIds.size === 1 ? Array.from(nextSelectedIds)[0] : null);\n    };\n\n    const stopSelectionBox = (clientX: number, clientY: number) => {\n        const dragState = selectionDragRef.current;\n        if (!dragState) {\n            return;\n        }\n        clearSelectionAutoScroll();\n        const dx = Math.abs(clientX - dragState.anchorClientX);\n        const dy = Math.abs(clientY - dragState.anchorClientY);\n        const moved = dragState.moved || dx > 4 || dy > 4;\n        selectionDragRef.current = null;\n        setSelectionBoxRect(null);\n        if (!moved && !dragState.additive) {\n            setSelectedIds(new Set());\n            setFocusedId(null);\n        }\n    };\n\n    const handleItemsMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {\n        if (event.button !== 0) {\n            return;\n        }\n        const target = event.target as HTMLElement | null;\n        if (target?.closest('.fe_fileexplorer_item_wrap_inner, .fe_fileexplorer_popup_wrap')) {\n            return;\n        }\n        const itemsScroll = itemsScrollRef.current;\n        if (!itemsScroll) {\n            return;\n        }\n        event.preventDefault();\n        setPopupMenu(null);\n        const scrollRect = itemsScroll.getBoundingClientRect();\n        clearSelectionAutoScroll();\n        selectionAutoScrollRef.current.lastClientX = event.clientX;\n        selectionAutoScrollRef.current.lastClientY = event.clientY;\n        selectionDragRef.current = {\n            anchorClientX: event.clientX,\n            anchorClientY: event.clientY,\n            anchorContentX: event.clientX - scrollRect.left + itemsScroll.scrollLeft,\n            anchorContentY: event.clientY - scrollRect.top + itemsScroll.scrollTop,\n            additive: event.metaKey || event.ctrlKey,\n            baseSelectedIds: event.metaKey || event.ctrlKey ? new Set(selectedIds) : new Set<string>(),\n            moved: false,\n        };\n        const handleWindowMouseMove = (moveEvent: MouseEvent) => {\n            const dragState = selectionDragRef.current;\n            if (!dragState) {\n                return;\n            }\n            const dx = Math.abs(moveEvent.clientX - dragState.anchorClientX);\n            const dy = Math.abs(moveEvent.clientY - dragState.anchorClientY);\n            if (!dragState.moved && (dx > 4 || dy > 4)) {\n                dragState.moved = true;\n            }\n            updateSelectionBox(moveEvent.clientX, moveEvent.clientY);\n            syncSelectionAutoScroll(moveEvent.clientX, moveEvent.clientY);\n        };\n        const handleWindowMouseUp = (upEvent: MouseEvent) => {\n            window.removeEventListener('mousemove', handleWindowMouseMove, true);\n            window.removeEventListener('mouseup', handleWindowMouseUp, true);\n            window.removeEventListener('blur', handleWindowBlur, true);\n            stopSelectionBox(upEvent.clientX, upEvent.clientY);\n        };\n        const handleWindowBlur = () => {\n            window.removeEventListener('mousemove', handleWindowMouseMove, true);\n            window.removeEventListener('mouseup', handleWindowMouseUp, true);\n            window.removeEventListener('blur', handleWindowBlur, true);\n            stopSelectionBox(selectionAutoScrollRef.current.lastClientX || event.clientX, selectionAutoScrollRef.current.lastClientY || event.clientY);\n        };\n        window.addEventListener('mousemove', handleWindowMouseMove, true);\n        window.addEventListener('mouseup', handleWindowMouseUp, true);\n        window.addEventListener('blur', handleWindowBlur, true);\n    };\n\n    const openHistoryMenu = () => {\n        const anchorRect = navHistoryRef.current?.getBoundingClientRect();\n        if (!anchorRect) {\n            return;\n        }\n        let minIndex = Math.max(0, historyIndex - 4);\n        let maxIndex = Math.min(historyEntries.length - 1, historyIndex + 4);\n        if (maxIndex - minIndex < 8) {\n            minIndex = Math.max(0, Math.min(minIndex, historyEntries.length - 9));\n            maxIndex = Math.min(historyEntries.length - 1, minIndex + 8);\n        }\n        const items = historyEntries\n            .slice(minIndex, maxIndex + 1)\n            .map((entry, offset) => {\n                const index = minIndex + offset;\n                return {\n                    id: `history-${index}`,\n                    label: entry.segments[entry.segments.length - 1] ?? rootLabel,\n                    active: index === historyIndex,\n                    iconClass: index < historyIndex\n                        ? 'fe_fileexplorer_popup_item_icon_back'\n                        : index > historyIndex\n                            ? 'fe_fileexplorer_popup_item_icon_forward'\n                            : 'fe_fileexplorer_popup_item_icon_check',\n                    onSelect: () => {\n                        setHistoryIndex(index);\n                        setCurrentSegments([...entry.segments]);\n                        setSelectedIds(new Set());\n                        setFocusedId(null);\n                        setStatusMessage('');\n                    },\n                };\n            })\n            .reverse();\n        openPopupMenu(items, Math.round(anchorRect.left), Math.round(anchorRect.bottom + 1));\n    };\n\n    const openPathSegmentMenu = async (segment: PathSegmentEntry, anchorElement: HTMLElement | null) => {\n        if (!anchorElement) {\n            return;\n        }\n        try {\n            const dirHandle = await navigateToPath(rootHandle, segment.segments);\n            const items: PopupMenuItem[] = [];\n            const activeChildId = currentSegments[segment.index + 1];\n            for await (const [name, handle] of dirHandle.entries()) {\n                if (handle.kind !== 'directory') {\n                    continue;\n                }\n                items.push({\n                    id: `${segment.index}:${name}`,\n                    label: name,\n                    iconClass: 'fe_fileexplorer_popup_item_icon_folder',\n                    active: activeChildId === name,\n                    onSelect: () => navigateToSegments([...segment.segments, name]),\n                });\n            }\n            items.sort((left, right) => left.label.localeCompare(right.label));\n            if (!items.length) {\n                items.push({\n                    id: `${segment.index}:empty`,\n                    label: '没有子文件夹',\n                    disabled: true,\n                    onSelect: () => undefined,\n                });\n            }\n            const rect = anchorElement.getBoundingClientRect();\n            openPopupMenu(items, Math.round(rect.left), Math.round(rect.bottom + 1));\n        }\n        catch (popupError: any) {\n            AppLogger.error('[StorageFileExplorer] Failed to open path segment menu:', popupError);\n            setError(popupError?.message ?? '打开路径菜单失败');\n        }\n    };\n\n    const openBackgroundMenu = (x: number, y: number) => {\n        openPopupMenu([\n            {\n                id: 'refresh',\n                label: '刷新',\n                onSelect: () => refresh(),\n            },\n            {\n                id: 'new-folder',\n                label: '新建文件夹',\n                iconClass: 'fe_fileexplorer_popup_item_icon_folder',\n                disabled: !!(canCreateFolder && !canCreateFolder(currentPath, currentSegments)),\n                onSelect: () => handleCreateFolder(),\n            },\n            {\n                id: 'upload',\n                label: '上传文件',\n                onSelect: () => handleUploadClick(),\n            },\n            {\n                id: 'paste',\n                label: '粘贴',\n                iconClass: 'fe_fileexplorer_popup_item_icon_paste',\n                disabled: !clipboard?.items.length,\n                onSelect: () => handlePaste(),\n            },\n            {\n                id: 'select-all',\n                label: '全选',\n                separatorBefore: true,\n                disabled: !entries.length,\n                onSelect: () => {\n                    setSelectedIds(new Set(entries.map((entry) => entry.id)));\n                    focusEntry(entries[0]?.id ?? null);\n                },\n            },\n        ], x, y);\n    };\n\n    const openItemMenu = (entry: ExplorerEntry, x: number, y: number) => {\n        const contextEntries = selectedIds.has(entry.id) && selectedEntries.length ? selectedEntries : [entry];\n        const singleContextEntry = contextEntries.length === 1 ? contextEntries[0] : undefined;\n        openPopupMenu([\n            {\n                id: 'open',\n                label: entry.type === 'folder' ? '打开' : '打开/下载',\n                iconClass: entry.type === 'folder' ? 'fe_fileexplorer_popup_item_icon_folder' : 'fe_fileexplorer_popup_item_icon_file',\n                onSelect: () => openEntry(entry),\n            },\n            {\n                id: 'copy',\n                label: '复制',\n                iconClass: 'fe_fileexplorer_popup_item_icon_copy',\n                onSelect: () => handleClipboardStage('copy', contextEntries),\n            },\n            {\n                id: 'cut',\n                label: '剪切',\n                iconClass: 'fe_fileexplorer_popup_item_icon_cut',\n                disabled: contextEntries.some((item) => !item.canModify),\n                onSelect: () => handleClipboardStage('cut', contextEntries),\n            },\n            {\n                id: 'rename',\n                label: '重命名',\n                separatorBefore: true,\n                disabled: !singleContextEntry || !singleContextEntry.canModify,\n                onSelect: () => handleRename(singleContextEntry),\n            },\n            {\n                id: 'download',\n                label: '下载',\n                iconClass: 'fe_fileexplorer_popup_item_icon_download',\n                onSelect: () => handleDownload(contextEntries),\n            },\n            {\n                id: 'delete',\n                label: '删除',\n                iconClass: 'fe_fileexplorer_popup_item_icon_delete',\n                disabled: contextEntries.some((item) => !item.canModify),\n                onSelect: () => handleDelete(contextEntries),\n            },\n        ], x, y);\n    };\n\n    const createDragImage = (entry: ExplorerEntry, count: number) => {\n        dragImageRef.current?.remove();\n        const dragImage = document.createElement('div');\n        dragImage.className = 'fe_fileexplorer_floating_drag_icon_wrap';\n        const inner = document.createElement('div');\n        inner.className = 'fe_fileexplorer_floating_drag_icon_wrap_inner';\n        if (count > 1) {\n            inner.dataset.numitems = String(count);\n        }\n        const icon = document.createElement('div');\n        icon.className = `fe_fileexplorer_item_icon ${entry.type === 'folder' ? 'fe_fileexplorer_item_icon_folder' : 'fe_fileexplorer_item_icon_file'}`;\n        inner.appendChild(icon);\n        dragImage.appendChild(inner);\n        document.body.appendChild(dragImage);\n        dragImageRef.current = dragImage;\n        return dragImage;\n    };\n\n    const clearDragImage = () => {\n        dragImageRef.current?.remove();\n        dragImageRef.current = null;\n    };\n\n    const handleClipboardEvent = (event: React.ClipboardEvent<HTMLDivElement>, mode: 'copy' | 'cut') => {\n        const target = event.target as HTMLElement | null;\n        if (target?.closest('input, textarea, [contenteditable=\"true\"]')) {\n            return;\n        }\n        if (!selectedEntries.length) {\n            return;\n        }\n        const payload: ExplorerClipboard = {\n            mode,\n            sourceSegments: [...currentSegments],\n            items: selectedEntries.map((entry) => ({ id: entry.id, type: entry.type })),\n        };\n        setClipboard(payload);\n        setShowPasteOverlay(true);\n        event.preventDefault();\n        event.clipboardData.setData('application/x-ra2-fileexplorer-clipboard', JSON.stringify(payload));\n        event.clipboardData.setData('text/plain', JSON.stringify({\n            'application/x-ra2-fileexplorer-clipboard': payload,\n        }));\n        emitInfo(`${mode === 'copy' ? '已复制' : '已剪切'} ${selectedEntries.length} 个项目`);\n    };\n\n    const handlePasteEvent = (event: React.ClipboardEvent<HTMLDivElement>) => {\n        const target = event.target as HTMLElement | null;\n        if (target?.closest('input, textarea, [contenteditable=\"true\"]')) {\n            return;\n        }\n        const payload = parseClipboardPayload(event.clipboardData) ?? clipboard;\n        if (!payload?.items.length) {\n            return;\n        }\n        event.preventDefault();\n        setClipboard(payload);\n        setShowPasteOverlay(false);\n        void transferEntries({\n            sourceSegments: payload.sourceSegments,\n            items: payload.items,\n        }, payload.mode, currentSegments);\n    };\n\n    const breadcrumbSegments: PathSegmentEntry[] = [\n        { label: rootLabel, index: -1, segments: [] },\n        ...currentSegments.map((segment, index) => ({\n            label: segment,\n            index,\n            segments: currentSegments.slice(0, index + 1),\n        })),\n    ];\n    const focusBreadcrumbPosition = (position: number, target: 'name' | 'opts' = 'name') => {\n        const segment = breadcrumbSegments[position];\n        if (!segment) {\n            return;\n        }\n        focusPathSegmentByIndex(segment.index, target);\n    };\n    const handlePathSegmentKeyDown = (\n        event: React.KeyboardEvent<HTMLButtonElement>,\n        segment: PathSegmentEntry,\n        position: number,\n        target: 'name' | 'opts',\n    ) => {\n        if (event.key === 'ArrowLeft') {\n            if (position > 0) {\n                event.preventDefault();\n                focusBreadcrumbPosition(position - 1);\n            }\n            return;\n        }\n        if (event.key === 'ArrowRight') {\n            if (position < breadcrumbSegments.length - 1) {\n                event.preventDefault();\n                focusBreadcrumbPosition(position + 1);\n            }\n            return;\n        }\n        if (event.key === 'ArrowDown') {\n            event.preventDefault();\n            const anchorNode = pathOptionRefs.current.get(segment.index) ?? pathNameRefs.current.get(segment.index) ?? event.currentTarget;\n            focusPathSegmentByIndex(segment.index, target === 'opts' ? 'opts' : 'name');\n            void openPathSegmentMenu(segment, anchorNode);\n            return;\n        }\n        if (target === 'opts' && (event.key === 'Enter' || event.key === ' ')) {\n            event.preventDefault();\n            void openPathSegmentMenu(segment, event.currentTarget);\n        }\n    };\n    const pasteShortcutLabel = typeof navigator !== 'undefined' && /mac/i.test(navigator.platform) ? 'Cmd+V' : 'Ctrl+V';\n    const statusSegments = [\n        selectedEntries.length ? `已选择 ${selectedEntries.length} 项` : `项目 ${entries.length} 个`,\n        currentPath,\n        clipboard ? `${clipboard.mode === 'copy' ? '复制板' : '剪切板'}: ${clipboard.items.length} 项` : '',\n        error || statusMessage || '',\n    ].filter(Boolean);\n\n    return (\n        <div\n            ref={rootRef}\n            className={`fe_fileexplorer_wrap${loading ? ' fe_fileexplorer_operation_in_progress' : ''}`}\n            onMouseEnter={startBrowserCapture}\n            onMouseLeave={stopBrowserCapture}\n            onFocusCapture={startBrowserCapture}\n            onBlurCapture={(event) => {\n                const nextTarget = event.relatedTarget as Node | null;\n                if (nextTarget && rootRef.current?.contains(nextTarget)) {\n                    return;\n                }\n                stopBrowserCapture();\n            }}\n            onCopy={(event) => handleClipboardEvent(event, 'copy')}\n            onCut={(event) => handleClipboardEvent(event, 'cut')}\n            onPaste={handlePasteEvent}\n            onContextMenu={(event) => {\n                if ((event.target as HTMLElement).closest('.fe_fileexplorer_item_wrap_inner, .fe_fileexplorer_popup_wrap')) {\n                    return;\n                }\n                event.preventDefault();\n                openBackgroundMenu(event.clientX, event.clientY);\n            }}\n        >\n            <div className={`fe_fileexplorer_inner_wrap fe_fileexplorer_inner_wrap_focused${showCheckboxes ? ' fe_fileexplorer_show_item_checkboxes' : ''}`}>\n                <div className=\"fe_fileexplorer_toolbar\">\n                    <div className=\"fe_fileexplorer_navtools\">\n                        <button\n                            type=\"button\"\n                            className={`fe_fileexplorer_navtool_back${historyIndex < 1 ? ' fe_fileexplorer_disabled' : ''}`}\n                            onClick={handleGoBack}\n                            disabled={historyIndex < 1 || loading}\n                            title=\"后退\"\n                            aria-label=\"后退\"\n                        />\n                        <button\n                            type=\"button\"\n                            className={`fe_fileexplorer_navtool_forward${historyIndex >= historyEntries.length - 1 ? ' fe_fileexplorer_disabled' : ''}`}\n                            onClick={handleGoForward}\n                            disabled={historyIndex >= historyEntries.length - 1 || loading}\n                            title=\"前进\"\n                            aria-label=\"前进\"\n                        />\n                        <button\n                            ref={navHistoryRef}\n                            type=\"button\"\n                            className=\"fe_fileexplorer_navtool_history\"\n                            onMouseDown={(event) => {\n                                event.preventDefault();\n                                openHistoryMenu();\n                            }}\n                            onKeyDown={(event) => {\n                                if (event.key === 'Enter' || event.key === ' ') {\n                                    event.preventDefault();\n                                    openHistoryMenu();\n                                }\n                            }}\n                            title=\"最近访问\"\n                            aria-label=\"最近访问\"\n                        />\n                        <button\n                            type=\"button\"\n                            className={`fe_fileexplorer_navtool_up${currentSegments.length < 1 ? ' fe_fileexplorer_disabled' : ''}`}\n                            onClick={() => navigateToSegments(currentSegments.slice(0, -1))}\n                            disabled={currentSegments.length < 1 || loading}\n                            title=\"上一级\"\n                            aria-label=\"上一级\"\n                        />\n                    </div>\n                    <div className=\"fe_fileexplorer_path_wrap\">\n                        <div className=\"fe_fileexplorer_path_icon\">\n                            <div className=\"fe_fileexplorer_path_icon_inner\" />\n                        </div>\n                        <div className=\"fe_fileexplorer_path_segments_scroll_wrap\">\n                            <div className=\"fe_fileexplorer_path_segments_wrap\">\n                                {breadcrumbSegments.map((segment, position) => (\n                                    <div\n                                        key={`${segment.label}-${segment.index}`}\n                                        className={`fe_fileexplorer_path_segment_wrap${dragPathHoverIndex === segment.index ? ' fe_fileexplorer_drag_hover' : ''}`}\n                                        onDragOver={(event) => {\n                                            const hasInternal = parseInternalDragPayload(event.dataTransfer);\n                                            const hasFiles = Array.from(event.dataTransfer?.items ?? []).some((item) => item.kind === 'file');\n                                            if (!hasInternal && !hasFiles) {\n                                                return;\n                                            }\n                                            event.preventDefault();\n                                            setDragPathHoverIndex(segment.index);\n                                            event.dataTransfer.dropEffect = event.ctrlKey || event.metaKey ? 'copy' : 'move';\n                                        }}\n                                        onDragLeave={() => {\n                                            if (dragPathHoverIndex === segment.index) {\n                                                setDragPathHoverIndex(null);\n                                            }\n                                        }}\n                                        onDrop={(event) => {\n                                            event.preventDefault();\n                                            setDragActive(false);\n                                            setDragPathHoverIndex(null);\n                                            const targetSegments = segment.segments;\n                                            const internalPayload = parseInternalDragPayload(event.dataTransfer);\n                                            if (internalPayload) {\n                                                void transferEntries(internalPayload, event.ctrlKey || event.metaKey ? 'copy' : 'cut', targetSegments);\n                                                return;\n                                            }\n                                            void uploadFiles(Array.from(event.dataTransfer.files ?? []), targetSegments);\n                                        }}\n                                    >\n                                        <button\n                                            type=\"button\"\n                                            className=\"fe_fileexplorer_path_name\"\n                                            ref={(node) => {\n                                                if (node) {\n                                                    pathNameRefs.current.set(segment.index, node);\n                                                }\n                                                else {\n                                                    pathNameRefs.current.delete(segment.index);\n                                                }\n                                            }}\n                                            onClick={() => navigateToSegments(segment.segments)}\n                                            onFocus={(event) => event.currentTarget.scrollIntoView({ block: 'nearest', inline: 'nearest' })}\n                                            onKeyDown={(event) => handlePathSegmentKeyDown(event, segment, position, 'name')}\n                                            disabled={loading}\n                                        >\n                                            {segment.label}\n                                        </button>\n                                        <button\n                                            type=\"button\"\n                                            className=\"fe_fileexplorer_path_opts\"\n                                            ref={(node) => {\n                                                if (node) {\n                                                    pathOptionRefs.current.set(segment.index, node);\n                                                }\n                                                else {\n                                                    pathOptionRefs.current.delete(segment.index);\n                                                }\n                                            }}\n                                            onMouseDown={(event) => {\n                                                event.preventDefault();\n                                                void openPathSegmentMenu(segment, event.currentTarget);\n                                            }}\n                                            onFocus={(event) => event.currentTarget.scrollIntoView({ block: 'nearest', inline: 'nearest' })}\n                                            onKeyDown={(event) => handlePathSegmentKeyDown(event, segment, position, 'opts')}\n                                            disabled={loading}\n                                            title={`浏览 ${segment.label} 下的文件夹`}\n                                            aria-label={`浏览 ${segment.label} 下的文件夹`}\n                                        />\n                                    </div>\n                                ))}\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <input ref={uploadInputRef} type=\"file\" multiple style={{ display: 'none' }} onChange={handleUploadChange} />\n                <div className=\"fe_fileexplorer_body_wrap_outer\">\n                    <div className=\"fe_fileexplorer_body_wrap\">\n                        <div className=\"fe_fileexplorer_folder_tools_scroll_wrap\">\n                            <div className=\"fe_fileexplorer_folder_tools\">\n                                <button\n                                    type=\"button\"\n                                    className=\"fe_fileexplorer_folder_tool_new_folder\"\n                                    onClick={() => void handleCreateFolder()}\n                                    disabled={loading}\n                                    title=\"新建文件夹\"\n                                    aria-label=\"新建文件夹\"\n                                />\n                                <button\n                                    type=\"button\"\n                                    className=\"fe_fileexplorer_folder_tool_upload\"\n                                    onClick={handleUploadClick}\n                                    disabled={loading}\n                                    title=\"上传\"\n                                    aria-label=\"上传\"\n                                />\n                                <button\n                                    type=\"button\"\n                                    className={`fe_fileexplorer_folder_tool_download${selectedEntries.length ? '' : ' fe_fileexplorer_disabled'}`}\n                                    onClick={() => void handleDownload()}\n                                    disabled={loading || !selectedEntries.length}\n                                    title=\"下载\"\n                                    aria-label=\"下载\"\n                                />\n                                <button\n                                    type=\"button\"\n                                    className={`fe_fileexplorer_folder_tool_copy${selectedEntries.length ? '' : ' fe_fileexplorer_disabled'}`}\n                                    onClick={() => handleClipboardStage('copy')}\n                                    disabled={loading || !selectedEntries.length}\n                                    title=\"复制\"\n                                    aria-label=\"复制\"\n                                />\n                                <button\n                                    type=\"button\"\n                                    className={`fe_fileexplorer_folder_tool_paste${clipboard?.items.length ? '' : ' fe_fileexplorer_disabled'}`}\n                                    onClick={() => void handlePaste()}\n                                    disabled={loading || !clipboard?.items.length}\n                                    title=\"粘贴\"\n                                    aria-label=\"粘贴\"\n                                />\n                                <button\n                                    type=\"button\"\n                                    className={`fe_fileexplorer_folder_tool_cut${selectedEntries.length ? '' : ' fe_fileexplorer_disabled'}`}\n                                    onClick={() => handleClipboardStage('cut')}\n                                    disabled={loading || !selectedEntries.length}\n                                    title=\"剪切\"\n                                    aria-label=\"剪切\"\n                                />\n                                <button\n                                    type=\"button\"\n                                    className={`fe_fileexplorer_folder_tool_delete${selectedEntries.length ? '' : ' fe_fileexplorer_disabled'}`}\n                                    onClick={() => void handleDelete()}\n                                    disabled={loading || !selectedEntries.length}\n                                    title=\"删除\"\n                                    aria-label=\"删除\"\n                                />\n                                <div className=\"fe_fileexplorer_folder_tool_separator\" />\n                                <button\n                                    type=\"button\"\n                                    className=\"fe_fileexplorer_folder_tool_item_checkboxes\"\n                                    onClick={() => setShowCheckboxes((value) => !value)}\n                                    disabled={loading}\n                                    title=\"切换复选框\"\n                                    aria-label=\"切换复选框\"\n                                />\n                            </div>\n                        </div>\n                        <div\n                            ref={itemsScrollRef}\n                            className={`fe_fileexplorer_items_scroll_wrap${dragActive ? ' fe_fileexplorer_items_scroll_wrap_drag_active' : ''}`}\n                            tabIndex={0}\n                            onKeyDown={handleItemsKeyDown}\n                            onMouseDown={handleItemsMouseDown}\n                            onDragEnter={(event) => {\n                                const internalPayload = parseInternalDragPayload(event.dataTransfer);\n                                if (internalPayload) {\n                                    event.preventDefault();\n                                    return;\n                                }\n                                if (Array.from(event.dataTransfer?.items ?? []).some((item) => item.kind === 'file')) {\n                                    event.preventDefault();\n                                    setDragActive(true);\n                                }\n                            }}\n                            onDragOver={(event) => {\n                                const internalPayload = parseInternalDragPayload(event.dataTransfer);\n                                if (internalPayload) {\n                                    event.preventDefault();\n                                    event.dataTransfer.dropEffect = event.ctrlKey || event.metaKey ? 'copy' : 'move';\n                                    return;\n                                }\n                                if (Array.from(event.dataTransfer?.items ?? []).some((item) => item.kind === 'file')) {\n                                    event.preventDefault();\n                                    setDragActive(true);\n                                }\n                            }}\n                            onDragLeave={(event) => {\n                                if (event.currentTarget === event.target) {\n                                    setDragActive(false);\n                                }\n                                setDragPathHoverIndex(null);\n                            }}\n                            onDrop={(event) => {\n                                event.preventDefault();\n                                setDragActive(false);\n                                setDragPathHoverIndex(null);\n                                const internalPayload = parseInternalDragPayload(event.dataTransfer);\n                                if (internalPayload) {\n                                    void transferEntries(internalPayload, event.ctrlKey || event.metaKey ? 'copy' : 'cut', currentSegments);\n                                    return;\n                                }\n                                void uploadFiles(Array.from(event.dataTransfer.files ?? []));\n                            }}\n                        >\n                            <div className=\"fe_fileexplorer_items_scroll_wrap_inner\">\n                                {selectionBoxRect ? (\n                                    <div\n                                        className=\"fe_fileexplorer_select_box\"\n                                        style={{\n                                            left: `${selectionBoxRect.left}px`,\n                                            top: `${selectionBoxRect.top}px`,\n                                            width: `${selectionBoxRect.width}px`,\n                                            height: `${selectionBoxRect.height}px`,\n                                        }}\n                                    />\n                                ) : null}\n                                {showPasteOverlay && clipboard?.items.length && !loading ? (\n                                    <div\n                                        className=\"fe_fileexplorer_items_clipboard_overlay_paste_wrap fe_fileexplorer_items_show_clipboard_overlay_paste\"\n                                        tabIndex={0}\n                                        onClick={() => itemsScrollRef.current?.focus()}\n                                        onKeyDown={(event) => {\n                                            if (event.key === 'Escape') {\n                                                setShowPasteOverlay(false);\n                                            }\n                                        }}\n                                        onContextMenu={(event) => {\n                                            event.preventDefault();\n                                            openBackgroundMenu(event.clientX, event.clientY);\n                                        }}\n                                    >\n                                        <div className=\"fe_fileexplorer_items_clipboard_overlay_paste_inner_wrap\">\n                                            <div className=\"fe_fileexplorer_items_clipboard_overlay_paste_text_wrap\">\n                                                <div className=\"fe_fileexplorer_items_clipboard_overlay_paste_text\">\n                                                    <div className=\"fe_fileexplorer_items_clipboard_overlay_paste_text_big\">\n                                                        按 {pasteShortcutLabel} 粘贴到当前目录\n                                                    </div>\n                                                    <div className=\"fe_fileexplorer_items_clipboard_overlay_paste_text_small\">\n                                                        或右键打开粘贴菜单\n                                                    </div>\n                                                </div>\n                                            </div>\n                                        </div>\n                                    </div>\n                                ) : null}\n                                {dragActive ? (\n                                    <div className=\"fe_fileexplorer_drop_message\">释放文件以上传到当前目录</div>\n                                ) : null}\n                                {loading ? (\n                                    <div className=\"fe_fileexplorer_items_message_wrap\">{loadingLabel ?? '加载中...'}</div>\n                                ) : entries.length ? (\n                                    <div className=\"fe_fileexplorer_items_wrap fe_fileexplorer_items_focus\">\n                                        {entries.map((entry) => {\n                                            const selected = selectedIds.has(entry.id);\n                                            const extLabel = getFileExtLabel(entry);\n                                            return (\n                                                <div\n                                                    key={entry.id}\n                                                    className={`fe_fileexplorer_item_wrap${entry.type === 'folder' ? ' fe_fileexplorer_item_folder' : ''}${selected ? ' fe_fileexplorer_item_selected' : ''}${dragFolderHoverId === entry.id ? ' fe_fileexplorer_drag_hover' : ''}`}\n                                                >\n                                                    <div\n                                                        className=\"fe_fileexplorer_item_wrap_inner\"\n                                                        role=\"button\"\n                                                        tabIndex={0}\n                                                        ref={(node) => {\n                                                            if (node) {\n                                                                itemRefs.current.set(entry.id, node);\n                                                            }\n                                                            else {\n                                                                itemRefs.current.delete(entry.id);\n                                                            }\n                                                        }}\n                                                        onClick={(event) => handleSelect(entry.id, event.metaKey || event.ctrlKey, event.shiftKey)}\n                                                        onDoubleClick={() => void openEntry(entry)}\n                                                        onFocus={() => setFocusedId(entry.id)}\n                                                        onKeyDown={(event) => {\n                                                            if (event.key === 'Enter') {\n                                                                event.preventDefault();\n                                                                void openEntry(entry);\n                                                            }\n                                                        }}\n                                                        title={entry.name}\n                                                        draggable\n                                                        onDragStart={(event) => {\n                                                            const dragEntries = selectedIds.has(entry.id) ? selectedEntries : [entry];\n                                                            if (!selectedIds.has(entry.id)) {\n                                                                setSelectedIds(new Set([entry.id]));\n                                                                setFocusedId(entry.id);\n                                                            }\n                                                            const payload: InternalDragPayload = {\n                                                                sourceSegments: [...currentSegments],\n                                                                items: dragEntries.map((item) => ({ id: item.id, type: item.type })),\n                                                            };\n                                                            event.dataTransfer.setData('application/x-ra2-fileexplorer-items', JSON.stringify(payload));\n                                                            event.dataTransfer.effectAllowed = 'copyMove';\n                                                            const dragImage = createDragImage(entry, dragEntries.length);\n                                                            event.dataTransfer.setDragImage(dragImage, 24, 40);\n                                                        }}\n                                                        onDragEnd={() => {\n                                                            clearDragImage();\n                                                            setDragFolderHoverId(null);\n                                                            setDragPathHoverIndex(null);\n                                                        }}\n                                                        onContextMenu={(event) => {\n                                                            event.preventDefault();\n                                                            if (!selectedIds.has(entry.id)) {\n                                                                setSelectedIds(new Set([entry.id]));\n                                                                setFocusedId(entry.id);\n                                                            }\n                                                            openItemMenu(entry, event.clientX, event.clientY);\n                                                        }}\n                                                        onDragOver={(event) => {\n                                                            if (entry.type !== 'folder') {\n                                                                return;\n                                                            }\n                                                            const internalPayload = parseInternalDragPayload(event.dataTransfer);\n                                                            if (internalPayload) {\n                                                                event.preventDefault();\n                                                                setDragFolderHoverId(entry.id);\n                                                                event.dataTransfer.dropEffect = event.ctrlKey || event.metaKey ? 'copy' : 'move';\n                                                                return;\n                                                            }\n                                                            if (Array.from(event.dataTransfer?.items ?? []).some((item) => item.kind === 'file')) {\n                                                                event.preventDefault();\n                                                                setDragFolderHoverId(entry.id);\n                                                            }\n                                                        }}\n                                                        onDragLeave={() => {\n                                                            if (dragFolderHoverId === entry.id) {\n                                                                setDragFolderHoverId(null);\n                                                            }\n                                                        }}\n                                                        onDrop={(event) => {\n                                                            if (entry.type !== 'folder') {\n                                                                return;\n                                                            }\n                                                            event.preventDefault();\n                                                            setDragActive(false);\n                                                            setDragFolderHoverId(null);\n                                                            const internalPayload = parseInternalDragPayload(event.dataTransfer);\n                                                            if (internalPayload) {\n                                                                void transferEntries(internalPayload, event.ctrlKey || event.metaKey ? 'copy' : 'cut', [...currentSegments, entry.id]);\n                                                                return;\n                                                            }\n                                                            void uploadFiles(Array.from(event.dataTransfer.files ?? []), [...currentSegments, entry.id]);\n                                                        }}\n                                                    >\n                                                        <input\n                                                            type=\"checkbox\"\n                                                            className=\"fe_fileexplorer_item_checkbox\"\n                                                            checked={selected}\n                                                            onChange={(event) => {\n                                                                event.stopPropagation();\n                                                                handleSelect(entry.id, true);\n                                                            }}\n                                                            onClick={(event) => event.stopPropagation()}\n                                                        />\n                                                        <div\n                                                            className={`fe_fileexplorer_item_icon ${getFileIconClass(entry)}`}\n                                                            data-ext={extLabel}\n                                                        >\n                                                            {entry.type === 'file' ? null : null}\n                                                        </div>\n                                                        <div className=\"fe_fileexplorer_item_text\">{entry.name}</div>\n                                                    </div>\n                                                </div>\n                                            );\n                                        })}\n                                    </div>\n                                ) : (\n                                    <div className=\"fe_fileexplorer_items_message_wrap\">\n                                        {emptyState ?? '当前目录为空。'}\n                                    </div>\n                                )}\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <div className={`fe_fileexplorer_statusbar_wrap${statusSegments.length > 2 ? ' fe_fileexplorer_statusbar_wrap_multiline' : ''}`}>\n                    <div className=\"fe_fileexplorer_statusbar_text_wrap\">\n                        {statusSegments.map((segment, index) => (\n                            <div\n                                key={`${segment}-${index}`}\n                                className={`fe_fileexplorer_statusbar_text_segment_wrap${index === statusSegments.length - 1 ? ' fe_fileexplorer_statusbar_text_segment_wrap_last' : ''}`}\n                            >\n                                {segment}\n                            </div>\n                        ))}\n                        {!statusSegments.length ? (\n                            <div className=\"fe_fileexplorer_statusbar_text_segment_wrap fe_fileexplorer_statusbar_text_segment_wrap_last\">{currentPath}</div>\n                        ) : null}\n                    </div>\n                    <div className=\"fe_fileexplorer_action_wrap\">\n                        <button\n                            type=\"button\"\n                            className=\"fe_fileexplorer_open_icon\"\n                            onClick={() => void refresh()}\n                            disabled={loading}\n                            title=\"刷新\"\n                            aria-label=\"刷新\"\n                        />\n                    </div>\n                </div>\n            </div>\n            {popupMenu ? (\n                <div\n                    ref={popupRef}\n                    className=\"fe_fileexplorer_popup_wrap\"\n                    tabIndex={0}\n                    style={{ left: popupMenu.x, top: popupMenu.y }}\n                    onKeyDown={handlePopupKeyDown}\n                >\n                    <div className=\"fe_fileexplorer_popup_inner_wrap\">\n                        {popupMenu.items.map((item, index) => (\n                            <React.Fragment key={item.id}>\n                                {item.separatorBefore ? <div className=\"fe_fileexplorer_popup_item_split\" /> : null}\n                                <div\n                                    className={`fe_fileexplorer_popup_item_wrap${item.disabled ? ' fe_fileexplorer_popup_item_disabled' : ''}${index === popupMenu.focusIndex ? ' fe_fileexplorer_popup_item_wrap_focus' : ''}`}\n                                    onMouseEnter={() => setPopupMenu((current) => current ? { ...current, focusIndex: index } : current)}\n                                    onMouseDown={(event) => event.preventDefault()}\n                                    onClick={() => void activatePopupItem(index)}\n                                >\n                                    <div className=\"fe_fileexplorer_popup_item_icon\">\n                                        <div className={`fe_fileexplorer_popup_item_icon_inner${item.iconClass ? ` ${item.iconClass}` : ''}`} />\n                                    </div>\n                                    <div className={`fe_fileexplorer_popup_item_text${item.active ? ' fe_fileexplorer_popup_item_active' : ''}`}>\n                                        {item.label}\n                                    </div>\n                                </div>\n                            </React.Fragment>\n                        ))}\n                    </div>\n                </div>\n            ) : null}\n        </div>\n    );\n};\n"
  },
  {
    "path": "src/gui/jsx/HtmlView.ts",
    "content": "import { UiComponent } from './UiComponent';\nimport { UiObject } from '../UiObject';\nimport { HtmlReactElement } from '../HtmlReactElement';\nimport * as THREE from 'three';\nexport class HtmlView extends UiComponent {\n    createUiObject(props: any): UiObject {\n        const htmlElement = HtmlReactElement.factory(this.props.component, this.props.props || {});\n        htmlElement.setSize(props.width || 0, props.height || 0);\n        const uiObject = new UiObject(new THREE.Object3D(), htmlElement);\n        uiObject.setPosition(props.x || 0, props.y || 0);\n        if (props.hidden) {\n            uiObject.setVisible(false);\n        }\n        this.props.innerRef?.(htmlElement);\n        return uiObject;\n    }\n    getElement(): HtmlReactElement<any> | undefined {\n        return this.getUiObject().getHtmlContainer() as HtmlReactElement<any>;\n    }\n}\n"
  },
  {
    "path": "src/gui/jsx/JsxRenderer.ts",
    "content": "import { renderJsx } from \"./jsx\";\nimport { UiObjectSprite } from \"../UiObjectSprite\";\nimport { UiObject } from \"../UiObject\";\nimport { HtmlContainer } from \"../HtmlContainer\";\nimport { ShpSpriteBatch } from \"../ShpSpriteBatch\";\nimport { CanvasSpriteBuilder } from '../../engine/renderable/builder/CanvasSpriteBuilder';\nimport * as THREE from 'three';\nimport { Camera } from 'three';\nimport { PointerEvents } from '../PointerEvents';\ninterface SpriteProps {\n    images?: any;\n    image?: string | any;\n    palette?: string | any;\n    alignX?: number;\n    alignY?: number;\n    x?: number;\n    y?: number;\n    frame?: number;\n    animationRunner?: any;\n    hidden?: boolean;\n    zIndex?: number;\n    opacity?: number;\n    transparent?: boolean;\n    tooltip?: string;\n    onFrame?: () => void;\n    static?: boolean;\n    [key: string]: any;\n}\ninterface ContainerProps {\n    x?: number;\n    y?: number;\n    width?: number;\n    height?: number;\n    hidden?: boolean;\n    zIndex?: number;\n    onFrame?: () => void;\n    [key: string]: any;\n}\ninterface MeshProps {\n    x?: number;\n    y?: number;\n    hidden?: boolean;\n    zIndex?: number;\n    children?: any;\n    [key: string]: any;\n}\nconst hasImages = (props: SpriteProps): boolean => !!props.images;\nexport class JsxRenderer {\n    private images: Map<string, any> | any;\n    private palettes: Map<string, any> | any;\n    private camera: Camera;\n    private jsxIntrinsicRenderers: {\n        [key: string]: (props: any) => {\n            obj: any;\n            children?: any[];\n        };\n    };\n    constructor(images: Map<string, any> | any, palettes: Map<string, any> | any, camera: Camera, pointerEvents?: PointerEvents) {\n        this.images = images;\n        this.palettes = palettes;\n        this.camera = camera;\n        this.jsxIntrinsicRenderers = {\n            sprite: (props: SpriteProps) => {\n                let sprite: UiObjectSprite;\n                if (hasImages(props)) {\n                    const builder = new CanvasSpriteBuilder(props.images, this.camera);\n                    builder.setAlign(props.alignX ?? 0, props.alignY ?? 0);\n                    sprite = new UiObjectSprite(builder);\n                }\n                else {\n                    const image = typeof props.image === \"string\" ? this.getImage(props.image) : props.image;\n                    const palette = typeof props.palette === \"string\" ? this.getPalette(props.palette) : props.palette;\n                    sprite = UiObjectSprite.fromShpFile(image, palette, this.camera);\n                    if ((sprite as any).builder && (props.alignX !== undefined || props.alignY !== undefined)) {\n                        (sprite as any).builder.setAlign?.(props.alignX ?? 0, props.alignY ?? 0);\n                    }\n                }\n                if (pointerEvents) {\n                    sprite.setPointerEvents(pointerEvents);\n                }\n                this.setupListeners(sprite, props);\n                if (props.onFrame) {\n                    sprite.onFrame.subscribe(props.onFrame);\n                }\n                sprite.setPosition(props.x || 0, props.y || 0);\n                if (props.frame !== undefined) {\n                    sprite.setFrame(props.frame);\n                }\n                if (props.animationRunner) {\n                    sprite.setAnimationRunner(props.animationRunner);\n                }\n                if (props.hidden) {\n                    sprite.setVisible(false);\n                }\n                if (props.zIndex) {\n                    sprite.setZIndex(props.zIndex);\n                }\n                if (props.opacity !== undefined) {\n                    sprite.setOpacity(props.opacity);\n                }\n                if (props.transparent !== undefined) {\n                    sprite.setTransparent(props.transparent);\n                }\n                if (props.tooltip !== undefined) {\n                    sprite.setTooltip(props.tooltip);\n                }\n                return { obj: sprite };\n            },\n            \"sprite-batch\": (props: {\n                children?: any[];\n            }) => {\n                let children: any[] = [];\n                if (props.children) {\n                    children = Array.isArray(props.children)\n                        ? props.children.flat()\n                        : [props.children];\n                }\n                let dynamicChildren: any[] = [];\n                let staticSprites: any[] = [];\n                for (const child of children) {\n                    if (child.type === \"sprite\" && child.props.static && !hasImages(child.props)) {\n                        staticSprites.push(child.props);\n                    }\n                    else {\n                        dynamicChildren.push(child);\n                    }\n                }\n                return {\n                    obj: new ShpSpriteBatch(staticSprites, (name: string) => this.getImage(name), (name: string) => this.getPalette(name), this.camera),\n                    children: [...dynamicChildren],\n                };\n            },\n            container: (props: ContainerProps) => {\n                let container = new UiObject(new THREE.Object3D(), new HtmlContainer());\n                if (pointerEvents) {\n                    container.setPointerEvents(pointerEvents);\n                }\n                this.setupListeners(container, props);\n                if (props.onFrame) {\n                    container.onFrame.subscribe(props.onFrame);\n                }\n                if (props.hidden) {\n                    container.setVisible(false);\n                }\n                if (props.zIndex) {\n                    container.setZIndex(props.zIndex);\n                }\n                container.setPosition(props.x || 0, props.y || 0);\n                container.getHtmlContainer()?.setSize(props.width || 0, props.height || 0);\n                return { obj: container };\n            },\n            mesh: (props: MeshProps) => {\n                let mesh = new UiObject(props.children);\n                if (pointerEvents) {\n                    mesh.setPointerEvents(pointerEvents);\n                }\n                this.setupListeners(mesh, props);\n                mesh.setPosition(props.x || 0, props.y || 0);\n                if (props.zIndex) {\n                    mesh.setZIndex(props.zIndex);\n                }\n                if (props.hidden) {\n                    mesh.setVisible(false);\n                }\n                return { obj: mesh };\n            },\n        };\n    }\n    private setupListeners(object: any, props: any): void {\n        const eventMap: {\n            [key: string]: string;\n        } = {\n            click: \"onClick\",\n            dblclick: \"onDoubleClick\",\n            mousedown: \"onMouseDown\",\n            mouseenter: \"onMouseEnter\",\n            mouseleave: \"onMouseLeave\",\n            mouseout: \"onMouseOut\",\n            mouseover: \"onMouseOver\",\n            mouseup: \"onMouseUp\",\n            mousemove: \"onMouseMove\",\n            wheel: \"onWheel\",\n        };\n        Object.keys(eventMap).forEach((eventType) => {\n            const handler = props[eventMap[eventType]];\n            if (handler) {\n                object.addEventListener(eventType, handler);\n            }\n        });\n    }\n    public setCamera(camera: Camera): void {\n        this.camera = camera;\n    }\n    private getImage(name: string): any {\n        const image = this.images.get(name);\n        if (!image) {\n            throw new Error(`Missing image \"${name}\"`);\n        }\n        return image;\n    }\n    private getPalette(name: string): any {\n        const palette = this.palettes.get(name);\n        if (!palette) {\n            throw new Error(`Missing palette \"${name}\"`);\n        }\n        return palette;\n    }\n    public render(jsx: any): any {\n        return renderJsx(jsx, this.jsxIntrinsicRenderers);\n    }\n}\n"
  },
  {
    "path": "src/gui/jsx/UiComponent.ts",
    "content": "export interface UiComponentProps {\n    [key: string]: any;\n}\nexport class UiComponent<T extends UiComponentProps = UiComponentProps> {\n    protected props: T;\n    protected uiObject: any;\n    constructor(props: T) {\n        this.props = props;\n        this.uiObject = this.createUiObject(props);\n    }\n    protected createUiObject(_props?: T): any {\n        throw new Error('Method not implemented.');\n    }\n    getUiObject(): any {\n        return this.uiObject;\n    }\n}\n"
  },
  {
    "path": "src/gui/jsx/jsx.ts",
    "content": "export interface JsxRef<T = any> {\n    current: T | undefined;\n}\nexport function createRef<T = any>(): JsxRef<T> {\n    return { current: undefined };\n}\nexport interface JsxElement {\n    isJsxElement: true;\n    type: string | Function;\n    props: any;\n    ref?: JsxRef | ((instance: any) => void);\n}\nexport function jsx(type: string | Function, props?: any, ...children: any[]): JsxElement {\n    const { ref, ...restProps } = props || {};\n    return {\n        isJsxElement: true,\n        type,\n        props: {\n            ...restProps,\n            children: children.length > 1 ? children : children[0]\n        },\n        ref,\n    };\n}\nexport interface JsxIntrinsicRenderer {\n    (props: any): {\n        obj?: any;\n        children?: any;\n    };\n}\nexport interface JsxIntrinsicRenderers {\n    [elementType: string]: JsxIntrinsicRenderer;\n}\nexport function renderJsx(elements: any, intrinsicRenderers: JsxIntrinsicRenderers): any[] {\n    const elementsArray = Array.isArray(elements) ? elements : [elements];\n    return elementsArray\n        .map((element) => {\n        if (element == null || !element.isJsxElement) {\n            return [];\n        }\n        let obj: any;\n        let refTarget: any;\n        let children = element.props.children;\n        if (typeof element.type === 'string') {\n            if (element.type === 'fragment') {\n                obj = undefined;\n            }\n            else {\n                const renderer = intrinsicRenderers[element.type];\n                if (!renderer) {\n                    throw new Error(`No renderer defined for intrinsic JSX element \"${element.type}\"`);\n                }\n                const result = renderer({ ref: element.ref, ...element.props });\n                obj = result.obj;\n                refTarget = obj;\n                if (result.children) {\n                    children = result.children;\n                }\n            }\n        }\n        else {\n            const instance = new (element.type as any)(element.props);\n            obj = instance.getUiObject();\n            children = instance.defineChildren?.() || element.props.children;\n            if (instance.onRender && obj.onFrame) {\n                obj.onFrame.subscribeOnce((deltaTime: number, source: any) => instance.onRender(deltaTime));\n            }\n            if (instance.onFrame && obj.onFrame) {\n                obj.onFrame.subscribe((deltaTime: number, source: any) => instance.onFrame(deltaTime));\n            }\n            if (instance.onDispose && obj.onDispose) {\n                obj.onDispose.subscribe((_data?: any, _source?: any) => instance.onDispose());\n            }\n            refTarget = instance;\n        }\n        const childObjects = children\n            ? (Array.isArray(children) ? children : [children])\n                .map((child) => renderJsx(child, intrinsicRenderers))\n                .reduce((acc, curr) => [...acc, ...curr], [])\n            : [];\n        if (obj && obj.add) {\n            console.log(`[renderJsx] Adding ${childObjects.length} children to parent object:`, obj.constructor.name);\n            obj.add(...childObjects);\n        }\n        else {\n            console.log(`[renderJsx] Not adding children - obj:`, obj ? obj.constructor.name : 'null', 'add method:', obj?.add ? 'exists' : 'missing', 'children count:', childObjects.length);\n        }\n        if (refTarget && element.ref) {\n            if (typeof element.ref === 'function') {\n                element.ref(refTarget);\n            }\n            else {\n                element.ref.current = refTarget;\n            }\n        }\n        return obj ? [obj] : (obj !== null ? childObjects : []);\n    })\n        .reduce((acc, curr) => [...acc, ...curr], []);\n}\n"
  },
  {
    "path": "src/gui/replay/ReplayExistsError.ts",
    "content": "export class ReplayExistsError extends Error {\n}\n"
  },
  {
    "path": "src/gui/replay/ReplayMeta.ts",
    "content": "export interface ReplayMeta {\n    id: string;\n    name: string;\n    keep: boolean;\n    timestamp: number;\n}\n"
  },
  {
    "path": "src/gui/replay/ReplayStorage.ts",
    "content": "import { ReplayMeta } from './ReplayMeta';\nexport interface ReplayStorage {\n    getManifest(rebuild?: boolean): Promise<ReplayMeta[]>;\n    saveManifest(manifest: ReplayMeta[]): Promise<void>;\n    getReplayData(meta: ReplayMeta): Promise<string>;\n    hasReplayData(meta: ReplayMeta): Promise<boolean>;\n    saveReplayData(meta: ReplayMeta, data: string): Promise<void>;\n    deleteReplayData(meta: ReplayMeta): Promise<void>;\n}\n"
  },
  {
    "path": "src/gui/replay/ReplayStorageError.ts",
    "content": "export class ReplayStorageError extends Error {\n}\n"
  },
  {
    "path": "src/gui/replay/ReplayStorageFileSystem.ts",
    "content": "import { VirtualFile } from '../../data/vfs/VirtualFile';\nimport { DataStream } from '../../data/DataStream';\nimport { StorageQuotaError } from '../../data/vfs/StorageQuotaError';\nimport { Replay } from '../../network/gamestate/Replay';\nimport { ReplayStorageError } from './ReplayStorageError';\nimport { ReplayMeta } from './ReplayMeta';\ndeclare const THREE: {\n    MathUtils: {\n        generateUUID(): string;\n    };\n};\ninterface VirtualFileSystemDirectory {\n    containsEntry(fileName: string): Promise<boolean>;\n    openFile(fileName: string): Promise<VirtualFile>;\n    writeFile(file: VirtualFile): Promise<void>;\n    deleteFile(fileName: string): Promise<void>;\n    getEntries(): AsyncIterable<string>;\n    getRawFiles(): AsyncIterable<{\n        name: string;\n        lastModified: number;\n    }>;\n    getRawFile(fileName: string): Promise<string>;\n}\ninterface SentryService {\n    captureException(error: Error, callback?: (event: any) => any): void;\n}\nexport class ReplayStorageFileSystem {\n    public static readonly manifestFileName = '_index.json';\n    public static readonly unsavedReplayPrefix = 'Unsaved_';\n    constructor(private dir: VirtualFileSystemDirectory, private sentry?: SentryService) { }\n    async getManifest(rebuild: boolean = false): Promise<ReplayMeta[]> {\n        if (rebuild) {\n            return await this.rebuildManifest();\n        }\n        if (!(await this.dir.containsEntry(ReplayStorageFileSystem.manifestFileName))) {\n            return [];\n        }\n        const manifestContent = (await this.dir.openFile(ReplayStorageFileSystem.manifestFileName)).readAsString('utf-8');\n        if (!manifestContent.length) {\n            return [];\n        }\n        try {\n            return JSON.parse(manifestContent);\n        }\n        catch (error) {\n            console.error('Replay manifest is corrupt', error);\n            this.sentry?.captureException(error as Error, (event) => {\n                event.addAttachment({\n                    filename: ReplayStorageFileSystem.manifestFileName,\n                    data: manifestContent,\n                });\n                return event;\n            });\n            await this.deleteManifest();\n            return await this.rebuildManifest();\n        }\n    }\n    async saveManifest(manifest: ReplayMeta[]): Promise<void> {\n        const stream = new DataStream();\n        stream.writeString(JSON.stringify(manifest), 'utf-8');\n        const file = new VirtualFile(stream, ReplayStorageFileSystem.manifestFileName);\n        try {\n            await this.dir.writeFile(file);\n        }\n        catch (error) {\n            if (error instanceof StorageQuotaError) {\n                throw error;\n            }\n            throw new ReplayStorageError(`Failed to save manifest (${(error as Error).message})`, { cause: error as Error });\n        }\n    }\n    async deleteManifest(): Promise<void> {\n        await this.dir.deleteFile(ReplayStorageFileSystem.manifestFileName);\n    }\n    async rebuildManifest(): Promise<ReplayMeta[]> {\n        const currentManifest = await this.getManifest();\n        let replayFileCount = 0;\n        for await (const entry of this.dir.getEntries()) {\n            if (entry.endsWith(Replay.extension)) {\n                replayFileCount++;\n            }\n        }\n        if (replayFileCount === currentManifest.length) {\n            return currentManifest;\n        }\n        console.info('Rebuilding replay index...');\n        const replayFiles = new Map<string, {\n            name: string;\n            lastModified: number;\n        }>();\n        for await (const file of this.dir.getRawFiles()) {\n            if (file.name.endsWith(Replay.extension)) {\n                replayFiles.set(file.name, file);\n            }\n        }\n        const newManifest: ReplayMeta[] = [];\n        for (const entry of currentManifest) {\n            const fileName = this.getReplayFileName(entry);\n            if (replayFiles.has(fileName)) {\n                newManifest.push(entry);\n                replayFiles.delete(fileName);\n            }\n        }\n        if (currentManifest.length - newManifest.length > 0) {\n            console.info(`Removed ${currentManifest.length - newManifest.length} orphaned entries from index`);\n        }\n        if (replayFiles.size > 0) {\n            for (const file of replayFiles.values()) {\n                const timestamp = file.lastModified;\n                newManifest.unshift({\n                    id: THREE.MathUtils.generateUUID(),\n                    name: file.name\n                        .replace(ReplayStorageFileSystem.unsavedReplayPrefix, '')\n                        .replace(Replay.extension, ''),\n                    keep: !file.name.startsWith(ReplayStorageFileSystem.unsavedReplayPrefix),\n                    timestamp: timestamp,\n                });\n            }\n            newManifest.sort((a, b) => a.timestamp === b.timestamp\n                ? a.name.localeCompare(b.name)\n                : b.timestamp - a.timestamp);\n            console.info(`Added ${replayFiles.size} new entries to replay index`);\n        }\n        try {\n            await this.saveManifest(newManifest);\n        }\n        catch (error) {\n            if (!(error instanceof StorageQuotaError)) {\n                throw error;\n            }\n            console.error('Failed to save rebuilt manifest because storage is full', error);\n        }\n        console.info('Rebuild finished.');\n        return newManifest;\n    }\n    async deleteAllReplays(): Promise<void> {\n        for await (const entry of this.dir.getEntries()) {\n            if (entry.endsWith(Replay.extension)) {\n                await this.dir.deleteFile(entry);\n            }\n        }\n        await this.deleteManifest();\n    }\n    async getReplayData(meta: ReplayMeta): Promise<string> {\n        const fileName = this.getReplayFileName(meta);\n        if (!(await this.dir.containsEntry(fileName))) {\n            throw new Error(`Replay file \"${fileName}\" not found.`);\n        }\n        return await this.dir.getRawFile(fileName);\n    }\n    async hasReplayData(meta: ReplayMeta): Promise<boolean> {\n        const fileName = this.getReplayFileName(meta);\n        return await this.dir.containsEntry(fileName);\n    }\n    async saveReplayData(meta: ReplayMeta, data: string): Promise<void> {\n        const stream = new DataStream();\n        stream.writeString(data, 'utf-8');\n        const fileName = this.getReplayFileName(meta);\n        const file = new VirtualFile(stream, fileName);\n        try {\n            await this.dir.writeFile(file);\n        }\n        catch (error) {\n            if (error instanceof StorageQuotaError) {\n                throw error;\n            }\n            if (error instanceof TypeError) {\n                throw new ReplayStorageError(`Failed to save replay file \"${fileName}\" (${(error as Error).message})`, { cause: error as Error });\n            }\n            throw new ReplayStorageError(`Failed to save replay file (${(error as Error).message})`, { cause: error as Error });\n        }\n    }\n    async deleteReplayData(meta: ReplayMeta): Promise<void> {\n        await this.dir.deleteFile(this.getReplayFileName(meta));\n    }\n    getReplayFileName(meta: ReplayMeta): string {\n        return ((meta.keep ? '' : ReplayStorageFileSystem.unsavedReplayPrefix) +\n            meta.name +\n            Replay.extension);\n    }\n}\n"
  },
  {
    "path": "src/gui/replay/ReplayStorageMemStorage.ts",
    "content": "import { ReplayMeta } from './ReplayMeta';\nexport class ReplayStorageMemStorage {\n    private replays = new Map<string, string>();\n    private manifest?: string;\n    async getManifest(): Promise<ReplayMeta[]> {\n        return this.manifest ? JSON.parse(this.manifest) : [];\n    }\n    async saveManifest(manifest: ReplayMeta[]): Promise<void> {\n        this.manifest = JSON.stringify(manifest);\n    }\n    async getReplayData(meta: ReplayMeta): Promise<string> {\n        const data = this.replays.get(meta.id);\n        if (!data) {\n            throw new Error(`Replay \"${meta.id}\" not found in memory`);\n        }\n        return data;\n    }\n    async hasReplayData(meta: ReplayMeta): Promise<boolean> {\n        return this.replays.has(meta.id);\n    }\n    async saveReplayData(meta: ReplayMeta, data: string): Promise<void> {\n        this.replays.set(meta.id, data);\n    }\n    async deleteReplayData(meta: ReplayMeta): Promise<void> {\n        this.replays.delete(meta.id);\n    }\n}\n"
  },
  {
    "path": "src/gui/replay/ReplayStorageMigration.ts",
    "content": "import { Replay } from '../../network/gamestate/Replay';\nimport { ReplayStorageFileSystem } from './ReplayStorageFileSystem';\nimport { ReplayMeta } from './ReplayMeta';\ndeclare const THREE: {\n    Math: {\n        generateUUID(): string;\n    };\n};\ninterface SplashScreen {\n    setLoadingText(text: string): void;\n}\ninterface Strings {\n    get(key: string, value?: number): string;\n}\ninterface LocalPrefs {\n    getItem(key: string): string | null;\n    setItem(key: string, value: string): void;\n    removeItem(key: string): void;\n    listItems(): string[];\n}\ninterface VirtualFileSystemDirectory {\n    containsEntry(fileName: string): Promise<boolean>;\n    openFile(fileName: string): Promise<VirtualFile>;\n    writeFile(file: VirtualFile): Promise<void>;\n    deleteFile(fileName: string): Promise<void>;\n    getEntries(): AsyncIterable<string>;\n}\ninterface VirtualFile {\n    readAsString(encoding: string): string;\n    filename: string;\n}\ninterface OldReplayMeta {\n    id: string;\n    name: string;\n    keep: boolean;\n    timestamp: number;\n}\nexport class ReplayStorageMigration {\n    public static readonly migratedMarker = '_r_replays_migrated';\n    constructor(private splashScreen: SplashScreen, private strings: Strings, private replayDir: VirtualFileSystemDirectory, private localPrefs: LocalPrefs, private storageFileSystem: ReplayStorageFileSystem) { }\n    async migrate(): Promise<void> {\n        if (Number(this.localPrefs.getItem(ReplayStorageMigration.migratedMarker) || 0) !== 4) {\n            console.info('Running replay storage migrations...');\n            await this.runMigrationTo4();\n            this.localPrefs.setItem(ReplayStorageMigration.migratedMarker, '4');\n            console.info('Migrations finished.');\n        }\n    }\n    private async runMigrationTo4(): Promise<void> {\n        this.localPrefs.removeItem('_r_replayList');\n        for (const item of this.localPrefs.listItems()) {\n            if (item.startsWith('_r_replay_')) {\n                this.localPrefs.removeItem(item);\n            }\n        }\n        const replayDir = this.replayDir;\n        const oldManifestName = 'replays.json';\n        if (await replayDir.containsEntry(oldManifestName)) {\n            if (await replayDir.containsEntry(ReplayStorageFileSystem.manifestFileName)) {\n                await replayDir.deleteFile(oldManifestName);\n            }\n            else {\n                const oldManifestContent = (await replayDir.openFile(oldManifestName)).readAsString('utf-8');\n                let oldManifest: OldReplayMeta[] = [];\n                try {\n                    oldManifest = JSON.parse(oldManifestContent);\n                }\n                catch (error) {\n                }\n                if (oldManifest.length > 0) {\n                    this.splashScreen.setLoadingText(this.strings.get('ts:replay_storage_migrating', 0));\n                    const newManifest: ReplayMeta[] = [];\n                    const existingFiles = new Set<string>();\n                    for await (const entry of replayDir.getEntries()) {\n                        if (entry.endsWith(Replay.extension)) {\n                            existingFiles.add(entry);\n                        }\n                    }\n                    const fileRenames = new Map<string, string>();\n                    const usedNames = new Set<string>();\n                    for (const oldEntry of oldManifest) {\n                        const oldFileName = 'replay_' + oldEntry.id + Replay.extension;\n                        if (existingFiles.has(oldFileName)) {\n                            const newEntry: ReplayMeta = {\n                                id: THREE.MathUtils.generateUUID(),\n                                name: Replay.sanitizeFileName(oldEntry.name),\n                                keep: oldEntry.keep,\n                                timestamp: oldEntry.timestamp,\n                            };\n                            newManifest.push(newEntry);\n                            let newFileName = this.storageFileSystem.getReplayFileName(newEntry);\n                            let counter = 1;\n                            while (usedNames.has(newFileName.toLowerCase())) {\n                                let baseName = newFileName.replace(Replay.extension, '');\n                                if (counter > 1) {\n                                    baseName = baseName.replace(/ \\(\\d+\\)$/, '');\n                                }\n                                newFileName = baseName + ` (${++counter})` + Replay.extension;\n                            }\n                            if (counter > 1) {\n                                newEntry.name += ` (${counter})`;\n                            }\n                            fileRenames.set(oldFileName, newFileName);\n                            usedNames.add(newFileName.toLowerCase());\n                        }\n                    }\n                    await replayDir.deleteFile(oldManifestName);\n                    try {\n                        let processed = 0;\n                        const total = fileRenames.size;\n                        for (const [oldName, newName] of fileRenames) {\n                            const file = await replayDir.openFile(oldName);\n                            (file as any).filename = newName;\n                            await replayDir.writeFile(file as any);\n                            await replayDir.deleteFile(oldName);\n                            this.splashScreen.setLoadingText(this.strings.get('ts:replay_storage_migrating', Math.floor((++processed / total) * 100)));\n                        }\n                        await this.storageFileSystem.saveManifest(newManifest);\n                    }\n                    catch (error) {\n                        console.error(error);\n                    }\n                }\n                else {\n                    await replayDir.deleteFile(oldManifestName);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/Controller.ts",
    "content": "import { EventDispatcher } from '../../util/event';\nexport interface Screen {\n    title?: string;\n    musicType?: any;\n    onEnter(params?: any): void | Promise<void>;\n    onLeave(): void | Promise<void>;\n    onStack?(): void | Promise<void>;\n    onUnstack?(params?: any): void | Promise<void>;\n    update?(deltaTime: number): void;\n    destroy?(): void;\n}\nexport abstract class Controller {\n    protected screens = new Map<number, Screen>();\n    protected currentScreen?: Screen;\n    protected screenStack: Array<{\n        screen: Screen;\n        screenType: number;\n    }> = [];\n    protected _onScreenChange = new EventDispatcher<Controller, number | undefined>();\n    get onScreenChange() {\n        return this._onScreenChange.asEvent();\n    }\n    addScreen(screenType: number, screen: Screen): void {\n        this.screens.set(screenType, screen);\n    }\n    async goToScreenBlocking(screenType: number, params?: any): Promise<void> {\n        console.log(`[Controller] Going to screen: ${screenType}`);\n        while (this.currentScreen || this.screenStack.length) {\n            await this.leaveCurrentScreen();\n        }\n        await this.pushScreen(screenType, params);\n    }\n    async leaveCurrentScreen(): Promise<void> {\n        await this.popScreen();\n    }\n    goToScreen(screenType: number, params?: any): void {\n        this.goToScreenBlocking(screenType, params).catch(error => {\n            console.error('[Controller] Error navigating to screen:', error);\n        });\n    }\n    async pushScreen(screenType: number, params?: any): Promise<void> {\n        console.log(`[Controller] Pushing screen: ${screenType}`);\n        if (this.currentScreen) {\n            const currentScreenType = this.getCurrentScreenType();\n            if (currentScreenType !== undefined) {\n                await this.currentScreen.onStack?.();\n                this.screenStack.push({\n                    screen: this.currentScreen,\n                    screenType: currentScreenType\n                });\n            }\n        }\n        const screen = this.screens.get(screenType);\n        if (!screen) {\n            throw new Error(`Screen ${screenType} not found`);\n        }\n        this.currentScreen = screen;\n        await screen.onEnter(params);\n        this._onScreenChange.dispatch(this, screenType);\n    }\n    async popScreen(params?: any): Promise<void> {\n        console.log('[Controller] Popping screen');\n        if (this.currentScreen) {\n            await this.currentScreen.onLeave();\n        }\n        const previousScreenInfo = this.screenStack.pop();\n        if (previousScreenInfo) {\n            this.currentScreen = previousScreenInfo.screen;\n            await previousScreenInfo.screen.onUnstack?.(params);\n            this._onScreenChange.dispatch(this, previousScreenInfo.screenType);\n        }\n        else {\n            this.currentScreen = undefined;\n            this._onScreenChange.dispatch(this, undefined);\n        }\n    }\n    getCurrentScreen(): Screen | undefined {\n        return this.currentScreen;\n    }\n    getCurrentScreenType(): number | undefined {\n        if (!this.currentScreen)\n            return undefined;\n        for (const [screenType, screen] of this.screens.entries()) {\n            if (screen === this.currentScreen) {\n                return screenType;\n            }\n        }\n        return undefined;\n    }\n    update(deltaTime: number): void {\n        if (this.currentScreen?.update) {\n            this.currentScreen.update(deltaTime);\n        }\n    }\n    destroy(): void {\n        for (const screen of this.screens.values()) {\n            screen.destroy?.();\n        }\n        this.screens.clear();\n        this.screenStack = [];\n        this.currentScreen = undefined;\n    }\n    abstract rerenderCurrentScreen(): void;\n}\n"
  },
  {
    "path": "src/gui/screen/RootController.ts",
    "content": "import { Controller } from './Controller';\nimport { ScreenType } from './ScreenType';\nexport class RootController extends Controller {\n    private serverRegions?: any;\n    constructor(serverRegions?: any) {\n        super();\n        this.serverRegions = serverRegions;\n    }\n    async goToScreenBlocking(screenType: ScreenType, params?: any): Promise<void> {\n        return super.goToScreenBlocking(screenType, params);\n    }\n    goToScreen(screenType: ScreenType, params?: any): void {\n        return super.goToScreen(screenType, params);\n    }\n    async pushScreen(screenType: ScreenType, params?: any): Promise<void> {\n        return super.pushScreen(screenType, params);\n    }\n    createGame(gameId: string, timestamp: number, gameServer?: string, playerName?: string, gameOpts?: any, singlePlayer?: boolean, tournament?: boolean, mapTransfer: boolean = false, createPrivateGame: boolean = false, returnTo?: any): void {\n        if (!this.serverRegions) {\n            throw new Error('Server regions must be loaded first');\n        }\n        let gservUrl = '';\n        if (!singlePlayer) {\n            if (!gameServer) {\n                throw new Error('Game server must be set for a multiplayer game');\n            }\n            gservUrl = gameServer;\n        }\n        this.goToScreen(ScreenType.Game, {\n            create: true,\n            gameId,\n            timestamp,\n            playerName,\n            gameOpts,\n            singlePlayer,\n            tournament,\n            mapTransfer,\n            createPrivateGame,\n            gservUrl,\n            returnTo,\n        });\n    }\n    joinGame(gameId: string, timestamp: number, gservUrl: string, playerName?: string, tournament?: boolean, mapTransfer: boolean = false, returnTo?: any): void {\n        if (!this.serverRegions) {\n            throw new Error('Server regions must be loaded first');\n        }\n        this.goToScreen(ScreenType.Game, {\n            create: false,\n            gameId,\n            timestamp,\n            playerName,\n            tournament,\n            mapTransfer,\n            gservUrl,\n            returnTo,\n        });\n    }\n    rerenderCurrentScreen(): void {\n        const currentScreen = this.getCurrentScreen() as {\n            onViewportChange?: () => void;\n        } | undefined;\n        console.log('[RootController] Rerender current screen requested', {\n            hasCurrentScreen: Boolean(currentScreen),\n            screenType: this.getCurrentScreenType(),\n            hasViewportHandler: Boolean(currentScreen?.onViewportChange),\n        });\n        currentScreen?.onViewportChange?.();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/RootRoute.ts",
    "content": "export class RootRoute {\n    public screenType: string;\n    public params: any;\n    constructor(screenType: string, params?: any) {\n        this.screenType = screenType;\n        this.params = params;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/RootScreen.ts",
    "content": "import { Screen } from './Controller';\nexport abstract class RootScreen implements Screen {\n    constructor() {\n    }\n    abstract onEnter(params?: any): void | Promise<void>;\n    abstract onLeave(): void | Promise<void>;\n    onStack?(): void | Promise<void> {\n    }\n    onUnstack?(): void | Promise<void> {\n    }\n    update?(deltaTime: number): void {\n    }\n    destroy?(): void {\n    }\n    abstract onViewportChange?(): void;\n}\n"
  },
  {
    "path": "src/gui/screen/Screen.ts",
    "content": "export abstract class Screen {\n    public abstract init(): void;\n    public abstract update(deltaTime: number): void;\n    public abstract render(): void;\n    public abstract destroy(): void;\n}\n"
  },
  {
    "path": "src/gui/screen/ScreenType.ts",
    "content": "export enum ScreenType {\n    MainMenuRoot = 0,\n    Game = 1,\n    Replay = 2\n}\nexport enum MainMenuScreenType {\n    Home = 0,\n    Skirmish = 1,\n    QuickGame = 2,\n    CustomGame = 3,\n    Login = 4,\n    NewAccount = 5,\n    Lobby = 6,\n    MapSelection = 7,\n    Ladder = 8,\n    LadderRules = 9,\n    ReplaySelection = 10,\n    ModSelection = 11,\n    Score = 12,\n    InfoAndCredits = 13,\n    PatchNotes = 14,\n    Credits = 15,\n    Options = 16,\n    OptionsSound = 17,\n    OptionsKeyboard = 18,\n    OptionsStorage = 19,\n    TestEntry = 20,\n    LanSetup = 21\n}\n"
  },
  {
    "path": "src/gui/screen/game/ChatNetHandler.ts",
    "content": "import { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { ChatRecipientType } from '@/network/chat/ChatMessage';\nimport { RECIPIENT_ALL, RECIPIENT_TEAM } from '@/network/gservConfig';\nexport class ChatNetHandler {\n    private disposables = new CompositeDisposable();\n    constructor(private gservCon: any, private wolCon: any, private messageList: any, private chatHistory: any, private chatMessageFormat: any, private localPlayer: any, private game: any, private replayRecorder: any, private mutedPlayers: Set<string>) { }\n    init(): void {\n        this.wolCon.onChatMessage.subscribe(this.handleMessage);\n        this.disposables.add(() => this.wolCon.onChatMessage.unsubscribe(this.handleMessage));\n        this.gservCon.onChatMessage.subscribe(this.handleMessage);\n        this.disposables.add(() => this.gservCon.onChatMessage.unsubscribe(this.handleMessage));\n    }\n    private handleMessage = (message: any): void => {\n        if (message.from === this.localPlayer.name &&\n            message.to.type === ChatRecipientType.Whisper) {\n            this.messageList.addChatMessage(this.chatMessageFormat.formatPrefixPlain(message) + ' ' + message.text, 'mediumpurple');\n            this.chatHistory.addChatMessage(message);\n            return;\n        }\n        const prefix = this.chatMessageFormat.formatPrefixPlain(message);\n        let color: string;\n        if (message.to.type !== ChatRecipientType.Page ||\n            (message.from !== this.gservCon.getServerName() &&\n                message.from !== this.wolCon.getServerName())) {\n            let playerName: string;\n            if (message.to.type === ChatRecipientType.Whisper) {\n                playerName = message.from;\n                color = 'mediumpurple';\n            }\n            else {\n                const player = this.game.getPlayerByName(message.from);\n                playerName = player.name;\n                color = player.color.asHexString();\n            }\n            if (this.mutedPlayers.has(playerName)) {\n                return;\n            }\n        }\n        else {\n            color = 'yellow';\n        }\n        if (message.to.type === ChatRecipientType.Channel &&\n            message.to.name === RECIPIENT_ALL) {\n            this.replayRecorder.recordChatMessage(this.game.currentTick, message.from, message.text);\n        }\n        this.messageList.addChatMessage(prefix + ' ' + message.text, color);\n        this.chatHistory.addChatMessage(message);\n        if (message.to.type === ChatRecipientType.Whisper &&\n            message.to.name !== this.wolCon.getServerName() &&\n            message.to.name !== this.gservCon.getServerName()) {\n            this.chatHistory.lastWhisperFrom.value = message.from;\n        }\n    };\n    submitMessage(text: string, recipient: any): void {\n        if (!this.gservCon.isOpen()) {\n            console.warn(\"Can't send chat message. Network connection is already closed.\");\n            return;\n        }\n        if (recipient.type === ChatRecipientType.Channel &&\n            recipient.name === RECIPIENT_ALL) {\n            if (text.startsWith('/')) {\n                const currentUser = this.wolCon.getCurrentUser();\n                if (this.wolCon.isOpen() && currentUser) {\n                    this.wolCon.privmsg([currentUser], text);\n                }\n            }\n            else {\n                this.gservCon.sayChannel(text);\n            }\n        }\n        else if (recipient.type === ChatRecipientType.Channel &&\n            recipient.name === RECIPIENT_TEAM) {\n            const allies = this.game.alliances\n                .getAllies(this.localPlayer)\n                .filter((player: any) => !player.isAi)\n                .map((player: any) => player.name);\n            this.gservCon.privmsg([...allies, this.localPlayer.name], text);\n        }\n        else if (recipient.type === ChatRecipientType.Whisper &&\n            this.wolCon.isOpen()) {\n            this.wolCon.privmsg([recipient.name], text);\n            this.chatHistory.lastWhisperTo.value = recipient.name;\n        }\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/ChatTypingHandler.ts",
    "content": "import { ChatRecipientType } from '@/network/chat/ChatMessage';\nimport { RECIPIENT_TEAM } from '@/network/gservConfig';\nexport class ChatTypingHandler {\n    private isTyping = false;\n    constructor(private keyboardHandler: any, private arrowScrollHandler: any, private messageList: any, private chatHistory: any) { }\n    startTyping(): void {\n        if (!this.isTyping) {\n            this.keyboardHandler.pause();\n            this.arrowScrollHandler.pause();\n            this.messageList.isComposing = true;\n            this.isTyping = true;\n        }\n    }\n    endTyping(): void {\n        if (this.isTyping) {\n            this.keyboardHandler.unpause();\n            this.arrowScrollHandler.unpause();\n            this.messageList.isComposing = false;\n            this.isTyping = false;\n        }\n    }\n    handleKeyDown(event: KeyboardEvent): void {\n        if (this.isTyping) {\n            return;\n        }\n        if (event.key === 'Enter') {\n            this.startTyping();\n        }\n        else if (event.key === 'Backspace') {\n            this.chatHistory.lastComposeTarget.value = {\n                type: ChatRecipientType.Channel,\n                name: RECIPIENT_TEAM,\n            };\n            this.startTyping();\n        }\n    }\n    handleKeyUp(event: KeyboardEvent): void {\n    }\n    dispose(): void {\n        this.keyboardHandler.unpause();\n        this.arrowScrollHandler.unpause();\n        this.messageList.isComposing = false;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/CombatantUi.tsx",
    "content": "import React from 'react';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { SoundKey } from '@/engine/sound/SoundKey';\nimport { ChannelType } from '@/engine/sound/ChannelType';\nimport { ActionType } from '@/game/action/ActionType';\nimport { OrderType } from '@/game/order/OrderType';\nimport { OrderUnitsAction } from '@/game/action/OrderUnitsAction';\nimport { KeyCommandType } from '@/gui/screen/game/worldInteraction/keyboard/KeyCommandType';\nimport { MapPanningHelper } from '@/engine/util/MapPanningHelper';\nimport { SelectGroupCmd } from '@/gui/screen/game/worldInteraction/keyboard/command/SelectGroupCmd';\nimport { CenterGroupCmd } from '@/gui/screen/game/worldInteraction/keyboard/command/CenterGroupCmd';\nimport { SidebarItemTargetType, SidebarCategory } from '@/gui/screen/game/component/hud/viewmodel/SidebarModel';\nimport { EventType } from '@/game/event/EventType';\nimport { SellMode } from '@/gui/screen/game/worldInteraction/SellMode';\nimport { LastRadarEventCmd } from '@/gui/screen/game/worldInteraction/keyboard/command/LastRadarEventCmd';\nimport { QueueStatus } from '@/game/player/production/ProductionQueue';\nimport { UpdateType } from '@/game/action/UpdateQueueAction';\nimport { RepairMode } from '@/gui/screen/game/worldInteraction/RepairMode';\nimport { TriggerMode } from '@/gui/screen/game/worldInteraction/keyboard/KeyCommand';\nimport { PlanningMode } from '@/gui/screen/game/worldInteraction/PlanningMode';\nimport { OrderFeedbackType } from '@/game/order/OrderFeedbackType';\nimport { SelectNextUnitCmd } from '@/gui/screen/game/worldInteraction/keyboard/command/SelectNextUnitCmd';\nimport { SetCameraLocationCmd } from '@/gui/screen/game/worldInteraction/keyboard/command/SetCameraLocationCmd';\nimport { GoToCameraLocationCmd } from '@/gui/screen/game/worldInteraction/keyboard/command/GoToCameraLocationCmd';\nimport { SpecialActionMode } from '@/gui/screen/game/worldInteraction/SpecialActionMode';\nimport { SuperWeaponStatus } from '@/game/SuperWeapon';\nimport { CenterViewCmd } from '@/gui/screen/game/worldInteraction/keyboard/command/CenterViewCmd';\nimport { FollowUnitCmd } from '@/gui/screen/game/worldInteraction/keyboard/command/FollowUnitCmd';\nimport { PendingPlacementHandler } from '@/gui/screen/game/worldInteraction/PendingPlacementHandler';\nimport { CommandBarButtonType } from '@/gui/screen/game/component/hud/commandBar/CommandBarButtonType';\nimport { BeaconMode } from '@/gui/screen/game/worldInteraction/BeaconMode';\nimport { ReportBug } from '@/gui/screen/mainMenu/main/ReportBug';\nimport { CenterBaseCmd } from '@/gui/screen/game/worldInteraction/keyboard/command/CenterBaseCmd';\nimport { SelectByTypeCmd } from '@/gui/screen/game/worldInteraction/keyboard/command/SelectTypeByCmd';\nimport { PlacementMode } from '@/gui/screen/game/worldInteraction/PlacementMode';\nimport { ObjectType } from '@/engine/type/ObjectType';\nexport class CombatantUi {\n    private readonly disposables = new CompositeDisposable();\n    private hudDisposables = new CompositeDisposable();\n    private lastSelectionHash?: string;\n    private placementMode?: PlacementMode;\n    private pendingPlacementHandler?: PendingPlacementHandler;\n    private sellMode?: SellMode;\n    private repairMode?: RepairMode;\n    private beaconMode?: BeaconMode;\n    private planningMode?: PlanningMode;\n    private specialMode?: SpecialActionMode;\n    public worldInteraction?: any;\n    constructor(private game: any, private player: any, private isSinglePlayer: boolean, private actionQueue: any, private actionFactory: any, private sidebarModel: any, private renderer: any, private worldScene: any, private soundHandler: any, private messageList: any, private sound: any, private eva: any, private worldInteractionFactory: any, private gameMenu: any, private pointer: any, private runtimeVars: any, private speedCheat: any, private strings: any, private tauntHandler: any, private renderableManager: any, private superWeaponFxHandler: any, private beaconFxHandler: any, private messageBoxApi: any, private discordUrl?: string) { }\n    init(hud: any): void {\n        const unitSelection = this.game.getUnitSelection();\n        const placementMode = PlacementMode.factory(this.game, this.player, this.renderer, this.worldScene, this.eva);\n        this.placementMode = placementMode;\n        this.disposables.add(placementMode);\n        const pendingPlacementHandler = PendingPlacementHandler.factory(this.game, this.player, this.renderer, this.worldScene);\n        this.pendingPlacementHandler = pendingPlacementHandler;\n        pendingPlacementHandler.init();\n        this.disposables.add(pendingPlacementHandler);\n        placementMode.onBuildingPlaceRequest.subscribe(({ rules, tile }) => {\n            pendingPlacementHandler.pushPlacementInfo({ rules, tile });\n            this.pushAction(ActionType.PlaceBuilding, (action: any) => {\n                action.buildingRules = rules;\n                action.tile = { x: tile.rx, y: tile.ry };\n            });\n        });\n        const sellMode = SellMode.factory(this.game, this.player, this.sidebarModel, this.pointer, this.renderer);\n        this.sellMode = sellMode;\n        this.disposables.add(sellMode);\n        sellMode.onExecute.subscribe((gameObject) => {\n            this.pushAction(ActionType.SellObject, (action: any) => {\n                action.objectId = gameObject.id;\n            });\n        });\n        const repairMode = RepairMode.factory(this.game, this.player, this.sidebarModel, this.pointer, this.renderer);\n        this.repairMode = repairMode;\n        this.disposables.add(repairMode);\n        const beaconMode = BeaconMode.factory(this.pointer, this.renderer);\n        this.beaconMode = beaconMode;\n        this.disposables.add(beaconMode);\n        repairMode.onExecute.subscribe((building) => {\n            this.pushAction(ActionType.ToggleRepair, (action: any) => {\n                action.buildingId = building.id;\n            });\n            this.sound.play(SoundKey.GenericClick, ChannelType.Ui);\n        });\n        beaconMode.onExecute.subscribe((tile) => this.handleBeacon(tile));\n        const worldInteraction = this.worldInteractionFactory.create();\n        this.worldInteraction = worldInteraction;\n        worldInteraction.init();\n        this.disposables.add(worldInteraction);\n        const planningMode = new PlanningMode(this.player, this.messageList, this.sound, this.strings, this.worldScene, unitSelection, worldInteraction.unitSelectionHandler, this.renderer, worldInteraction.targetLines, this.game.rules.general.maxWaypointPathLength);\n        this.planningMode = planningMode;\n        this.disposables.add(planningMode, () => this.specialMode?.dispose());\n        placementMode.init();\n        this.initKeyboardCommands(worldInteraction);\n        this.initGameEventListeners();\n        this.initGameMenuListeners();\n        this.initHudEventListeners(hud, sellMode, repairMode, beaconMode, worldInteraction);\n        this.lastSelectionHash = unitSelection.getHash();\n        worldInteraction.unitSelectionHandler.onUserSelectionChange.subscribe((event: any) => {\n            if (planningMode.isActive()) {\n                const updatedSelection = planningMode.updateSelection(event.selection);\n                if (updatedSelection) {\n                    for (const unit of updatedSelection) {\n                        unitSelection.addToSelection(unit);\n                    }\n                }\n            }\n            this.lastSelectionHash = unitSelection.getHash();\n            this.pushAction(ActionType.SelectUnits, (action: any) => {\n                action.unitIds = unitSelection.getSelectedUnits().map((unit: any) => unit.id);\n            });\n        });\n        worldInteraction.unitSelectionHandler.onUserSelectionUpdate.subscribe((event: any) => this.soundHandler.handleSelectionChangeEvent(event));\n        worldInteraction.defaultActionHandler.onOrder.subscribe(({ orderType, terminal, feedbackType, feedbackUnit, target }: any) => {\n            if (planningMode.isActive()) {\n                planningMode.pushOrder(orderType, target, terminal);\n            }\n            else {\n                this.pushOrder(orderType, target, feedbackType, feedbackUnit);\n            }\n        });\n        this.disposables.add(() => this.hudDisposables.dispose());\n    }\n    handleHudChange(hud: any): void {\n        if (this.worldInteraction && this.sellMode && this.repairMode && this.beaconMode) {\n            this.initHudEventListeners(hud, this.sellMode, this.repairMode, this.beaconMode, this.worldInteraction);\n        }\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n    private initGameEventListeners(): void {\n        const updateAvailableObjects = (gameObject: any) => {\n            if (gameObject.isTechno?.() &&\n                gameObject.owner === this.player &&\n                (gameObject.isBuilding?.() ||\n                    Number.isFinite(gameObject.rules.buildLimit) ||\n                    (gameObject.isVehicle?.() && gameObject.transportTrait) ||\n                    this.game.rules.general.padAircraft.includes(gameObject.name))) {\n                this.sidebarModel.updateAvailableObjects(this.game.art);\n                this.soundHandler.handleAvailableObjectsUpdate?.(this.player.production.getAvailableObjects());\n            }\n        };\n        const world = this.game.getWorld();\n        this.sidebarModel.updateAvailableObjects(this.game.art);\n        world.onObjectSpawned.subscribe(updateAvailableObjects);\n        world.onObjectRemoved.subscribe(updateAvailableObjects);\n        this.disposables.add(() => world.onObjectSpawned.unsubscribe(updateAvailableObjects), () => world.onObjectRemoved.unsubscribe(updateAvailableObjects));\n        this.disposables.add(this.game.events.subscribe(EventType.BuildingInfiltration, (event: any) => {\n            if (event.source.owner === this.player) {\n                this.sidebarModel.updateAvailableObjects(this.game.art);\n            }\n        }), this.game.events.subscribe(EventType.ObjectOwnerChange, (event: any) => {\n            if (event.target.isBuilding?.() &&\n                (event.prevOwner === this.player || event.target.owner === this.player)) {\n                this.sidebarModel.updateAvailableObjects(this.game.art);\n                this.soundHandler.handleAvailableObjectsUpdate?.(this.player.production.getAvailableObjects());\n            }\n        }));\n        this.player.production.onQueueUpdate.subscribe((queue: any) => {\n            this.sidebarModel.updateFromQueue(queue);\n            const currentBuilding = this.placementMode?.getBuilding();\n            if (currentBuilding &&\n                !this.player.production.getQueueForObject(currentBuilding).find(currentBuilding).length) {\n                this.worldInteraction?.setMode(undefined);\n            }\n            this.soundHandler.handleProductionQueueUpdate?.(queue);\n        });\n        const updateSuperWeapons = (): void => {\n            this.sidebarModel.updateSuperWeapons();\n            if (this.specialMode &&\n                this.worldInteraction?.getMode() === this.specialMode &&\n                !this.player.superWeaponsTrait\n                    ?.getAll()\n                    .find((superWeapon: any) => superWeapon.rules.type === this.specialMode?.superWeaponType)) {\n                this.worldInteraction?.setMode(undefined);\n                this.specialMode.dispose();\n                this.specialMode = undefined;\n            }\n        };\n        this.renderer.onFrame.subscribe(updateSuperWeapons);\n        this.disposables.add(() => this.renderer.onFrame.unsubscribe(updateSuperWeapons));\n        this.disposables.add(this.game.events.subscribe((event: any) => {\n            if (event.type === EventType.PowerChange && event.target === this.player) {\n                this.sidebarModel.powerGenerated = event.power;\n                this.sidebarModel.powerDrained = event.drain;\n            }\n        }));\n    }\n    private initGameMenuListeners(): void {\n        const handleToggleAlliance = (toggle: boolean, otherPlayer: any) => {\n            this.pushAction(ActionType.ToggleAlliance, (action: any) => {\n                action.toPlayer = otherPlayer;\n                action.toggle = toggle;\n            });\n        };\n        this.gameMenu.onToggleAlliance.subscribe(handleToggleAlliance);\n        this.disposables.add(() => this.gameMenu.onToggleAlliance.unsubscribe(handleToggleAlliance));\n    }\n    private initHudEventListeners(hud: any, sellMode: SellMode, repairMode: RepairMode, beaconMode: BeaconMode, worldInteraction: any): void {\n        this.hudDisposables.dispose();\n        this.hudDisposables = new CompositeDisposable();\n        const onSidebarSlotClick = (event: any) => this.handleSidebarSlotClick(event);\n        const onSidebarTabClick = () => this.sound.play(SoundKey.GUITabSound, ChannelType.Ui);\n        const onRepairButtonClick = () => {\n            if (worldInteraction.isEnabled()) {\n                worldInteraction.setMode(this.sidebarModel.repairMode ? undefined : repairMode);\n                this.sound.play(SoundKey.GenericClick, ChannelType.Ui);\n            }\n        };\n        const onSellButtonClick = () => {\n            if (worldInteraction.isEnabled()) {\n                worldInteraction.setMode(this.sidebarModel.sellMode ? undefined : sellMode);\n                this.sound.play(SoundKey.GenericClick, ChannelType.Ui);\n            }\n        };\n        hud.onSidebarSlotClick.subscribe(onSidebarSlotClick);\n        hud.onSidebarTabClick.subscribe(onSidebarTabClick);\n        hud.onRepairButtonClick.subscribe(onRepairButtonClick);\n        hud.onSellButtonClick.subscribe(onSellButtonClick);\n        this.hudDisposables.add(() => hud.onSidebarSlotClick.unsubscribe(onSidebarSlotClick), () => hud.onSidebarTabClick.unsubscribe(onSidebarTabClick), () => hud.onRepairButtonClick.unsubscribe(onRepairButtonClick), () => hud.onSellButtonClick.unsubscribe(onSellButtonClick));\n        const creditTickSounds = this.game.rules.audioVisual.creditTicks;\n        const onCreditsTick = (direction: any) => {\n            this.sound.play(direction === 'up' ? creditTickSounds[0] : creditTickSounds[1], ChannelType.CreditTicks);\n        };\n        const onMessagesTick = () => this.sound.play(SoundKey.MessageCharTyped, ChannelType.Ui);\n        const onScrollButtonClick = (enabled: boolean) => {\n            this.sound.play(enabled ? SoundKey.GenericClick : SoundKey.ScoldSound, ChannelType.Ui);\n        };\n        hud.onCreditsTick.subscribe(onCreditsTick);\n        hud.onMessagesTick.subscribe(onMessagesTick);\n        hud.onScrollButtonClick.subscribe(onScrollButtonClick);\n        this.hudDisposables.add(() => hud.onCreditsTick.unsubscribe(onCreditsTick), () => hud.onMessagesTick.unsubscribe(onMessagesTick), () => hud.onScrollButtonClick.unsubscribe(onScrollButtonClick));\n        let hasShownPlanningModeIntro = false;\n        const unitSelectionHandler = worldInteraction.unitSelectionHandler;\n        const onCommandBarButtonClick = (buttonType: CommandBarButtonType) => {\n            switch (buttonType) {\n                case CommandBarButtonType.BugReport:\n                    if (!this.discordUrl) {\n                        break;\n                    }\n                    this.gameMenu.open();\n                    this.messageBoxApi.show(React.createElement(ReportBug, { discordUrl: this.discordUrl, strings: this.strings }), this.strings.get('GUI:OK'));\n                    break;\n                case CommandBarButtonType.Beacon:\n                    if (worldInteraction.getMode() !== beaconMode) {\n                        worldInteraction.setMode(beaconMode);\n                    }\n                    break;\n                case CommandBarButtonType.Cheer:\n                    this.pushOrder(OrderType.Cheer, undefined);\n                    break;\n                case CommandBarButtonType.Deploy:\n                    this.handleDeploy();\n                    break;\n                case CommandBarButtonType.Guard:\n                    this.handleGuard();\n                    break;\n                case CommandBarButtonType.PlanningMode:\n                    if (!this.planningMode) {\n                        break;\n                    }\n                    if (this.planningMode.isActive()) {\n                        const queuedPaths = this.planningMode.exit();\n                        this.sound.play(SoundKey.EndPlanningModeSound, ChannelType.Ui);\n                        this.queueOrders(queuedPaths);\n                        if (!hasShownPlanningModeIntro) {\n                            this.messageList.addUiFeedbackMessage(this.strings.get('MSG:PlanningModeIntro3'));\n                            hasShownPlanningModeIntro = true;\n                        }\n                    }\n                    else {\n                        this.planningMode.enter();\n                        this.planningMode.updateSelection(worldInteraction.unitSelectionHandler.getSelectedUnits());\n                        this.sound.play(SoundKey.StartPlanningModeSound, ChannelType.Ui);\n                        if (!hasShownPlanningModeIntro) {\n                            this.messageList.addUiFeedbackMessage(this.strings.get('MSG:PlanningModeIntro1Button'));\n                        }\n                    }\n                    break;\n                case CommandBarButtonType.Stop:\n                    this.handleStop();\n                    break;\n                case CommandBarButtonType.Team01:\n                    this.handleCommandBarTeam(1, unitSelectionHandler);\n                    break;\n                case CommandBarButtonType.Team02:\n                    this.handleCommandBarTeam(2, unitSelectionHandler);\n                    break;\n                case CommandBarButtonType.Team03:\n                    this.handleCommandBarTeam(3, unitSelectionHandler);\n                    break;\n                case CommandBarButtonType.TypeSelect:\n                    unitSelectionHandler.selectByType();\n                    break;\n                default:\n                    console.warn(`[CombatantUi] Unhandled command bar button ${buttonType}`);\n            }\n        };\n        hud.onCommandBarButtonClick.subscribe(onCommandBarButtonClick);\n        this.hudDisposables.add(() => hud.onCommandBarButtonClick.unsubscribe(onCommandBarButtonClick));\n    }\n    private handleSidebarSlotClick(rawEvent: any): void {\n        if (!this.worldInteraction?.isEnabled()) {\n            return;\n        }\n        const event = rawEvent.isTouch && rawEvent.button === 0 && rawEvent.touchDuration && rawEvent.touchDuration > 300\n            ? { ...rawEvent, shiftKey: true, button: 2 }\n            : rawEvent;\n        if (event.target.type !== SidebarItemTargetType.Special) {\n            const rules = event.target.rules;\n            const queue = this.player.production.getQueueForObject(rules);\n            const entries = queue.find(rules);\n            const queuedQuantity = entries.reduce((sum: number, item: any) => sum + item.quantity, 0);\n            let rejected = false;\n            if (event.button === 0) {\n                if (queue.status === QueueStatus.Ready && rules.type === ObjectType.Building) {\n                    if (entries[0] === queue.getFirst()) {\n                        this.placementMode?.setBuilding(rules);\n                        this.worldInteraction?.setMode(this.placementMode);\n                    }\n                    else {\n                        this.eva.play('EVA_UnableToComply');\n                    }\n                }\n                else if (queue.status === QueueStatus.OnHold && entries[0] === queue.getFirst()) {\n                    this.pushAction(ActionType.UpdateQueue, (action: any) => {\n                        action.queueType = queue.type;\n                        action.updateType = UpdateType.Resume;\n                    });\n                }\n                else {\n                    const maxQuantity = Math.min(queue.maxSize - queue.currentSize, queue.maxItemQuantity - queuedQuantity);\n                    const quantity = Math.min(event.shiftKey ? 5 : 1, maxQuantity);\n                    if (quantity <= 0) {\n                        if (rules.type === ObjectType.Building) {\n                            this.eva.play('EVA_UnableToComply');\n                        }\n                        else {\n                            rejected = true;\n                            this.sound.play(SoundKey.ScoldSound, ChannelType.Ui);\n                        }\n                    }\n                    else {\n                        const ctrlPressed = this.worldInteraction.getLastKeyModifiers()?.ctrlKey ?? false;\n                        this.pushAction(ActionType.UpdateQueue, (action: any) => {\n                            action.queueType = queue.type;\n                            action.updateType = ctrlPressed ? UpdateType.AddNext : UpdateType.Add;\n                            action.item = rules;\n                            action.quantity = quantity;\n                        });\n                    }\n                }\n            }\n            else if (event.button === 2) {\n                if (queue.status === QueueStatus.Active && entries[0] === queue.getFirst()) {\n                    this.pushAction(ActionType.UpdateQueue, (action: any) => {\n                        action.queueType = queue.type;\n                        action.updateType = UpdateType.Pause;\n                    });\n                }\n                else if (entries.length && [QueueStatus.Ready, QueueStatus.OnHold, QueueStatus.Active].includes(queue.status)) {\n                    const quantity = Math.min(queuedQuantity, event.shiftKey ? Number.POSITIVE_INFINITY : 1);\n                    if (quantity > 0) {\n                        this.pushAction(ActionType.UpdateQueue, (action: any) => {\n                            action.queueType = queue.type;\n                            action.updateType = UpdateType.Cancel;\n                            action.item = rules;\n                            action.quantity = quantity;\n                        });\n                        this.eva.play('EVA_Canceled');\n                    }\n                }\n                else {\n                    rejected = true;\n                }\n            }\n            else {\n                return;\n            }\n            if (!rejected) {\n                this.sound.play(SoundKey.GenericClick, ChannelType.Ui);\n            }\n            return;\n        }\n        if (event.button !== 0) {\n            return;\n        }\n        this.sound.play(SoundKey.GenericClick, ChannelType.Ui);\n        if (this.player.superWeaponsTrait?.getAll().find((superWeapon: any) => superWeapon.rules === event.target.rules)?.status !==\n            SuperWeaponStatus.Ready) {\n            return;\n        }\n        if (event.target.rules.type !== undefined) {\n            this.activateSpecialMode(event.target.rules);\n        }\n    }\n    private pushOrder(orderType: OrderType, target: any, feedbackType: OrderFeedbackType = OrderFeedbackType.None, feedbackUnit: any = undefined): void {\n        const unitSelection = this.game.getUnitSelection();\n        const selectionHash = unitSelection.getHash();\n        const selectedUnits = unitSelection.getSelectedUnits();\n        const lastAction = this.actionQueue.getLast() as any;\n        if (lastAction &&\n            lastAction instanceof OrderUnitsAction &&\n            lastAction.orderType === orderType &&\n            !lastAction.queue &&\n            selectionHash === this.lastSelectionHash) {\n            if (!lastAction.target || !target || lastAction.target.equals(target)) {\n                return;\n            }\n            this.actionQueue.dequeueLast();\n        }\n        if (selectionHash !== this.lastSelectionHash) {\n            this.lastSelectionHash = selectionHash;\n            this.pushAction(ActionType.SelectUnits, (action: any) => {\n                action.unitIds = selectedUnits.map((unit: any) => unit.id);\n            });\n        }\n        this.pushAction(ActionType.OrderUnits, (action: any) => {\n            action.orderType = orderType;\n            action.target = target;\n        });\n        this.soundHandler.handleOrderPushed(feedbackUnit || selectedUnits[0], orderType, feedbackType);\n    }\n    private queueOrders(paths: any[]): void {\n        if (!paths.length) {\n            return;\n        }\n        for (const path of paths) {\n            this.pushAction(ActionType.SelectUnits, (action: any) => {\n                action.unitIds = [...path.units].map((unit: any) => unit.id);\n            });\n            for (const waypoint of path.waypoints) {\n                this.pushAction(ActionType.OrderUnits, (action: any) => {\n                    action.orderType = waypoint.orderType;\n                    action.target = waypoint.target;\n                    action.queue = true;\n                });\n            }\n        }\n        this.pushAction(ActionType.SelectUnits, (action: any) => {\n            action.unitIds = this.worldInteraction.unitSelectionHandler.getSelectedUnits().map((unit: any) => unit.id);\n        });\n    }\n    private pushAction(actionType: ActionType, configure?: (action: any) => void): void {\n        const action = this.actionFactory.create(actionType);\n        action.player = this.player;\n        configure?.(action);\n        this.actionQueue.push(action);\n    }\n    private activateSpecialMode(superWeaponRules: any): void {\n        this.specialMode?.dispose();\n        const specialMode = SpecialActionMode.factory(this.game.rules.superWeaponRules, superWeaponRules, this.superWeaponFxHandler, this.pointer, this.eva);\n        this.specialMode = specialMode;\n        specialMode.onExecute.subscribe(({ tile, tile2 }) => {\n            this.pushAction(ActionType.ActivateSuperWeapon, (action: any) => {\n                action.superWeaponType = superWeaponRules.type;\n                action.tile = { x: tile.rx, y: tile.ry };\n                if (tile2) {\n                    action.tile2 = { x: tile2.rx, y: tile2.ry };\n                }\n            });\n        });\n        this.worldInteraction?.setMode(specialMode);\n    }\n    private initKeyboardCommands(worldInteraction: any): void {\n        const unitSelectionHandler = worldInteraction.unitSelectionHandler;\n        const selectByTypeCmd = new SelectByTypeCmd(unitSelectionHandler);\n        selectByTypeCmd.init();\n        this.disposables.add(selectByTypeCmd);\n        worldInteraction\n            .registerKeyCommand(KeyCommandType.Options, () => this.gameMenu.open())\n            .registerKeyCommand(KeyCommandType.Scoreboard, () => this.gameMenu.openDiplo())\n            .registerKeyCommand(KeyCommandType.DeployObject, () => this.handleDeploy())\n            .registerKeyCommand(KeyCommandType.StopObject, () => this.handleStop())\n            .registerKeyCommand(KeyCommandType.GuardObject, () => this.handleGuard())\n            .registerKeyCommand(KeyCommandType.AllToCheer, () => this.pushOrder(OrderType.Cheer, undefined))\n            .registerKeyCommand(KeyCommandType.TypeSelect, selectByTypeCmd)\n            .registerKeyCommand(KeyCommandType.CombatantSelect, () => unitSelectionHandler.selectCombatants())\n            .registerKeyCommand(KeyCommandType.VeterancyNav, () => unitSelectionHandler.selectByVeterancy())\n            .registerKeyCommand(KeyCommandType.HealthNav, () => unitSelectionHandler.selectByHealth());\n        [\n            KeyCommandType.TeamCreate_1,\n            KeyCommandType.TeamCreate_2,\n            KeyCommandType.TeamCreate_3,\n            KeyCommandType.TeamCreate_4,\n            KeyCommandType.TeamCreate_5,\n            KeyCommandType.TeamCreate_6,\n            KeyCommandType.TeamCreate_7,\n            KeyCommandType.TeamCreate_8,\n            KeyCommandType.TeamCreate_9,\n            KeyCommandType.TeamCreate_10,\n        ].forEach((commandType, index) => {\n            worldInteraction.registerKeyCommand(commandType, () => unitSelectionHandler.createGroup((index + 1) % 10));\n        });\n        [\n            KeyCommandType.TeamAddSelect_1,\n            KeyCommandType.TeamAddSelect_2,\n            KeyCommandType.TeamAddSelect_3,\n            KeyCommandType.TeamAddSelect_4,\n            KeyCommandType.TeamAddSelect_5,\n            KeyCommandType.TeamAddSelect_6,\n            KeyCommandType.TeamAddSelect_7,\n            KeyCommandType.TeamAddSelect_8,\n            KeyCommandType.TeamAddSelect_9,\n            KeyCommandType.TeamAddSelect_10,\n        ].forEach((commandType, index) => {\n            worldInteraction.registerKeyCommand(commandType, () => unitSelectionHandler.addGroupToSelection((index + 1) % 10));\n        });\n        const mapPanningHelper = new MapPanningHelper(this.game.map);\n        [\n            KeyCommandType.TeamSelect_1,\n            KeyCommandType.TeamSelect_2,\n            KeyCommandType.TeamSelect_3,\n            KeyCommandType.TeamSelect_4,\n            KeyCommandType.TeamSelect_5,\n            KeyCommandType.TeamSelect_6,\n            KeyCommandType.TeamSelect_7,\n            KeyCommandType.TeamSelect_8,\n            KeyCommandType.TeamSelect_9,\n            KeyCommandType.TeamSelect_10,\n        ].forEach((commandType, index) => {\n            worldInteraction.registerKeyCommand(commandType, new SelectGroupCmd((index + 1) % 10, unitSelectionHandler, worldInteraction.targetLines, mapPanningHelper, this.worldScene.cameraPan));\n        });\n        [\n            KeyCommandType.TeamCenter_1,\n            KeyCommandType.TeamCenter_2,\n            KeyCommandType.TeamCenter_3,\n            KeyCommandType.TeamCenter_4,\n            KeyCommandType.TeamCenter_5,\n            KeyCommandType.TeamCenter_6,\n            KeyCommandType.TeamCenter_7,\n            KeyCommandType.TeamCenter_8,\n            KeyCommandType.TeamCenter_9,\n            KeyCommandType.TeamCenter_10,\n        ].forEach((commandType, index) => {\n            worldInteraction.registerKeyCommand(commandType, new CenterGroupCmd((index + 1) % 10, unitSelectionHandler, mapPanningHelper, this.worldScene.cameraPan));\n        });\n        new Map([\n            [KeyCommandType.StructureTab, SidebarCategory.Structures],\n            [KeyCommandType.DefenseTab, SidebarCategory.Armory],\n            [KeyCommandType.InfantryTab, SidebarCategory.Infantry],\n            [KeyCommandType.UnitTab, SidebarCategory.Vehicles],\n        ]).forEach((tabId, commandType) => {\n            worldInteraction.registerKeyCommand(commandType, () => {\n                this.sidebarModel.selectTab(tabId);\n                for (const queue of this.player.production.getAllQueues().filter((queue: any) => queue.status === QueueStatus.Ready)) {\n                    const tab = this.sidebarModel.getTabForQueueType(queue.type);\n                    if (tabId === tab.id && queue.getFirst().rules.type === ObjectType.Building) {\n                        this.placementMode?.setBuilding(queue.getFirst().rules);\n                        worldInteraction.setMode(this.placementMode);\n                        break;\n                    }\n                }\n            });\n        });\n        worldInteraction.registerKeyCommand(KeyCommandType.CenterBase, new CenterBaseCmd(this.player, this.game.rules, mapPanningHelper, this.worldScene.cameraPan));\n        worldInteraction.registerKeyCommand(KeyCommandType.ToggleSell, () => {\n            worldInteraction.setMode(this.sidebarModel.sellMode ? undefined : this.sellMode);\n        });\n        worldInteraction.registerKeyCommand(KeyCommandType.ToggleRepair, () => {\n            worldInteraction.setMode(this.sidebarModel.repairMode ? undefined : this.repairMode);\n        });\n        const lastRadarEventCmd = new LastRadarEventCmd(this.player, mapPanningHelper, this.worldScene.cameraPan);\n        worldInteraction.registerKeyCommand(KeyCommandType.CenterOnRadarEvent, lastRadarEventCmd);\n        this.disposables.add(this.game.events.subscribe((event: any) => lastRadarEventCmd.handleGameEvent(event)));\n        const syncCheatCommands = () => {\n            if (this.runtimeVars.cheatsEnabled.value) {\n                worldInteraction\n                    .registerKeyCommand(KeyCommandType.BuildCheat, () => (this.speedCheat.value = !this.speedCheat.value))\n                    .registerKeyCommand(KeyCommandType.FreeMoney, () => (this.player.credits += 10000))\n                    .registerKeyCommand(KeyCommandType.ToggleShroud, () => this.game.mapShroudTrait.revealMap(this.player, this.game));\n            }\n            else {\n                worldInteraction\n                    .unregisterKeyCommand(KeyCommandType.BuildCheat)\n                    .unregisterKeyCommand(KeyCommandType.FreeMoney)\n                    .unregisterKeyCommand(KeyCommandType.ToggleShroud);\n                this.speedCheat.value = false;\n            }\n        };\n        syncCheatCommands();\n        this.runtimeVars.cheatsEnabled.onChange.subscribe(syncCheatCommands);\n        this.disposables.add(() => this.runtimeVars.cheatsEnabled.onChange.unsubscribe(syncCheatCommands));\n        worldInteraction.registerKeyCommand(KeyCommandType.ToggleFps, () => {\n            this.runtimeVars.fps.value = !this.runtimeVars.fps.value;\n        });\n        worldInteraction.registerKeyCommand(KeyCommandType.ToggleAlliance, () => {\n            const settings = this.game.rules.mpDialogSettings;\n            if (!settings.alliesAllowed || !settings.allyChangeAllowed) {\n                return;\n            }\n            const targetPlayer = unitSelectionHandler.getSelectedUnits()[0]?.owner;\n            if (targetPlayer &&\n                targetPlayer !== this.player &&\n                this.game.alliances.canRequestAlliance(targetPlayer)) {\n                this.pushAction(ActionType.ToggleAlliance, (action: any) => {\n                    action.toPlayer = targetPlayer;\n                    action.toggle = !this.game.alliances.areAllied(this.player, targetPlayer);\n                });\n            }\n        });\n        let hasShownPlanningModeKeyIntro = false;\n        worldInteraction.registerKeyCommand(KeyCommandType.PlanningMode, {\n            triggerMode: TriggerMode.KeyDownUp,\n            execute: (isKeyUp: boolean) => {\n                if (!this.planningMode) {\n                    return;\n                }\n                if (isKeyUp) {\n                    const queuedPaths = this.planningMode.exit();\n                    this.sound.play(SoundKey.EndPlanningModeSound, ChannelType.Ui);\n                    this.queueOrders(queuedPaths);\n                    if (!hasShownPlanningModeKeyIntro) {\n                        this.messageList.addUiFeedbackMessage(this.strings.get('MSG:PlanningModeIntro3'));\n                        hasShownPlanningModeKeyIntro = true;\n                    }\n                }\n                else {\n                    this.planningMode.enter();\n                    this.planningMode.updateSelection(worldInteraction.unitSelectionHandler.getSelectedUnits());\n                    this.sound.play(SoundKey.StartPlanningModeSound, ChannelType.Ui);\n                    if (!hasShownPlanningModeKeyIntro) {\n                        this.messageList.addUiFeedbackMessage(this.strings.get('MSG:PlanningModeIntro1Key'));\n                    }\n                }\n            },\n        });\n        worldInteraction.registerKeyCommand(KeyCommandType.ScatterObject, () => {\n            if (this.planningMode?.isActive()) {\n                this.handleInvalidCommand(this.strings.get('MSG:PlanningModeNoScatter'));\n            }\n            else {\n                this.pushOrder(OrderType.Scatter, undefined);\n            }\n        });\n        const selectNextUnitCmd = new SelectNextUnitCmd(unitSelectionHandler, mapPanningHelper, this.worldScene.cameraPan, this.player, this.game.getWorld());\n        worldInteraction.registerKeyCommand(KeyCommandType.NextObject, () => {\n            selectNextUnitCmd.setReverse(false);\n            selectNextUnitCmd.execute();\n        });\n        worldInteraction.registerKeyCommand(KeyCommandType.PreviousObject, () => {\n            selectNextUnitCmd.setReverse(true);\n            selectNextUnitCmd.execute();\n        });\n        this.disposables.add(selectNextUnitCmd);\n        const startLocation = this.game.map.startingLocations[this.player.startLocation];\n        const startTile = this.game.map.tiles.getByMapCoords(startLocation.x, startLocation.y);\n        const defaultCameraLocation = startTile\n            ? mapPanningHelper.computeCameraPanFromTile(startTile.rx, startTile.ry)\n            : this.worldScene.cameraPan.getPan();\n        const cameraLocations = new Map();\n        [\n            KeyCommandType.SetView1,\n            KeyCommandType.SetView2,\n            KeyCommandType.SetView3,\n            KeyCommandType.SetView4,\n        ].forEach((commandType, index) => {\n            worldInteraction.registerKeyCommand(commandType, new SetCameraLocationCmd(this.worldScene.cameraPan, cameraLocations, index));\n        });\n        [\n            KeyCommandType.View1,\n            KeyCommandType.View2,\n            KeyCommandType.View3,\n            KeyCommandType.View4,\n        ].forEach((commandType, index) => {\n            worldInteraction.registerKeyCommand(commandType, new GoToCameraLocationCmd(this.worldScene.cameraPan, cameraLocations, index, defaultCameraLocation));\n        });\n        [\n            KeyCommandType.Taunt_1,\n            KeyCommandType.Taunt_2,\n            KeyCommandType.Taunt_3,\n            KeyCommandType.Taunt_4,\n            KeyCommandType.Taunt_5,\n            KeyCommandType.Taunt_6,\n            KeyCommandType.Taunt_7,\n            KeyCommandType.Taunt_8,\n        ].forEach((commandType, index) => {\n            worldInteraction.registerKeyCommand(commandType, () => this.tauntHandler?.sendTaunt(index + 1));\n        });\n        worldInteraction.registerKeyCommand(KeyCommandType.PlaceBeacon, () => {\n            if (worldInteraction.getMode() !== this.beaconMode) {\n                worldInteraction.setMode(this.beaconMode);\n            }\n        });\n        const centerViewCmd = new CenterViewCmd(unitSelectionHandler, mapPanningHelper, this.worldScene.cameraPan);\n        worldInteraction.registerKeyCommand(KeyCommandType.CenterView, centerViewCmd);\n        const followUnitCmd = new FollowUnitCmd(unitSelectionHandler, this.renderableManager, worldInteraction, mapPanningHelper, this.worldScene.cameraPan, this.worldScene);\n        followUnitCmd.init();\n        this.disposables.add(followUnitCmd);\n        worldInteraction.registerKeyCommand(KeyCommandType.Follow, followUnitCmd);\n        const playErrorSound = () => this.sound.play(SoundKey.SystemError, ChannelType.Ui);\n        [KeyCommandType.PageUser, KeyCommandType.ScreenCapture].forEach((commandType) => worldInteraction.registerKeyCommand(commandType, playErrorSound));\n    }\n    private handleDeploy(): void {\n        if (this.planningMode?.isActive()) {\n            this.handleInvalidCommand(this.strings.get('MSG:PlanningModeNoDeploy'));\n        }\n        else {\n            this.pushOrder(OrderType.DeploySelected, undefined);\n        }\n    }\n    private handleStop(): void {\n        if (this.planningMode?.isActive()) {\n            this.handleInvalidCommand(this.strings.get('MSG:PlanningModeNoStop'));\n        }\n        else {\n            this.pushOrder(OrderType.Stop, undefined);\n        }\n    }\n    private handleGuard(): void {\n        if (this.planningMode?.isActive()) {\n            this.handleInvalidCommand(this.strings.get('MSG:PlanningModeNoGuardArea'));\n        }\n        else {\n            this.pushOrder(OrderType.Guard, undefined);\n        }\n    }\n    private handleBeacon(tile: any): void {\n        if (this.isSinglePlayer) {\n            return;\n        }\n        if (this.beaconFxHandler.canPingLocation(this.player, tile)) {\n            this.pushAction(ActionType.PingLocation, (action: any) => {\n                action.tile = { x: tile.rx, y: tile.ry };\n            });\n        }\n    }\n    private handleCommandBarTeam(team: number, unitSelectionHandler: any): void {\n        const groupUnits = unitSelectionHandler.getGroupUnits(team);\n        if (!groupUnits.length) {\n            unitSelectionHandler.createGroup(team);\n            return;\n        }\n        if (unitSelectionHandler.getSelectedUnits().some((unit: any) => groupUnits.includes(unit))) {\n            new CenterGroupCmd(team, unitSelectionHandler, new MapPanningHelper(this.game.map), this.worldScene.cameraPan).execute();\n        }\n        else {\n            unitSelectionHandler.selectGroup(team);\n        }\n    }\n    private handleInvalidCommand(message: string): void {\n        this.sound.play(SoundKey.ScoldSound, ChannelType.Ui);\n        this.messageList.addUiFeedbackMessage(message);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/GameLoader.ts",
    "content": "import { DataStream } from '@/data/DataStream';\nimport { OperationCanceledError } from '@puzzl/core/lib/async/cancellation';\nimport { ResourceType, theaterSpecificResources } from '@/engine/resourceConfigs';\nimport { Engine } from '@/engine/Engine';\nimport { TheaterType } from '@/engine/TheaterType';\nimport { sleep } from '@/util/time';\nimport { SideType } from '@/game/SideType';\nimport { Coords } from '@/game/Coords';\nimport { IsoCoords } from '@/engine/IsoCoords';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { ImageFinder, MissingImageError } from '@/engine/ImageFinder';\nimport { ShpBuilder } from '@/engine/renderable/builder/ShpBuilder';\nimport { PipOverlay } from '@/engine/renderable/entity/PipOverlay';\nimport { CanvasSpriteBuilder } from '@/engine/renderable/builder/CanvasSpriteBuilder';\nimport { TileSets } from '@/game/theater/TileSets';\nimport { GameFactory } from '@/game/GameFactory';\nimport { TrailerSmokeFx } from '@/engine/renderable/fx/TrailerSmokeFx';\nimport { ShpAggregator } from '@/engine/renderable/builder/ShpAggregator';\nimport { BuildingShpHelper } from '@/engine/renderable/entity/building/BuildingShpHelper';\nimport { BuildingAnimArtProps } from '@/engine/renderable/entity/building/BuildingAnimArtProps';\nimport { isBetween } from '@/util/math';\nimport { MixFile } from '@/data/MixFile';\nimport { isIpad } from '@/util/userAgent';\nimport { GameOptRandomGen } from '@/game/gameopts/GameOptRandomGen';\nimport { DebugRenderable } from '@/engine/renderable/DebugRenderable';\nimport { MixinRules } from '@/game/ini/MixinRules';\nimport { isNotNullOrUndefined } from '@/util/typeGuard';\nexport class GameLoader {\n    constructor(private appVersion: string, private workerHostApi: any, private cdnResourceLoader: any, private appResourceLoader: any, private rules: any, private gameModes: any, private sound: any, private iniLogger: any, private actionLogger: any, private speedCheat: any, private gameResConfig: any, private vxlGeometryPool: any, private buildingImageDataCache: any, private debugBotIndex: any, private devMode: boolean) { }\n    async load(gameId: string, timestamp: number, gameOptions: any, mapFile: any, playerName: string, isSinglePlayer: boolean, loadingScreenApi: any, cancellationToken?: any): Promise<any> {\n        const loadingPlayerInfos = this.resolveLoadingPlayerInfos(gameId, timestamp, gameOptions);\n        loadingScreenApi.start(loadingPlayerInfos, gameOptions.mapTitle, playerName);\n        try {\n            this.workerHostApi?.warmUpPool?.();\n            return await this.doLoad(gameId, timestamp, gameOptions, mapFile, playerName, isSinglePlayer, loadingScreenApi, cancellationToken);\n        }\n        finally {\n            this.workerHostApi?.dispose?.();\n        }\n    }\n    private resolveLoadingPlayerInfos(gameId: string, timestamp: number, gameOptions: any): any[] {\n        const randomGen = GameOptRandomGen.factory(gameId, timestamp);\n        const generatedColors = randomGen.generateColors(gameOptions);\n        const generatedCountries = randomGen.generateCountries(gameOptions, this.rules);\n        return gameOptions.humanPlayers.map((player: any) => ({\n            ...player,\n            colorId: generatedColors.get(player) ?? player.colorId,\n            countryId: generatedCountries.get(player) ?? player.countryId,\n        }));\n    }\n    private async doLoad(gameId: string, timestamp: number, gameOptions: any, mapFile: any, playerName: string, isSinglePlayer: boolean, loadingScreenApi: any, cancellationToken?: any): Promise<any> {\n        if (!Engine.vfs) {\n            throw new Error('Virtual File System not initialized');\n        }\n        this.clearStaticCaches();\n        this.buildingImageDataCache.clear();\n        try {\n            if (!Engine.getActiveMod()) {\n                await this.loadFestiveAssets(cancellationToken);\n            }\n        }\n        catch (error) {\n            if (error instanceof OperationCanceledError)\n                throw error;\n            console.error(\"Couldn't load festive assets\", error);\n        }\n        await this.loadTheater(mapFile.theaterType, cancellationToken, (percent) => loadingScreenApi.onLoadProgress((percent / 100) * 30));\n        await sleep(1);\n        const botsLib = await this.loadBotsLib();\n        if (!this.devMode && botsLib.version !== this.appVersion) {\n            throw new Error(`Bot library version mismatch. Expected ${this.appVersion}, but got ${botsLib.version}`);\n        }\n        const { game, theater } = await this.createGame(gameId, timestamp, gameOptions, mapFile, isSinglePlayer, botsLib);\n        let hudSide = SideType.GDI;\n        let localPlayer: any;\n        if (playerName) {\n            localPlayer = game.getPlayerByName(playerName);\n            if (!localPlayer.isObserver) {\n                hudSide = localPlayer.country.side;\n            }\n        }\n        let cdnResources: any;\n        if (this.gameResConfig.isCdn()) {\n            cdnResources = await this.cdnResourceLoader.loadResources([\n                ResourceType.Sounds,\n                ...(hudSide === SideType.GDI\n                    ? [ResourceType.EvaAlly, ResourceType.UiAlly]\n                    : [ResourceType.EvaSov, ResourceType.UiSov]),\n                ResourceType.Cameo,\n            ], cancellationToken, (percent) => loadingScreenApi.onLoadProgress(30 + (percent / 100) * 15));\n        }\n        if (cdnResources) {\n            Engine.vfs.addArchive(new MixFile(new DataStream(cdnResources.pop(ResourceType.Cameo))), this.cdnResourceLoader.getResourceFileName(ResourceType.Cameo));\n            await Engine.vfs.addMixFile('cameocd.mix');\n        }\n        const cameoFilenames = this.collectCameoFileNames(game);\n        await this.loadHudSideImages(cdnResources, hudSide);\n        loadingScreenApi.onLoadProgress(40);\n        await sleep(1);\n        if (cdnResources) {\n            const soundResources = [\n                ResourceType.Sounds,\n                hudSide === SideType.GDI ? ResourceType.EvaAlly : ResourceType.EvaSov,\n            ];\n            for (const resourceType of soundResources) {\n                Engine.vfs.addArchive(new MixFile(new DataStream(cdnResources.pop(resourceType))), this.cdnResourceLoader.getResourceFileName(resourceType));\n            }\n            await Engine.vfs.addBagFile('audio.bag');\n        }\n        loadingScreenApi.onLoadProgress(45);\n        await sleep(1);\n        const isMobile = /iPhone|Android|CrOS|Windows Phone|webOS/i.test(navigator.userAgent) || isIpad();\n        if (!isMobile) {\n            console.time('Load sounds');\n            await this.prepareSounds(cancellationToken, (percent) => loadingScreenApi.onLoadProgress(45 + (percent / 100) * 15));\n            console.timeEnd('Load sounds');\n        }\n        loadingScreenApi.onLoadProgress(60);\n        await sleep(1);\n        if (!isMobile) {\n            const images = Engine.getImages();\n            const imageFinder = new ImageFinder(images as any, theater);\n            console.time('Load textures');\n            await this.prepareTextures(game.rules, game.art, mapFile, imageFinder, cancellationToken, (percent) => loadingScreenApi.onLoadProgress(60 + (percent / 100) * 10));\n            console.timeEnd('Load textures');\n        }\n        loadingScreenApi.onLoadProgress(70);\n        await sleep(1);\n        console.time('Load voxels');\n        await this.prepareVxlGeometries(game.rules, game.art, game.map, Engine.getVoxels(), cancellationToken, (percent) => loadingScreenApi.onLoadProgress(70 + (percent / 100) * 20));\n        console.timeEnd('Load voxels');\n        await sleep(1);\n        cancellationToken?.throwIfCancelled();\n        IsoCoords.init({\n            x: 0,\n            y: (game.map.mapBounds.getFullSize().width * Coords.getWorldTileSize()) / 2,\n        });\n        game.init(localPlayer);\n        cancellationToken?.throwIfCancelled();\n        loadingScreenApi.onLoadProgress(95);\n        await sleep(1);\n        return { game, theater, hudSide, cameoFilenames };\n    }\n    private collectCameoFileNames(game: any): string[] {\n        const filenames: string[] = [];\n        const objects = [\n            ...game.rules.buildingRules.values(),\n            ...game.rules.infantryRules.values(),\n            ...game.rules.vehicleRules.values(),\n            ...game.rules.aircraftRules.values(),\n        ];\n        for (const obj of objects) {\n            if (game.art.hasObject(obj.name, obj.type)) {\n                const artObj = game.art.getObject(obj.name, obj.type);\n                filenames.push(artObj.cameo + '.shp');\n                filenames.push(artObj.altCameo + '.shp');\n            }\n        }\n        for (const superWeapon of game.rules.superWeaponRules.values()) {\n            if (superWeapon.sidebarImage.length) {\n                filenames.push(superWeapon.sidebarImage + '.shp');\n            }\n        }\n        const filteredFilenames = filenames.filter(filename => Engine.getImages().has(filename));\n        return [...new Set(filteredFilenames)];\n    }\n    private async prepareSounds(cancellationToken?: any, onProgress?: (percent: number) => void): Promise<void> {\n        const soundFiles = new Set<any>();\n        for (const soundSpec of this.sound.soundSpecs.getAll()) {\n            for (const soundName of soundSpec.sounds) {\n                const wavFile = this.sound.getWavFile(soundName);\n                if (wavFile && wavFile.isRawImaAdpcm()) {\n                    soundFiles.add(wavFile);\n                }\n            }\n        }\n        let processed = 0;\n        const total = soundFiles.size;\n        if (total > 0) {\n            if (!this.workerHostApi || !this.workerHostApi.concurrency) {\n                return;\n            }\n            const sortedFiles = [...soundFiles].sort((a, b) => a.getRawData().length - b.getRawData().length);\n            const concurrency = this.workerHostApi.concurrency;\n            try {\n                for (let i = 0; i < concurrency; i++) {\n                    this.workerHostApi.queueTask(async (worker) => {\n                        while (sortedFiles.length && !cancellationToken?.isCancelled()) {\n                            const file = sortedFiles.pop()!;\n                            const rawData = file.getRawData();\n                            const decodedData = await worker.decodeWav(rawData);\n                            file.setData(decodedData);\n                            processed++;\n                            const progress = (processed / total) * 100;\n                            if (Math.floor(progress) % 10 === 0) {\n                                onProgress?.((processed / total) * 100);\n                            }\n                        }\n                    });\n                }\n                await Promise.resolve();\n                await this.workerHostApi.waitForTasks?.();\n                cancellationToken?.throwIfCancelled();\n            }\n            catch (error) {\n                if (error instanceof OperationCanceledError)\n                    throw error;\n                console.error(error);\n            }\n        }\n    }\n    private async loadTheater(theaterType: TheaterType, cancellationToken?: any, onProgress?: (percent: number) => void): Promise<void> {\n        if (this.gameResConfig.isCdn()) {\n            const theaterResources = theaterSpecificResources.get(theaterType);\n            if (!theaterResources) {\n                throw new Error(`Unhandled theater type ${TheaterType[theaterType]}`);\n            }\n            const resourceTypes = [\n                ResourceType.BuildGen,\n                ResourceType.Anims,\n                ResourceType.Vxl,\n                ...theaterResources,\n            ];\n            const resources = await this.cdnResourceLoader.loadResources(resourceTypes, cancellationToken, (percent) => onProgress?.((percent / 100) * 60));\n            for (const resourceType of resourceTypes) {\n                Engine.vfs.addArchive(new MixFile(new DataStream(resources.pop(resourceType))), this.cdnResourceLoader.getResourceFileName(resourceType));\n            }\n        }\n        else {\n            onProgress?.(100);\n        }\n    }\n    private async createGame(gameId: string, timestamp: number, gameOptions: any, mapFile: any, isSinglePlayer: boolean, botsLib: any): Promise<{\n        game: any;\n        theater: any;\n    }> {\n        const rulesIni = Engine.getIni(this.gameModes.getById(gameOptions.gameMode).rulesOverride);\n        const mixinRulesInis = MixinRules.getTypes(gameOptions)\n            .map(type => Engine.mixinRulesFileNames.get(type))\n            .filter(isNotNullOrUndefined)\n            .map(fileName => Engine.getIni(fileName));\n        const theater = await Engine.loadTheater(mapFile.theaterType);\n        const activeEngine = Engine.getActiveEngine();\n        const theaterSettings = Engine.getTheaterSettings(activeEngine, mapFile.theaterType);\n        const theaterIni = Engine.getTheaterIni(activeEngine, mapFile.theaterType);\n        const tileSets = new TileSets(theaterIni);\n        tileSets.loadTileData(Engine.getTileData(), theaterSettings.extension);\n        const game = GameFactory.create(mapFile, tileSets, Engine.getRules(), Engine.getArt(), Engine.getAi(), rulesIni, mixinRulesInis, gameId, timestamp, gameOptions, this.gameModes, isSinglePlayer, botsLib, this.iniLogger, this.speedCheat, this.debugBotIndex, this.actionLogger);\n        return { game, theater };\n    }\n    private async loadBotsLib(): Promise<any> {\n        try {\n            const botsLib = await (window as any).SystemJS.import('@chronodivide/sp-bots');\n            return botsLib;\n        }\n        catch (error) {\n            return { version: this.appVersion };\n        }\n    }\n    private async loadHudSideImages(cdnResources?: any, hudSide: SideType = SideType.GDI): Promise<void> {\n        if (!Engine.vfs)\n            throw new Error('VFS is not initialized');\n        Engine.vfs.removeArchive('sidec01.mix');\n        Engine.vfs.removeArchive('sidec02.mix');\n        Engine.vfs.removeArchive('sidec01cd.mix');\n        Engine.vfs.removeArchive('sidec02cd.mix');\n        Engine.unloadSideMixData();\n        if (cdnResources) {\n            const resourceType = hudSide === SideType.GDI ? ResourceType.UiAlly : ResourceType.UiSov;\n            const fileName = this.cdnResourceLoader.getResourceFileName(resourceType);\n            if (!['sidec01.mix', 'sidec02.mix'].includes(fileName)) {\n                throw new Error(`Side mix file name \"${fileName}\" mismatch`);\n            }\n            Engine.vfs.addArchive(new MixFile(new DataStream(cdnResources.pop(resourceType))), fileName);\n        }\n        else {\n            await Engine.vfs.addMixFile(hudSide === SideType.GDI ? 'sidec01.mix' : 'sidec02.mix');\n        }\n        await Engine.vfs.addMixFile(hudSide === SideType.GDI ? 'sidec01cd.mix' : 'sidec02cd.mix');\n    }\n    private async prepareTextures(rules: any, art: any, mapFile: any, imageFinder: ImageFinder, cancellationToken?: any, onProgress?: (percent: number) => void): Promise<void> {\n        const buildingShpHelper = new BuildingShpHelper(imageFinder);\n        const shpAggregator = new ShpAggregator();\n        const animationShpFiles = new Set<string>();\n        let lastProgressTime = performance.now();\n        const structuresOnMap = new Set<string>();\n        for (const structure of mapFile.structures) {\n            structuresOnMap.add(structure.name);\n        }\n        const buildingsToLoad: string[] = [];\n        for (const [name, building] of rules.buildingRules) {\n            if (structuresOnMap.has(name) || building.techLevel !== -1) {\n                buildingsToLoad.push(name);\n            }\n        }\n        let processed = 0;\n        const total = buildingsToLoad.length + rules.animationNames.size;\n        for (const buildingName of buildingsToLoad) {\n            cancellationToken?.throwIfCancelled();\n            const now = performance.now();\n            if (now - lastProgressTime > 1000) {\n                lastProgressTime = now;\n                onProgress?.((processed / total) * 100);\n                await sleep(0);\n            }\n            processed++;\n            if (!this.buildingImageDataCache.has(buildingName) && art.hasObject(buildingName, ObjectType.Building)) {\n                const artObject = art.getObject(buildingName, ObjectType.Building);\n                if (!artObject.demandLoad) {\n                    const animProps = new BuildingAnimArtProps();\n                    animProps.read(artObject.art, art);\n                    for (const animList of animProps.getAll().values()) {\n                        for (const anim of animList) {\n                            animationShpFiles.add(anim.name);\n                        }\n                    }\n                    try {\n                        const mainShp = imageFinder.findByObjectArt(artObject);\n                        const bibShp = artObject.bibShape\n                            ? imageFinder.find(artObject.bibShape, artObject.useTheaterExtension)\n                            : undefined;\n                        const animShps = buildingShpHelper.collectAnimShpFiles(animProps as any, artObject);\n                        const frameInfos = buildingShpHelper.getShpFrameInfos(artObject, mainShp, bibShp, animShps as any);\n                        const aggregatedShp = shpAggregator.aggregate([...frameInfos.values()], `agg_${buildingName}.shp`);\n                        this.buildingImageDataCache.set(buildingName, aggregatedShp);\n                        ShpBuilder.prepareTexture(aggregatedShp.file);\n                    }\n                    catch (error) {\n                        if (error instanceof MissingImageError) {\n                            continue;\n                        }\n                        throw error;\n                    }\n                }\n            }\n        }\n        for (const animName of rules.animationNames) {\n            cancellationToken?.throwIfCancelled();\n            const now = performance.now();\n            if (now - lastProgressTime > 1000) {\n                lastProgressTime = now;\n                onProgress?.((processed / total) * 100);\n                await sleep(0);\n            }\n            processed++;\n            if (!animationShpFiles.has(animName) && art.hasObject(animName, ObjectType.Animation)) {\n                const animation = art.getAnimation(animName);\n                try {\n                    const shpFile = imageFinder.findByObjectArt(animation);\n                    ShpBuilder.prepareTexture(shpFile);\n                }\n                catch (error) {\n                    if (error instanceof MissingImageError) {\n                        continue;\n                    }\n                    throw error;\n                }\n            }\n        }\n    }\n    private async prepareVxlGeometries(rules: any, art: any, gameMap: any, voxels: any, cancellationToken?: any, onProgress?: (percent: number) => void): Promise<void> {\n        if (!this.workerHostApi || !this.workerHostApi.concurrency) {\n            return;\n        }\n        const objectsToLoad = new Set([\n            ...rules.vehicleRules.values(),\n            ...rules.aircraftRules.values(),\n            ...rules.buildingRules.values(),\n        ].filter(obj => (obj.techLevel !== -1 || obj.spawned) && art.hasObject(obj.name, obj.type)));\n        for (const building of rules.buildingRules.values()) {\n            if (building.freeUnit) {\n                if (rules.hasObject(building.freeUnit, ObjectType.Vehicle)) {\n                    const freeUnit = rules.getObject(building.freeUnit, ObjectType.Vehicle);\n                    objectsToLoad.add(freeUnit);\n                }\n            }\n            if (building.undeploysInto && rules.hasObject(building.undeploysInto, ObjectType.Vehicle)) {\n                objectsToLoad.add(rules.getObject(building.undeploysInto, ObjectType.Vehicle));\n            }\n        }\n        for (const techno of gameMap.getInitialMapObjects().technos) {\n            if ((techno.isVehicle() || techno.isAircraft()) && rules.hasObject(techno.name, techno.type)) {\n                objectsToLoad.add(rules.getObject(techno.name, techno.type));\n            }\n        }\n        const vxlFiles = new Map<string, any>();\n        for (const obj of objectsToLoad) {\n            const artObj = art.getObject(obj.name, obj.type);\n            if (artObj.isVoxel || (obj.type === ObjectType.Building && obj.turretAnimIsVoxel)) {\n                const imageName = artObj.imageName.toLowerCase();\n                const filesToAdd: string[] = [];\n                if (obj.type !== ObjectType.Building) {\n                    filesToAdd.push(`${imageName}.vxl`);\n                    if (obj.spawns && obj.noSpawnAlt) {\n                        filesToAdd.push(`${imageName}wo.vxl`);\n                    }\n                    if (obj.harvester && obj.unloadingClass && rules.hasObject(obj.unloadingClass, ObjectType.Vehicle)) {\n                        const unloadingUnit = rules.getObject(obj.unloadingClass, ObjectType.Vehicle);\n                        filesToAdd.push(`${unloadingUnit.imageName.toLowerCase()}.vxl`);\n                    }\n                    if (obj.turret) {\n                        for (let i = 0; i < obj.turretCount; ++i) {\n                            filesToAdd.push(`${imageName}tur${i || ''}.vxl`);\n                        }\n                        const barrelFile = `${imageName}barl.vxl`;\n                        if (voxels.has(barrelFile)) {\n                            filesToAdd.push(barrelFile);\n                        }\n                    }\n                }\n                else if (obj.turretAnimIsVoxel) {\n                    const turretFile = `${obj.turretAnim.toLowerCase()}.vxl`;\n                    filesToAdd.push(turretFile);\n                    const barrelFile = turretFile.replace('tur', 'barl');\n                    if (voxels.has(barrelFile)) {\n                        filesToAdd.push(barrelFile);\n                    }\n                }\n                for (const filename of filesToAdd) {\n                    const vxlFile = voxels.get(filename);\n                    if (vxlFile) {\n                        vxlFiles.set(filename, vxlFile);\n                    }\n                }\n            }\n        }\n        let loaded = 0;\n        const filesToGenerate: Array<[\n            string,\n            any\n        ]> = [];\n        for (const [filename, vxlFile] of vxlFiles) {\n            cancellationToken?.throwIfCancelled();\n            if (await this.vxlGeometryPool.loadFromStorage(vxlFile, filename)) {\n                loaded++;\n                onProgress?.((loaded / vxlFiles.size) * 100);\n            }\n            else {\n                filesToGenerate.push([filename, vxlFile]);\n            }\n        }\n        if (filesToGenerate.length > 0) {\n            filesToGenerate.sort((a, b) => b[1].voxelCount - a[1].voxelCount);\n            const concurrency = this.workerHostApi.concurrency;\n            const modelQuality = this.vxlGeometryPool.getModelQuality();\n            const persistTasks: Array<() => void> = [() => this.vxlGeometryPool.clearOtherModStorage()];\n            try {\n                for (let i = 0; i < concurrency; i++) {\n                    this.workerHostApi.queueTask(async (worker) => {\n                        while (filesToGenerate.length && !cancellationToken?.isCancelled()) {\n                            const [filename, vxlFile] = filesToGenerate.pop()!;\n                            const geometry = await worker.generateVxlGeometry(vxlFile, modelQuality);\n                            persistTasks.push(() => this.vxlGeometryPool.persistToStorage(vxlFile, filename, geometry));\n                            loaded++;\n                            onProgress?.((loaded / vxlFiles.size) * 100);\n                        }\n                    });\n                }\n                await this.workerHostApi.waitForTasks();\n                cancellationToken?.throwIfCancelled();\n            }\n            catch (error) {\n                if (error instanceof OperationCanceledError)\n                    throw error;\n                console.error(error);\n                console.warn('Failed to pre-load VXL geometries. Skipping.');\n            }\n            await Promise.all(persistTasks.map(task => task())).catch(error => console.warn('Failed to persist VXL geometry cache', [error]));\n        }\n    }\n    private async loadFestiveAssets(cancellationToken?: any): Promise<void> {\n        const now = new Date();\n        const month = now.getMonth() + 1;\n        const day = now.getDate();\n        let festiveResource: ResourceType | undefined;\n        if ((month === 10 && isBetween(day, 24, 31)) || (month === 11 && isBetween(day, 1, 6))) {\n            festiveResource = ResourceType.HalloweenMix;\n        }\n        else if (month === 12 && isBetween(day, 16, 31)) {\n            festiveResource = ResourceType.XmasMix;\n        }\n        if (festiveResource !== undefined) {\n            const fileName = this.appResourceLoader.getResourceFileName(festiveResource);\n            if (!Engine.vfs.hasArchive(fileName)) {\n                const resources = await this.appResourceLoader.loadResources([festiveResource], cancellationToken);\n                const resourceData = resources.pop(festiveResource);\n                const mixFile = new MixFile(new DataStream(resourceData));\n                Engine.vfs.addArchive(mixFile, fileName);\n            }\n        }\n    }\n    clearStaticCaches(): void {\n        PipOverlay.clearCaches();\n        ShpBuilder.clearCaches();\n        DebugRenderable.clearCaches();\n        CanvasSpriteBuilder.clearCaches();\n        TrailerSmokeFx.clearTextureCache();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/GameMenu.ts",
    "content": "import { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { GameMenuController } from '@/gui/screen/game/gameMenu/GameMenuController';\nimport { ScreenType } from '@/gui/screen/game/gameMenu/ScreenType';\nimport { EventDispatcher } from '@/util/event';\nexport class GameMenu {\n    private _onOpen = new EventDispatcher<GameMenu, void>();\n    private _onQuit = new EventDispatcher<GameMenu, void>();\n    private _onObserve = new EventDispatcher<GameMenu, void>();\n    private _onCancel = new EventDispatcher<GameMenu, void>();\n    private _onToggleAlliance = new EventDispatcher<GameMenu, any>();\n    private _onSendMessage = new EventDispatcher<GameMenu, any>();\n    private disposables = new CompositeDisposable();\n    private controller?: GameMenuController;\n    get onOpen() {\n        return this._onOpen.asEvent();\n    }\n    get onQuit() {\n        return this._onQuit.asEvent();\n    }\n    get onObserve() {\n        return this._onObserve.asEvent();\n    }\n    get onCancel() {\n        return this._onCancel.asEvent();\n    }\n    get onToggleAlliance() {\n        return this._onToggleAlliance.asEvent();\n    }\n    get onSendMessage() {\n        return this._onSendMessage.asEvent();\n    }\n    constructor(private screens: Map<number, any>, private game: any, private localPlayer: any, private chatHistory: any, private gservCon?: any, private isSinglePlayer: boolean = false, private isTournament: boolean = false) { }\n    init(hud: any): void {\n        const controller = this.controller = new GameMenuController(hud);\n        for (const [screenType, screen] of this.screens) {\n            screen.setController?.(controller);\n            controller.addScreen(screenType, screen);\n        }\n        this.disposables.add(controller, () => (this.controller = undefined));\n        this.bindHudEvents(hud);\n    }\n    open(): void {\n        if (!this.controller)\n            return;\n        this._onOpen.dispatch(this);\n        this.controller.goToScreen(ScreenType.Home, {\n            observeAllowed: !(this.isTournament ||\n                this.isSinglePlayer ||\n                this.localPlayer === undefined ||\n                this.localPlayer.isObserver ||\n                this.localPlayer.defeated),\n            onQuit: async () => {\n                this.controller!.close();\n                this._onQuit.dispatch(this);\n            },\n            onObserve: () => {\n                this.controller!.close();\n                this._onObserve.dispatch(this);\n            },\n            onCancel: () => {\n                this.controller!.close();\n                this._onCancel.dispatch(this);\n            }\n        });\n    }\n    close(): void {\n        if (!this.controller)\n            return;\n        if (this.controller.getCurrentScreen()) {\n            this.controller.close();\n            this._onCancel.dispatch(this);\n        }\n    }\n    openDiplo(): void {\n        if (!this.controller)\n            return;\n        this._onOpen.dispatch(this);\n        this.controller.goToScreen(ScreenType.Diplo, {\n            game: this.game,\n            localPlayer: this.localPlayer,\n            isSinglePlayer: this.isSinglePlayer,\n            chatHistory: this.chatHistory,\n            gservCon: this.gservCon,\n            onToggleAlliance: (player: any, enabled: boolean) => {\n                this._onToggleAlliance.dispatch(player, enabled);\n            },\n            onSendMessage: (message: any) => this._onSendMessage.dispatch(this, message),\n            onCancel: () => {\n                this.controller!.close();\n                this._onCancel.dispatch(this);\n            }\n        });\n    }\n    openConnectionInfo(combatants: any, gservCon: any, chatNetHandler: any): void {\n        if (!this.controller)\n            return;\n        this._onOpen.dispatch(this);\n        this.controller.goToScreen(ScreenType.ConnectionInfo, {\n            players: combatants,\n            localPlayer: this.localPlayer,\n            chatHistory: this.chatHistory,\n            chatNetHandler: chatNetHandler,\n            gservCon: gservCon,\n            onQuit: async () => {\n                this.controller!.close();\n                this._onQuit.dispatch(this);\n            }\n        });\n    }\n    handleHudChange(hud: any): void {\n        if (!this.controller)\n            return;\n        this.controller.setHud(hud);\n        this.bindHudEvents(hud);\n        this.controller.rerenderCurrentScreen();\n    }\n    getCurrentScreen(): any {\n        return this.controller?.getCurrentScreen();\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n    private bindHudEvents(hud: any): void {\n        hud.onOptButtonClick.subscribe(() => this.open());\n        hud.onDiploButtonClick.subscribe(() => this.openDiplo());\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/GameMenuScreen.ts",
    "content": "export class GameMenuScreen {\n    protected controller?: any;\n    setController(controller: any): void {\n        this.controller = controller;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/GameScreen.ts",
    "content": "import { RootScreen } from '@/gui/screen/RootScreen';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { MedianPing } from './MedianPing';\nimport { ScreenType, MainMenuScreenType } from '@/gui/screen/ScreenType';\nimport { sleep } from '@puzzl/core/lib/async/sleep';\nimport { GameStatus } from '@/game/Game';\nimport { GameTurnManager } from '@/game/GameTurnManager';\nimport { ActionFactory } from '@/game/action/ActionFactory';\nimport { ActionQueue } from '@/game/action/ActionQueue';\nimport { DevToolsApi } from '@/tools/DevToolsApi';\nimport { GameAnimationLoop } from '@/engine/GameAnimationLoop';\nimport { GameResultPopup, GameResultType } from '@/gui/screen/game/component/GameResultPopup';\nimport { jsx } from '@/gui/jsx/jsx';\nimport { SoundHandler } from '@/gui/screen/game/SoundHandler';\nimport { StorageKey } from '@/LocalPrefs';\nimport { CombatantUi } from '@/gui/screen/game/CombatantUi';\nimport { ObserverUi } from '@/gui/screen/game/ObserverUi';\nimport { GameMenu } from '@/gui/screen/game/GameMenu';\nimport { WorldView } from '@/gui/screen/game/WorldView';\nimport { Eva } from '@/engine/sound/Eva';\nimport { EvaSpecs } from '@/engine/sound/EvaSpecs';\nimport { SideType } from '@/game/SideType';\nimport { HudFactory } from '@/gui/screen/game/HudFactory';\nimport { Minimap } from '@/gui/screen/game/component/Minimap';\nimport { Replay } from '@/network/gamestate/Replay';\nimport { ReplayRecorder } from '@/network/gamestate/ReplayRecorder';\nimport { SoloPlayTurnManager } from '@/network/gamestate/SoloPlayTurnManager';\nimport { LanLockstepTurnManager } from '@/network/lan/LanLockstepTurnManager';\nimport { LanMatchSession } from '@/network/lan/LanMatchSession';\nimport { CombatantSidebarModel } from '@/gui/screen/game/component/hud/viewmodel/CombatantSidebarModel';\nimport { ActionFactoryReg } from '@/game/action/ActionFactoryReg';\nimport { MessageList } from '@/gui/screen/game/component/hud/viewmodel/MessageList';\nimport { ChannelType } from '@/engine/sound/ChannelType';\nimport { ChatNetHandler } from '@/gui/screen/game/ChatNetHandler';\nimport { ChatTypingHandler } from '@/gui/screen/game/ChatTypingHandler';\nimport { IrcConnection } from '@/network/IrcConnection';\nimport { CancellationTokenSource, OperationCanceledError } from '@puzzl/core/lib/async/cancellation';\nimport { MusicType } from '@/engine/sound/Music';\nimport { ActionType } from '@/game/action/ActionType';\nimport { EventType } from '@/game/event/EventType';\nimport { CommandBarButtonList } from '@/gui/screen/game/component/hud/commandBar/CommandBarButtonList';\nimport { CommandBarButtonType } from '@/gui/screen/game/component/hud/commandBar/CommandBarButtonType';\nimport { LoadingScreenType } from '@/gui/screen/game/loadingScreen/LoadingScreenApiFactory';\nimport { MapFile } from '@/data/MapFile';\nimport { VirtualFile } from '@/data/vfs/VirtualFile';\nimport { base64StringToUint8Array, binaryStringToUint8Array } from '@/util/string';\nimport { MapDigest } from '@/engine/MapDigest';\nimport { MapSupport } from '@/engine/MapSupport';\nimport { OBS_COUNTRY_ID } from '@/game/gameopts/constants';\nimport { MainMenuRoute } from '@/gui/screen/mainMenu/MainMenuRoute';\nimport { RootRoute } from '@/gui/screen/RootRoute';\nimport { ChatHistory } from '@/gui/chat/ChatHistory';\nimport { PingMonitor } from '@/gui/screen/game/PingMonitor';\nimport { SidebarModel } from '@/gui/screen/game/component/hud/viewmodel/SidebarModel';\nimport { Engine } from '@/engine/Engine';\nimport * as A from '@/gui/screen/game/worldInteraction/WorldInteractionFactory';\nimport { ChatMessageFormat } from '@/gui/chat/ChatMessageFormat';\nimport { ActionsApi } from '@/game/api/ActionsApi';\nimport { OrderType } from '@/game/order/OrderType';\nimport { RadialTileFinder } from '@/game/map/tileFinder/RadialTileFinder';\nimport { Coords } from '@/game/Coords';\nimport * as THREE from 'three';\nexport class GameScreen extends RootScreen {\n    private disposables = new CompositeDisposable();\n    private avgPing = new MedianPing();\n    private preventUnload = true;\n    protected controller?: any;\n    private game?: any;\n    private replay?: any;\n    private replayRecorderInstance?: ReplayRecorder;\n    private gameTurnMgr?: any;\n    private gameAnimationLoop?: any;\n    private hud?: any;\n    private hudFactory?: any;\n    private minimap?: any;\n    private worldView?: any;\n    private activeWorldScene?: any;\n    private playerUi?: any;\n    private menu?: any;\n    private sidebarModel?: any;\n    private loadingScreenApi?: any;\n    private lagState = false;\n    private chatTypingHandler?: any;\n    private chatNetHandler?: any;\n    private lanMatchSession?: LanMatchSession;\n    private isSinglePlayer = false;\n    private isLanGame = false;\n    private isTournament = false;\n    private playerName = '';\n    private returnTo?: any;\n    private debugMapFile?: any;\n    private pausedAtSpeed?: number;\n    private gameEndHandled = false;\n    constructor(private workerHostApi: any, private gservCon: any, private wgameresService: any, private wolService: any, private mapTransferService: any, private engineVersion: string, private engineModHash: string, private errorHandler: any, private gameMenuSubScreens: any, private loadingScreenApiFactory: any, private gameOptsParser: any, private gameOptsSerializer: any, private config: any, private strings: any, private renderer: any, private uiScene: any, private runtimeVars: any, private messageBoxApi: any, private toastApi: any, private uiAnimationLoop: any, private viewport: any, private jsxRenderer: any, private pointer: any, private sound: any, private music: any, private mixer: any, private keyBinds: any, private generalOptions: any, private localPrefs: any, private actionLogger: any, private lockstepLogger: any, private replayManager: any, private fullScreen: any, private mapFileLoader: any, private mapDir: any, private mapList: any, private gameLoader: any, private vxlGeometryPool: any, private buildingImageDataCache: any, private mutedPlayers: any, private tauntsEnabled: any, private speedCheat: any, private sentry: any, private battleControlApi: any) {\n        super();\n        this.onGservClose = (error: any) => {\n            if (this.replay) {\n                this.replay.finish(this.game.currentTick);\n                this.saveReplay(this.replay);\n            }\n            this.handleError(error, this.strings.get('TXT_YOURE_DISCON'));\n            if (this.game) {\n                this.sendGameRes(this.game, {\n                    disconnect: true,\n                    desync: false,\n                    quit: false,\n                    finished: false,\n                });\n            }\n        };\n    }\n    setController(controller: any): void {\n        this.controller = controller;\n    }\n    private usesServerConnection(): boolean {\n        return !this.isSinglePlayer && !this.isLanGame;\n    }\n    async onEnter(params: any): Promise<void> {\n        this.gameEndHandled = false;\n        this.pointer.lock();\n        this.pointer.setVisible(false);\n        await this.music?.play(MusicType.Loading);\n        const cancellationTokenSource = new CancellationTokenSource();\n        this.disposables.add(() => cancellationTokenSource.cancel());\n        const cancellationToken = cancellationTokenSource.token;\n        let gameOpts: any;\n        const lanLaunch = params.lanLaunch;\n        this.lanMatchSession = params.lanMatchSession;\n        const gameId = lanLaunch?.gameId ?? params.gameId;\n        const timestamp = lanLaunch?.timestamp ?? params.timestamp;\n        this.returnTo = params.returnTo ?? lanLaunch?.returnRoute;\n        this.isTournament = params.tournament;\n        const playerName = this.playerName = lanLaunch?.localPlayerName ?? params.playerName;\n        const isSinglePlayer = this.isSinglePlayer = params.create && params.singlePlayer;\n        const isLanGame = this.isLanGame = Boolean(lanLaunch);\n        if (isSinglePlayer) {\n            gameOpts = params.gameOpts;\n        }\n        else if (isLanGame) {\n            gameOpts = lanLaunch.gameOpts;\n        }\n        else {\n            const credentials = this.wolService.getCredentials();\n            if (!credentials || credentials.user !== playerName) {\n                this.localPrefs.removeItem(StorageKey.LastConnection);\n                this.controller?.goToScreen(ScreenType.MainMenuRoot, {\n                    route: new MainMenuRoute(MainMenuScreenType.Login, {\n                        forceUser: playerName,\n                        afterLogin: (user: any) => new RootRoute('Game', params)\n                    })\n                });\n                return;\n            }\n            this.wolService.setAutoReconnect(true);\n            this.gservCon.onClose.subscribe(this.onGservClose);\n            try {\n                gameOpts = await this.connectToServerInstance(params, credentials, cancellationToken);\n            }\n            catch (error) {\n                this.handleGservConError(error);\n                return;\n            }\n            const { returnTo, ...connectionParams } = params;\n            this.localPrefs.setItem(StorageKey.LastConnection, JSON.stringify(connectionParams));\n        }\n        if (this.config.devMode) {\n            this.runtimeVars.cheatsEnabled.value = this.isSinglePlayer;\n        }\n        else if (!this.isSinglePlayer) {\n            this.runtimeVars.cheatsEnabled.value = false;\n        }\n        let mapFile: any;\n        try {\n            const mapFileData = await this.transferAndLoadMapFile(params, gameOpts.mapName, gameOpts.mapDigest, cancellationToken);\n            if (!gameOpts.mapOfficial) {\n                this.debugMapFile = mapFileData;\n                this.disposables.add(() => this.debugMapFile = undefined);\n            }\n            mapFile = new MapFile(mapFileData);\n            const mapSupportError = MapSupport.check(mapFile, this.strings);\n            if (mapSupportError) {\n                this.handleError(mapSupportError, mapSupportError);\n                return;\n            }\n        }\n        catch (error) {\n            this.handleMapLoadError(error, gameOpts.mapName);\n            return;\n        }\n        const loadingScreenType =\n            isSinglePlayer\n                ? LoadingScreenType.SinglePlayer\n                : isLanGame\n                    ? LoadingScreenType.Lan\n                    : LoadingScreenType.MultiPlayer;\n        const loadingScreenApi = this.loadingScreenApiFactory.create(loadingScreenType, this.lanMatchSession);\n        this.loadingScreenApi = loadingScreenApi;\n        this.disposables.add(loadingScreenApi, () => this.loadingScreenApi = undefined);\n        this.disposables.add(() => this.gameLoader.clearStaticCaches());\n        if (cancellationToken.isCancelled()) {\n            return;\n        }\n        let gameLoadResult: any;\n        try {\n            gameLoadResult = await this.gameLoader.load(gameId, timestamp, gameOpts, mapFile, playerName, this.isSinglePlayer, loadingScreenApi, cancellationToken);\n        }\n        catch (error) {\n            console.error('[GameScreen] Failed to load game', {\n                isLanGame: this.isLanGame,\n                isSinglePlayer: this.isSinglePlayer,\n                playerName,\n                gameId,\n                timestamp,\n                gameOpts,\n                error,\n            });\n            this.handleGameLoadError(error, params, gameOpts);\n            return;\n        }\n        if (cancellationToken.isCancelled()) {\n            return;\n        }\n        const { game, theater, hudSide, cameoFilenames } = gameLoadResult;\n        this.game = game;\n        this.disposables.add(game, () => this.game = undefined, () => Engine.unloadTheater(theater.type));\n        let localPlayer: any;\n        try {\n            localPlayer = game.getPlayerByName(playerName);\n        }\n        catch (error) {\n            console.error('[GameScreen] Failed to resolve local player after load', {\n                isLanGame: this.isLanGame,\n                playerName,\n                players: game.getAllPlayers?.().map((player: any) => player.name),\n                gameOpts,\n                error,\n            });\n            throw error;\n        }\n        let uiInitResult: any;\n        try {\n            uiInitResult = this.loadUi(game, theater, localPlayer, hudSide, cameoFilenames);\n        }\n        catch (error) {\n            const errorMessage = error.message?.match(/memory|allocation/i)\n                ? this.strings.get('TS:GameInitOom')\n                : this.strings.get('TS:GameInitError') +\n                    (game.gameOpts.mapOfficial ? '' : '\\n\\n' + this.strings.get('TS:CustomMapCrash'));\n            this.handleGameError(error, errorMessage, game);\n            return;\n        }\n        const actionFactory = new ActionFactory();\n        new ActionFactoryReg().register(actionFactory, game, playerName);\n        const actionQueue = new ActionQueue();\n        const replay = this.replay = new Replay();\n        replay.gameId = gameId;\n        replay.gameTimestamp = Math.floor(timestamp / 1000);\n        replay.gameOpts = gameOpts;\n        replay.engineVersion = this.engineVersion;\n        replay.modHash = this.engineModHash;\n        replay.timestamp = Date.now();\n        const playerNames = (gameOpts.humanPlayers ?? []).map((p: any) => p.name).join(' vs ');\n        const mapTitle = gameOpts.mapTitle ?? gameOpts.mapName ?? 'Unknown';\n        replay.name = Replay.sanitizeFileName(`${playerNames} - ${mapTitle}`);\n        this.disposables.add(() => this.replay = undefined);\n        const replayRecorder = this.replayRecorderInstance = new ReplayRecorder(game, replay);\n        this.disposables.add(() => this.replayRecorderInstance = undefined);\n        if (this.isSinglePlayer) {\n            this.gameTurnMgr = new SoloPlayTurnManager(game, localPlayer, actionQueue, this.actionLogger, replayRecorder);\n        }\n        else if (this.isLanGame) {\n            if (!this.lanMatchSession) {\n                this.handleError(new Error('Missing LAN match session'), this.strings.get('TS:ConnectFailed'));\n                return;\n            }\n            this.gameTurnMgr = this.initLockstep(game, localPlayer, actionFactory, actionQueue, replayRecorder, this.lanMatchSession);\n            this.lagState = false;\n        }\n        else {\n            this.gameTurnMgr = new GameTurnManager(game, actionQueue);\n            this.lagState = false;\n            if (localPlayer.isObserver) {\n                try {\n                }\n                catch (error) {\n                    if (error instanceof IrcConnection.SocketError) {\n                        return;\n                    }\n                    throw error;\n                }\n            }\n            else {\n                this.disposables.add(game.events.subscribe(EventType.PlayerDefeated, (event: any) => {\n                    if (event.target === localPlayer && localPlayer.isObserver) {\n                    }\n                }));\n            }\n        }\n        this.gameTurnMgr.init();\n        const startGameHandler = () => {\n            if (game.status !== GameStatus.Started) {\n                try {\n                    this.onGameStart(localPlayer, game, uiInitResult, actionQueue, actionFactory, replay);\n                }\n                catch (error) {\n                    const errorMessage = error.message?.match(/memory|allocation/i)\n                        ? this.strings.get('TS:GameInitOom')\n                        : this.strings.get('TS:GameInitError') +\n                            (game.gameOpts.mapOfficial ? '' : '\\n\\n' + this.strings.get('TS:CustomMapCrash'));\n                    this.handleGameError(error, errorMessage, game);\n                }\n            }\n        };\n        if (isSinglePlayer) {\n            startGameHandler();\n            DevToolsApi.registerCommand('reset', async () => {\n                await this.onLeave();\n                await this.onEnter(params);\n            });\n            DevToolsApi.registerVar('speed', game.desiredSpeed);\n            this.disposables.add(() => DevToolsApi.unregisterCommand('reset'), () => DevToolsApi.unregisterVar('speed'));\n            DevToolsApi.registerVar('cheats', this.runtimeVars.cheatsEnabled);\n            this.disposables.add(() => DevToolsApi.unregisterVar('cheats'));\n        }\n        else if (isLanGame) {\n            loadingScreenApi.onLoadProgress(100);\n            await this.waitForLanPlayersLoaded(cancellationToken);\n            if (cancellationToken.isCancelled()) {\n                return;\n            }\n            startGameHandler();\n        }\n        else if (this.gservCon.isOpen()) {\n            const rateChangeHandler = (rate: number) => this.gameTurnMgr.setRate(rate);\n            this.gservCon.onRateChange.subscribe(rateChangeHandler);\n            this.disposables.add(() => this.gservCon.onRateChange.unsubscribe(rateChangeHandler));\n            this.gservCon.onGameStart.subscribe(startGameHandler);\n            this.disposables.add(() => this.gservCon.onGameStart.unsubscribe(startGameHandler));\n            this.gservCon.sendLoadedPercent(100);\n        }\n    }\n\n    private async waitForLanPlayersLoaded(cancellationToken: any): Promise<void> {\n        while (!cancellationToken.isCancelled() && this.lanMatchSession && !this.lanMatchSession.areAllPlayersLoaded()) {\n            await sleep(50);\n        }\n    }\n\n    async onLeave(): Promise<void> {\n        this.pointer.unlock();\n        const hadGameAnimationLoop = Boolean(this.gameAnimationLoop);\n        if (this.gameAnimationLoop) {\n            this.gameAnimationLoop.destroy();\n            this.gameAnimationLoop = undefined;\n        }\n        this.restoreRendererToUiOnly();\n        this.clearDebugBridge();\n        if (this.hud) {\n            this.uiScene.remove(this.hud);\n            this.hud.destroy();\n            this.hud = undefined;\n        }\n        this.gameTurnMgr?.dispose();\n        this.gameTurnMgr = undefined;\n        this.lanMatchSession?.leaveRoom();\n        this.lanMatchSession?.dispose();\n        this.lanMatchSession = undefined;\n        this.disposables.dispose();\n        this.activeWorldScene = undefined;\n        if (hadGameAnimationLoop) {\n            this.uiAnimationLoop.start();\n        }\n        if (this.usesServerConnection()) {\n            this.wolService.setAutoReconnect(false);\n            this.gservCon.onClose.unsubscribe(this.onGservClose);\n            this.gservCon.close();\n        }\n    }\n    private restoreRendererToUiOnly(): void {\n        if (!this.renderer) {\n            return;\n        }\n        const scenesBefore = this.renderer.getScenes?.() ?? [];\n        console.log('[GameScreen.onLeave] restoring renderer to UI-only mode', scenesBefore.map((scene: any) => ({\n            type: scene?.constructor?.name,\n            viewport: scene?.viewport,\n        })));\n        if (this.activeWorldScene) {\n            this.renderer.removeScene(this.activeWorldScene);\n        }\n        const scenesAfterRemoval = this.renderer.getScenes?.() ?? [];\n        if (!scenesAfterRemoval.includes(this.uiScene)) {\n            this.renderer.addScene(this.uiScene);\n        }\n        this.renderer.flush?.();\n        const scenesAfter = this.renderer.getScenes?.() ?? [];\n        console.log('[GameScreen.onLeave] renderer scenes after cleanup', scenesAfter.map((scene: any) => ({\n            type: scene?.constructor?.name,\n            viewport: scene?.viewport,\n        })));\n    }\n    private clearDebugBridge(): void {\n        const debugRoot = (window as any).__ra2debug;\n        if (!debugRoot) {\n            return;\n        }\n        const keysToClear = [\n            'gameScreen',\n            'worldView',\n            'worldScene',\n            'mapRenderable',\n            'renderableManager',\n            'worldInteraction',\n            'localPlayer',\n            'minimap',\n            'game',\n            'actionQueue',\n            'actionFactory',\n            'actionsApi',\n            'unitSelection',\n            'helpers',\n        ];\n        for (const key of keysToClear) {\n            if (key in debugRoot) {\n                debugRoot[key] = undefined;\n            }\n        }\n        console.log('[GameScreen.onLeave] cleared __ra2debug game references');\n    }\n    onViewportChange(): void {\n        this.loadingScreenApi?.updateViewport();\n        this.rerenderHud();\n    }\n    private rerenderHud(): void {\n        if (this.hud) {\n            this.uiScene.remove(this.hud);\n            this.hud.destroy();\n            this.hudFactory.setSidebarModel(this.sidebarModel);\n            this.hudFactory.setViewport(this.viewport.value);\n            const newHud = this.hudFactory.create();\n            this.hud = newHud;\n            newHud.setMinimap(this.minimap);\n            this.worldView?.handleViewportChange(this.viewport.value);\n            if (this.playerUi) {\n                this.uiScene.add(newHud);\n                this.menu?.handleHudChange(newHud);\n                this.playerUi.handleHudChange(newHud);\n                if (this.chatTypingHandler) {\n                    this.initHudChatTypingEvents(this.chatTypingHandler, this.chatNetHandler, newHud);\n                }\n            }\n        }\n    }\n    private initHudChatTypingEvents(typingHandler: any, netHandler: any, hud: any): void {\n        hud.onMessageCancel.subscribe(() => {\n            typingHandler.endTyping();\n        });\n        hud.onMessageSubmit.subscribe((event: any) => {\n            typingHandler.endTyping();\n            if (event.value.length) {\n                netHandler.submitMessage(event.value, event.recipient);\n            }\n        });\n    }\n    private onGservClose: (error: any) => void;\n    private handleError(error: any, message: string, skipGoToMenu?: boolean): void {\n        if (this.gameTurnMgr) {\n            this.gameTurnMgr.setErrorState();\n        }\n        this.pointer.unlock();\n        const cleanup = () => {\n            if (!this.usesServerConnection()) {\n                return;\n            }\n            this.wolService.closeWolConnection();\n            if (this.gservCon.isOpen()) {\n                this.gservCon.onClose.unsubscribe(this.onGservClose);\n                this.gservCon.close();\n            }\n        };\n        this.errorHandler.handle(error, message, skipGoToMenu ? undefined : () => {\n            cleanup();\n            this.controller?.goToScreen('MainMenuRoot');\n        });\n        if (skipGoToMenu) {\n            cleanup();\n            this.playerUi?.dispose();\n        }\n    }\n    private saveReplay(replay: any): void {\n        if (!this.replayManager?.saveReplay) {\n            console.warn('[GameScreen.saveReplay] replayManager.saveReplay is unavailable');\n            return;\n        }\n        (async () => {\n            try {\n                await this.replayManager.saveReplay(replay);\n            }\n            catch (error) {\n                console.error(error);\n                try {\n                    this.toastApi?.push?.(this.strings.get('GUI:SaveReplayError'));\n                }\n                catch (toastError) {\n                    console.error('[GameScreen.saveReplay] failed to report replay save error', toastError);\n                }\n            }\n        })();\n    }\n    private async connectToServerInstance(params: any, credentials: any, cancellationToken: any): Promise<any> {\n        let messageBoxShown = false;\n        try {\n            setTimeout(() => {\n                if (!cancellationToken.isCancelled()) {\n                    this.messageBoxApi.show(this.strings.get('TXT_CONNECTING'));\n                    messageBoxShown = true;\n                }\n            }, 1000);\n            await this.gservCon.connect(params.gservUrl);\n            await this.gservCon.cvers(this.engineVersion);\n            await this.gservCon.login(credentials.user, credentials.pass);\n            if (params.create) {\n                const serializedOpts = this.gameOptsSerializer.serializeOptions(params.gameOpts);\n                const { gameId, timestamp } = params;\n                await this.gservCon.createGame(gameId, timestamp, serializedOpts, this.engineVersion, this.engineModHash, params.createPrivateGame);\n                console.log(`Created game instance with id ${params.gameId}.`);\n                this.localPrefs.removeItem(StorageKey.LastConnection);\n            }\n            else {\n                await this.joinGame(params.gameId, 5, cancellationToken);\n                console.log('Joined game instance with id ' + params.gameId);\n            }\n            const gameOptsData = await this.gservCon.gameOpts();\n            return this.gameOptsParser.parseOptions(gameOptsData);\n        }\n        catch (error) {\n            throw error;\n        }\n        finally {\n            if (messageBoxShown) {\n                this.messageBoxApi.destroy();\n            }\n        }\n    }\n    private async joinGame(gameId: string, retries: number, cancellationToken: any): Promise<void> {\n        if (retries) {\n            let lastError: any;\n            while (retries--) {\n                try {\n                    console.log(`Attempting to join game with id ${gameId}...`, retries + ' retries left');\n                    await this.gservCon.joinGame(gameId, this.engineVersion, this.engineModHash);\n                    return;\n                }\n                catch (error) {\n                    lastError = error;\n                    await sleep(3000);\n                }\n            }\n            this.localPrefs.removeItem(StorageKey.LastConnection);\n            throw lastError;\n        }\n        await this.gservCon.joinGame(gameId, this.engineVersion, this.engineModHash);\n    }\n    private async transferAndLoadMapFile(params: any, mapName: string, mapDigest: string, cancellationToken: any): Promise<any> {\n        let mapFileData: any;\n        if (params.lanMapDataBase64) {\n            mapFileData = VirtualFile.fromBytes(base64StringToUint8Array(params.lanMapDataBase64), mapName);\n        }\n        else if ((params.create && params.singlePlayer) || !params.mapTransfer) {\n            mapFileData = await this.mapFileLoader.load(mapName, cancellationToken);\n        }\n        else {\n            this.messageBoxApi.show(this.strings.get('GUI:MapTransfer'));\n            if (params.create) {\n                mapFileData = await this.mapFileLoader.load(mapName, cancellationToken);\n                if (this.mapTransferService.getUrl()) {\n                    await this.mapTransferService.putMap(mapFileData.getBytes(), params.gameId, cancellationToken);\n                }\n                else {\n                    this.gservCon.sendMap(mapFileData.readAsString());\n                }\n            }\n            else {\n                let transferredMapData: Uint8Array;\n                if (this.mapTransferService.getUrl()) {\n                    transferredMapData = await this.mapTransferService.getMap(params.gameId, cancellationToken);\n                }\n                else {\n                    transferredMapData = binaryStringToUint8Array(await this.gservCon.getMap());\n                }\n                mapFileData = VirtualFile.fromBytes(transferredMapData, mapName);\n                if (MapDigest.compute(mapFileData) !== mapDigest) {\n                    throw new Error('Transferred map is corrupt');\n                }\n                if (this.mapDir && !(await this.mapDir.containsEntry(mapName))) {\n                    try {\n                        await this.mapDir.writeFile(mapFileData);\n                        this.mapList.addFromMapFile(mapFileData);\n                    }\n                    catch (error) {\n                        console.error('Map couldn\\'t be saved', [error]);\n                    }\n                }\n            }\n            this.messageBoxApi.destroy();\n        }\n        return mapFileData;\n    }\n    private loadUi(game: any, theater: any, localPlayer: any, hudSide: any, cameoFilenames: any): any {\n        const sidebarModel = localPlayer.isObserver\n            ? new SidebarModel(game, this.replay)\n            : new CombatantSidebarModel(localPlayer, game);\n        const messageList = new MessageList(game.rules.audioVisual.messageDuration, 6, undefined);\n        const chatHistory = new ChatHistory();\n        this.sidebarModel = sidebarModel;\n        this.disposables.add(() => this.sidebarModel = undefined);\n        const uiIni = Engine.getUiIni();\n        const commandBarButtonList = new CommandBarButtonList();\n        if (!localPlayer.isObserver) {\n            commandBarButtonList.fromIni(uiIni.getOrCreateSection(this.isSinglePlayer ? 'AdvancedCommandBar' : 'MultiplayerAdvancedCommandBar'));\n        }\n        if (this.config.discordUrl) {\n            commandBarButtonList.buttons.push(CommandBarButtonType.BugReport);\n        }\n        this.hudFactory = new HudFactory(hudSide, this.viewport.value, sidebarModel, messageList, chatHistory, game.debugText, this.runtimeVars.debugText, localPlayer.isObserver ? undefined : localPlayer, game.getCombatants(), game.stalemateDetectTrait, game.countdownTimer, cameoFilenames, this.jsxRenderer, this.strings, commandBarButtonList.buttons, this.runtimeVars.persistentHoverTags);\n        this.disposables.add(() => this.hudFactory = undefined);\n        const hud = this.hudFactory.create();\n        this.hud = hud;\n        const minimap = this.minimap = new Minimap(game, localPlayer, hud.getTextColor(), game.rules.general.radar);\n        hud.setMinimap(minimap);\n        this.disposables.add(minimap, () => this.minimap = undefined);\n        minimap.setPointerEvents(this.pointer.pointerEvents);\n        const hudDimensions = { width: hud.sidebarWidth, height: hud.actionBarHeight } as any;\n        const worldView = new WorldView(hudDimensions, game, this.sound, this.renderer, this.runtimeVars, minimap, this.strings, this.generalOptions, this.vxlGeometryPool, this.buildingImageDataCache);\n        const worldViewInit = worldView.init(localPlayer, this.viewport.value, theater);\n        console.log('[GameScreen.loadUi] hudDimensions', {\n            sidebarWidth: hud.sidebarWidth,\n            actionBarHeight: hud.actionBarHeight,\n            viewport: this.viewport.value\n        });\n        console.log('[GameScreen.loadUi] worldViewInit keys', Object.keys(worldViewInit || {}));\n        this.worldView = worldView;\n        this.disposables.add(worldView, () => this.worldView = undefined);\n        const ws: any = worldViewInit.worldScene;\n        if (ws?.set3DObject && ws?.scene) {\n            ws.set3DObject(ws.scene);\n        }\n        worldViewInit.worldScene.create3DObject?.();\n        return {\n            worldViewInitResult: worldViewInit,\n            messageList,\n            chatHistory,\n            minimap\n        };\n    }\n    private initLockstep(game: any, localPlayer: any, actionFactory: any, actionQueue: any, replayRecorder: any, lanMatchSession: LanMatchSession): any {\n        const lockstepManager = new LanLockstepTurnManager(game, localPlayer, actionQueue, actionFactory, lanMatchSession, this.actionLogger, this.lockstepLogger, replayRecorder);\n        const onLagStateChange = (lagState: boolean) => {\n            this.lagState = lagState;\n        };\n        lockstepManager.onLagStateChange.subscribe(onLagStateChange);\n        this.disposables.add(() => lockstepManager.onLagStateChange.unsubscribe(onLagStateChange));\n        return lockstepManager;\n    }\n    private onGameStart(localPlayer: any, game: any, uiInitResult: any, actionQueue: any, actionFactory: any, replay: any): void {\n        this.localPrefs.removeItem(StorageKey.LastConnection);\n        this.loadingScreenApi?.dispose();\n        this.music?.play(MusicType.Normal);\n        const evaSpecs = new EvaSpecs(SideType.GDI).readIni(Engine.getIni('eva.ini'));\n        const eva = new Eva(evaSpecs, this.sound, this.renderer);\n        eva.init();\n        this.disposables.add(eva);\n        this.initUi(localPlayer, game, undefined, actionQueue, actionFactory, this.hud, eva, uiInitResult);\n        const worldScene = uiInitResult.worldViewInitResult?.worldScene;\n        if (worldScene) {\n            this.activeWorldScene = worldScene;\n            console.log('[GameScreen.onGameStart] adding worldScene to renderer');\n            this.renderer.removeScene(this.uiScene);\n            this.renderer.addScene(worldScene);\n            this.renderer.addScene(this.uiScene);\n            const scenes = this.renderer.getScenes?.() ?? [];\n            console.log('[GameScreen.onGameStart] scenes after add', scenes.map((s: any) => ({\n                type: s.constructor?.name,\n                viewport: s.viewport,\n            })));\n            console.log('[GameScreen.onGameStart] worldScene.scene children', worldScene.scene?.children?.length);\n        }\n        const debugRoot = ((window as any).__ra2debug ??= {});\n        const actionsApi = new ActionsApi(game, actionFactory, actionQueue, localPlayer);\n        const renderableManager = uiInitResult.worldViewInitResult?.renderableManager;\n        const worldInteraction = this.playerUi?.worldInteraction;\n        debugRoot.gameScreen = this;\n        debugRoot.renderer = this.renderer;\n        debugRoot.uiScene = this.uiScene;\n        debugRoot.worldScene = worldScene;\n        debugRoot.renderableManager = renderableManager;\n        debugRoot.worldInteraction = worldInteraction;\n        debugRoot.localPlayer = localPlayer;\n        debugRoot.game = game;\n        debugRoot.minimap = this.minimap;\n        debugRoot.actionQueue = actionQueue;\n        debugRoot.actionFactory = actionFactory;\n        debugRoot.actionsApi = actionsApi;\n        debugRoot.unitSelection = game.getUnitSelection();\n        if (this.lanMatchSession) {\n            const updateLanMatchDebugState = (snapshot: any) => {\n                debugRoot.lanMatch = snapshot;\n            };\n            updateLanMatchDebugState(this.lanMatchSession.getSnapshot());\n            this.lanMatchSession.onSnapshotChange.subscribe(updateLanMatchDebugState);\n            this.disposables.add(() => this.lanMatchSession?.onSnapshotChange.unsubscribe(updateLanMatchDebugState));\n        }\n        const serializeOwnedUnit = (unit: any) => ({\n            id: unit.id,\n            name: unit.name,\n            type: unit.constructor?.name,\n            isSpawned: unit.isSpawned,\n            tile: unit.tile ? { rx: unit.tile.rx, ry: unit.tile.ry, z: unit.tile.z } : undefined,\n        });\n        const serializeOwnedObject = (object: any) => ({\n            id: object.id,\n            name: object.name,\n            className: object.constructor?.name,\n            objectType: object.type,\n            isSpawned: Boolean(object.isSpawned),\n            isDestroyed: Boolean(object.isDestroyed),\n            isBuilding: Boolean(object.isBuilding?.()),\n            isUnit: Boolean(object.isUnit?.()),\n            insignificant: Boolean(object.rules?.insignificant),\n            inTransport: Boolean(object.limboData?.inTransport),\n            limboData: object.limboData\n                ? {\n                    selected: Boolean(object.limboData.selected),\n                    controlGroup: object.limboData.controlGroup,\n                    inTransport: Boolean(object.limboData.inTransport),\n                }\n                : undefined,\n            tile: object.tile ? { rx: object.tile.rx, ry: object.tile.ry, z: object.tile.z } : undefined,\n            traits: object.traits?.getAll?.().map((trait: any) => trait.constructor?.name) ?? [],\n        });\n        const getVictoryBlockers = () => {\n            const shortGame = game.gameOpts.shortGame;\n            const combatants = game.playerList.getCombatants();\n            return combatants.map((player: any) => {\n                const ownedObjects = player.getOwnedObjects(true);\n                const qualifyingAssets = shortGame\n                    ? ownedObjects.filter((object: any) => (object.isBuilding?.() && !object.rules.insignificant) ||\n                        game.rules.general.baseUnit.includes(object.name))\n                    : ownedObjects.filter((object: any) => !object.rules.insignificant && !object.limboData?.inTransport);\n                return {\n                    name: player.name,\n                    defeated: Boolean(player.defeated),\n                    isObserver: Boolean(player.isObserver),\n                    isAi: Boolean(player.isAi),\n                    ownedCount: ownedObjects.length,\n                    qualifyingCount: qualifyingAssets.length,\n                    ownedObjects: ownedObjects.map((object: any) => serializeOwnedObject(object)),\n                    qualifyingAssets: qualifyingAssets.map((object: any) => serializeOwnedObject(object)),\n                };\n            });\n        };\n        const resolveOwnedUnitById = (unitId: number) => {\n            const unit = localPlayer.getOwnedObjectById(unitId);\n            if (!unit) {\n                throw new Error(`No owned unit found with id \"${unitId}\"`);\n            }\n            if (!unit.isSpawned) {\n                throw new Error(`Owned unit \"${unit.name}\"#${unit.id} is not spawned`);\n            }\n            return unit;\n        };\n        const resolveOwnedUnitByName = (unitName: string) => {\n            const unit = localPlayer\n                .getOwnedObjects()\n                .find((ownedUnit: any) => ownedUnit.name === unitName && ownedUnit.isSpawned);\n            if (!unit) {\n                throw new Error(`No spawned owned unit found with name \"${unitName}\"`);\n            }\n            return unit;\n        };\n        const resolveOwnedBuildingById = (buildingId: number) => {\n            const building = localPlayer.getOwnedObjectById(buildingId);\n            if (!building) {\n                throw new Error(`No owned building found with id \"${buildingId}\"`);\n            }\n            if (!building.isBuilding?.()) {\n                throw new Error(`Owned object \"${building.name}\"#${building.id} is not a building`);\n            }\n            if (!building.isSpawned) {\n                throw new Error(`Owned building \"${building.name}\"#${building.id} is not spawned`);\n            }\n            return building;\n        };\n        const resolveOwnedBuildingByName = (buildingName: string) => {\n            const building = localPlayer\n                .getOwnedObjects()\n                .find((ownedObject: any) => ownedObject.name === buildingName && ownedObject.isBuilding?.() && ownedObject.isSpawned);\n            if (!building) {\n                throw new Error(`No spawned owned building found with name \"${buildingName}\"`);\n            }\n            return building;\n        };\n        const projectWorldPointToCanvasPoint = (worldPoint: THREE.Vector3) => {\n            if (!worldScene?.camera || !worldScene?.viewport) {\n                throw new Error('World scene camera or viewport is not available');\n            }\n            const projected = worldPoint.clone().project(worldScene.camera);\n            const viewportPoint = {\n                x: worldScene.viewport.x + ((projected.x + 1) / 2) * worldScene.viewport.width,\n                y: worldScene.viewport.y + ((1 - projected.y) / 2) * worldScene.viewport.height,\n            };\n            const resolvedViewportPoint = {\n                x: Math.max(worldScene.viewport.x, Math.min(worldScene.viewport.x + worldScene.viewport.width - 1, viewportPoint.x)),\n                y: Math.max(worldScene.viewport.y, Math.min(worldScene.viewport.y + worldScene.viewport.height - 1, viewportPoint.y)),\n            };\n            const canvas = this.renderer.getCanvas?.() ?? document.querySelector('canvas');\n            const rect = canvas?.getBoundingClientRect?.() ?? { left: 0, top: 0 };\n            return {\n                viewportX: resolvedViewportPoint.x,\n                viewportY: resolvedViewportPoint.y,\n                x: rect.left + resolvedViewportPoint.x,\n                y: rect.top + resolvedViewportPoint.y,\n            };\n        };\n        const getOwnedUnitClickPoint = (unit: any) => {\n            if (!renderableManager) {\n                throw new Error('Renderable manager is not available');\n            }\n            const renderable = renderableManager.getRenderableByGameObject(unit);\n            if (!renderable) {\n                throw new Error(`Renderable not found for unit \"${unit.name}\"#${unit.id}`);\n            }\n            const renderablePosition = renderable.getPosition?.()?.clone?.() ?? unit.position.worldPosition.clone();\n            return {\n                unitId: unit.id,\n                ...projectWorldPointToCanvasPoint(renderablePosition),\n            };\n        };\n        const getOwnedBuildingClickTargets = (building: any) => {\n            const foundation = building.getFoundation?.() ?? { width: 1, height: 1 };\n            const baseTile = building.tile;\n            if (!baseTile) {\n                throw new Error(`Building \"${building.name}\"#${building.id} does not have a tile`);\n            }\n            const candidatePoints = [];\n            const seen = new Set<string>();\n            const pushTilePoint = (tileX: number, tileY: number, label: string) => {\n                const key = `${tileX}:${tileY}`;\n                if (seen.has(key)) {\n                    return;\n                }\n                seen.add(key);\n                const worldPoint = Coords.tile3dToWorld(tileX + 0.5, tileY + 0.5, baseTile.z);\n                candidatePoints.push({\n                    label,\n                    tile: { rx: tileX, ry: tileY, z: baseTile.z },\n                    ...projectWorldPointToCanvasPoint(new THREE.Vector3(worldPoint.x, worldPoint.y, worldPoint.z)),\n                });\n            };\n            pushTilePoint(baseTile.rx + Math.floor((foundation.width - 1) / 2), baseTile.ry + Math.floor((foundation.height - 1) / 2), 'center');\n            pushTilePoint(baseTile.rx, baseTile.ry, 'topLeft');\n            pushTilePoint(baseTile.rx + foundation.width - 1, baseTile.ry, 'topRight');\n            pushTilePoint(baseTile.rx, baseTile.ry + foundation.height - 1, 'bottomLeft');\n            pushTilePoint(baseTile.rx + foundation.width - 1, baseTile.ry + foundation.height - 1, 'bottomRight');\n            return {\n                buildingId: building.id,\n                buildingName: building.name,\n                candidates: candidatePoints,\n                centerScreenPoint: candidatePoints[0],\n            };\n        };\n        const resolveSidebarTechnoSlot = (technoName: string) => {\n            const sidebarModel = (this.playerUi as any)?.sidebarModel;\n            const sidebarCard = (this.hud as any)?.sidebarCard;\n            const uiScene = this.uiScene;\n            if (!sidebarModel || !sidebarCard) {\n                throw new Error('Sidebar model or sidebar card is not available');\n            }\n            if (!uiScene?.viewport) {\n                throw new Error('UI scene viewport is not available');\n            }\n            const targetTabId = sidebarModel.tabs.findIndex((tab: any) => tab.items.some((item: any) => item.target?.rules?.name === technoName));\n            if (targetTabId === -1) {\n                throw new Error(`No sidebar item found for techno \"${technoName}\"`);\n            }\n            sidebarModel.selectTab(targetTabId);\n            const itemIndex = sidebarModel.activeTab.items.findIndex((item: any) => item.target?.rules?.name === technoName);\n            if (itemIndex === -1) {\n                throw new Error(`Sidebar techno \"${technoName}\" is not available in the active tab`);\n            }\n            const normalizedOffset = itemIndex - (itemIndex % 2);\n            if ((sidebarCard as any).pagingOffset !== normalizedOffset) {\n                sidebarCard.scrollToOffset?.(normalizedOffset);\n            }\n            sidebarCard.updateSlots?.(sidebarModel.activeTab.items, sidebarCard.props?.slots ?? 0);\n            const slotIndex = itemIndex - ((sidebarCard as any).pagingOffset ?? 0);\n            const slotContainer = sidebarCard.slotContainers?.[slotIndex];\n            if (!slotContainer?.get3DObject) {\n                throw new Error(`Sidebar slot ${slotIndex} is not available for techno \"${technoName}\"`);\n            }\n            return {\n                sidebarModel,\n                sidebarCard,\n                uiScene,\n                targetTabId,\n                itemIndex,\n                slotIndex,\n                slotContainer,\n                slotSize: sidebarCard.getSlotSize?.() ?? {\n                    width: sidebarCard.props?.slotSize?.width ?? sidebarCard.props?.cameoImages?.width ?? 0,\n                    height: sidebarCard.props?.slotSize?.height ?? sidebarCard.props?.cameoImages?.height ?? 0,\n                },\n                cameoSize: {\n                    width: sidebarCard.props?.cameoImages?.width ?? 0,\n                    height: sidebarCard.props?.cameoImages?.height ?? 0,\n                },\n            };\n        };\n        const getSidebarTechnoClickPointByName = (technoName: string) => {\n            const { uiScene, targetTabId, itemIndex, slotIndex, slotContainer, slotSize, } = resolveSidebarTechnoSlot(technoName);\n            const clickWorldPoint = new THREE.Vector3(slotSize.width / 2, slotSize.height / 2, 0);\n            slotContainer.get3DObject().localToWorld(clickWorldPoint);\n            const camera = uiScene.getCamera?.() ?? (uiScene as any).camera;\n            const projected = clickWorldPoint.project(camera);\n            const viewport = uiScene.viewport;\n            const viewportPoint = {\n                x: viewport.x + ((projected.x + 1) / 2) * viewport.width,\n                y: viewport.y + ((1 - projected.y) / 2) * viewport.height,\n            };\n            const resolvedViewportPoint = {\n                x: Math.max(viewport.x, Math.min(viewport.x + viewport.width - 1, viewportPoint.x)),\n                y: Math.max(viewport.y, Math.min(viewport.y + viewport.height - 1, viewportPoint.y)),\n            };\n            const canvas = this.renderer.getCanvas?.() ?? document.querySelector('canvas');\n            const rect = canvas?.getBoundingClientRect?.() ?? { left: 0, top: 0 };\n            return {\n                technoName,\n                tabId: targetTabId,\n                itemIndex,\n                slotIndex,\n                viewportX: resolvedViewportPoint.x,\n                viewportY: resolvedViewportPoint.y,\n                x: rect.left + resolvedViewportPoint.x,\n                y: rect.top + resolvedViewportPoint.y,\n            };\n        };\n        const getSidebarTechnoDebugStateByName = (technoName: string) => {\n            const { sidebarModel, sidebarCard, targetTabId, itemIndex, slotIndex, slotContainer, slotSize, cameoSize, } = resolveSidebarTechnoSlot(technoName);\n            const slotObject = sidebarCard.slotObjects?.[slotIndex];\n            const labelObject = sidebarCard.labelObjects?.[slotIndex];\n            const quantityObject = sidebarCard.quantityObjects?.[slotIndex];\n            const tagObject = sidebarCard.tagObjects?.[slotIndex];\n            const container3D = slotContainer.get3DObject();\n            const containerWorldPosition = new THREE.Vector3();\n            container3D.getWorldPosition(containerWorldPosition);\n            const getFrame = (uiObject: any) => typeof uiObject?.getFrame === 'function' ? uiObject.getFrame() : undefined;\n            const getVisible = (uiObject: any) => Boolean(uiObject?.get3DObject?.()?.visible);\n            const getPosition = (uiObject: any) => typeof uiObject?.getPosition === 'function' ? uiObject.getPosition() : undefined;\n            return {\n                technoName,\n                tabId: targetTabId,\n                activeTabId: sidebarModel.activeTabId,\n                itemIndex,\n                slotIndex,\n                pagingOffset: sidebarCard.pagingOffset ?? 0,\n                slotTooltip: container3D.userData?.tooltip,\n                width: sidebarCard.props?.cameoImages?.width ?? 0,\n                height: sidebarCard.props?.cameoImages?.height ?? 0,\n                slotSize,\n                cameoSize,\n                containerPosition: slotContainer.getPosition?.() ?? undefined,\n                containerWorldPosition: {\n                    x: containerWorldPosition.x,\n                    y: containerWorldPosition.y,\n                    z: containerWorldPosition.z,\n                },\n                centerScreenPoint: getSidebarTechnoClickPointByName(technoName),\n                slotFrame: getFrame(slotObject),\n                label: {\n                    visible: getVisible(labelObject),\n                    frame: getFrame(labelObject),\n                    position: getPosition(labelObject),\n                },\n                quantity: {\n                    visible: getVisible(quantityObject),\n                    frame: getFrame(quantityObject),\n                    position: getPosition(quantityObject),\n                },\n                tag: {\n                    visible: getVisible(tagObject),\n                    frame: getFrame(tagObject),\n                    position: getPosition(tagObject),\n                },\n            };\n        };\n        const spawnOwnedUnitCopiesById = (unitId: number, count: number, maxDistance: number = 6) => {\n            if (!Number.isInteger(count) || count <= 0) {\n                throw new Error(`count must be a positive integer, got \"${count}\"`);\n            }\n            const sourceUnit = resolveOwnedUnitById(unitId);\n            if (!sourceUnit.isUnit?.()) {\n                throw new Error(`Unit \"${sourceUnit.name}\"#${sourceUnit.id} is not a unit`);\n            }\n            const canSpawnAtTile = (tile: any) => !game.map.tileOccupation.getObjectsOnTile(tile).length &&\n                game.map.terrain.getPassableSpeed(tile, sourceUnit.rules.speedType, sourceUnit.isInfantry?.() ?? false, false) > 0 &&\n                !game.map.terrain.findObstacles({ tile, onBridge: undefined }, sourceUnit).length;\n            const finder = new RadialTileFinder(game.map.tiles, game.map.mapBounds, sourceUnit.tile, sourceUnit.getFoundation?.() ?? { width: 1, height: 1 }, 1, maxDistance, canSpawnAtTile);\n            const spawnedUnits = [];\n            for (let index = 0; index < count; index += 1) {\n                const spawnTile = finder.getNextTile();\n                if (!spawnTile) {\n                    throw new Error(`Unable to find enough spawn tiles near unit \"${sourceUnit.name}\"#${sourceUnit.id}. Spawned ${spawnedUnits.length}/${count}.`);\n                }\n                const spawnedUnit = game.createUnitForPlayer(sourceUnit.rules, localPlayer);\n                game.spawnObject(spawnedUnit, spawnTile);\n                spawnedUnits.push(spawnedUnit);\n            }\n            console.log('[GameScreen.debug] spawned owned unit copies', spawnedUnits.map((unit: any) => serializeOwnedUnit(unit)));\n            return spawnedUnits.map((unit: any) => serializeOwnedUnit(unit));\n        };\n        const despawnOwnedUnitsByIds = (unitIds: number[]) => {\n            const despawnedUnits = unitIds.map((unitId) => {\n                const unit = resolveOwnedUnitById(unitId);\n                game.unspawnObject(unit);\n                unit.dispose();\n                return serializeOwnedUnit(unit);\n            });\n            console.log('[GameScreen.debug] despawned owned units', despawnedUnits);\n            return despawnedUnits;\n        };\n        debugRoot.helpers = {\n            getSelectedUnitIds: () => game.getUnitSelection().getSelectedUnits().map((unit: any) => unit.id),\n            getOwnedUnits: () => localPlayer.getOwnedObjects().map((unit: any) => serializeOwnedUnit(unit)),\n            getOwnedUnitClickPointById: (unitId: number) => getOwnedUnitClickPoint(resolveOwnedUnitById(unitId)),\n            getOwnedUnitClickPointByName: (unitName: string) => {\n                return getOwnedUnitClickPoint(resolveOwnedUnitByName(unitName));\n            },\n            getOwnedBuildingClickTargetsById: (buildingId: number) => getOwnedBuildingClickTargets(resolveOwnedBuildingById(buildingId)),\n            getOwnedBuildingClickTargetsByName: (buildingName: string) => getOwnedBuildingClickTargets(resolveOwnedBuildingByName(buildingName)),\n            getSidebarTechnoClickPointByName: (technoName: string) => getSidebarTechnoClickPointByName(technoName),\n            getSidebarTechnoDebugStateByName: (technoName: string) => getSidebarTechnoDebugStateByName(technoName),\n            spawnOwnedUnitCopiesById: (unitId: number, count: number, maxDistance?: number) => spawnOwnedUnitCopiesById(unitId, count, maxDistance),\n            spawnOwnedUnitCopiesByName: (unitName: string, count: number, maxDistance?: number) => spawnOwnedUnitCopiesById(resolveOwnedUnitByName(unitName).id, count, maxDistance),\n            despawnOwnedUnitsByIds: (unitIds: number[]) => despawnOwnedUnitsByIds(unitIds),\n            selectOwnedUnitByName: (unitName: string) => {\n                const unit = resolveOwnedUnitByName(unitName);\n                game.getUnitSelection().deselectAll();\n                game.getUnitSelection().addToSelection(unit);\n                return unit.id;\n            },\n            deploySelectedUnits: () => {\n                const selectedUnits = game.getUnitSelection().getSelectedUnits();\n                if (!selectedUnits.length) {\n                    throw new Error('No selected units to deploy');\n                }\n                actionsApi.orderUnits(selectedUnits.map((unit: any) => unit.id), OrderType.DeploySelected);\n                return selectedUnits.map((unit: any) => unit.id);\n            },\n            activateSellMode: () => {\n                const sellMode = (this.playerUi as any)?.sellMode;\n                if (!sellMode || !worldInteraction) {\n                    throw new Error('Sell mode or world interaction is not available');\n                }\n                worldInteraction.setMode(sellMode);\n                return true;\n            },\n            isSellModeActive: () => {\n                const sellMode = (this.playerUi as any)?.sellMode;\n                return Boolean(sellMode && worldInteraction?.getMode?.() === sellMode);\n            },\n            getVictoryBlockers: () => getVictoryBlockers(),\n        };\n        this.pointer.setVisible(true);\n        const gameEndHandler = () => this.onGameEnd(game, localPlayer, eva, replay);\n        game.onEnd.subscribe(gameEndHandler);\n        this.disposables.add(() => game.onEnd.unsubscribe(gameEndHandler));\n        game.start?.();\n        if (this.usesServerConnection()) {\n            this.initNetStats(localPlayer);\n        }\n        this.gameAnimationLoop = new GameAnimationLoop(localPlayer, this.renderer, this.sound, this.gameTurnMgr, {\n            skipFrames: true,\n            skipBudgetMillis: 8,\n            onError: this.config.devMode ? undefined : (error: any, isCritical?: boolean) => this.handleError(error, this.strings.get('TS:GameCrashed') +\n                (isCritical || game.gameOpts.mapOfficial\n                    ? ''\n                    : '\\n\\n' + this.strings.get('TS:CustomMapCrash')), isCritical)\n        });\n        this.uiAnimationLoop.stop();\n        this.gameAnimationLoop.start();\n    }\n    private initNetStats(localPlayer: any): void {\n        const pingMonitor = new PingMonitor(this.gameTurnMgr, this.gservCon, this.avgPing);\n        pingMonitor.monitor();\n        this.disposables.add(pingMonitor);\n    }\n    private initUi(localPlayer: any, game: any, replayRecorder: any, actionQueue: any, actionFactory: any, hud: any, eva: any, uiInitResult: any): void {\n        const { messageList, chatHistory } = uiInitResult;\n        const soundHandler = new SoundHandler(game, uiInitResult.worldViewInitResult.worldSound, eva, this.sound, game.events, messageList, this.strings, localPlayer);\n        soundHandler.init?.();\n        this.disposables.add(soundHandler);\n        this.uiScene.add(hud);\n        const menu = this.menu = new GameMenu(this.gameMenuSubScreens, game, localPlayer, chatHistory, this.gservCon, this.isSinglePlayer, this.isTournament);\n        menu.init(hud);\n        this.initGameMenuEvents(menu, eva, game, localPlayer, actionQueue, actionFactory);\n        this.disposables.add(menu, () => this.menu = undefined);\n        if (localPlayer.isObserver) {\n            const worldScene = uiInitResult.worldViewInitResult.worldScene;\n            const renderableManager = uiInitResult.worldViewInitResult.renderableManager;\n            const worldInteractionFactory = new A.WorldInteractionFactory(undefined, game, game.unitSelection, renderableManager, this.uiScene, worldScene, this.pointer, this.renderer, this.keyBinds, this.generalOptions, this.runtimeVars.freeCamera, this.runtimeVars.debugPaths, this.config.devMode, document, this.minimap, this.strings, hud.getTextColor?.(), this.runtimeVars.debugText, this.battleControlApi);\n            this.playerUi = new ObserverUi(game, undefined, this.sidebarModel, this.replay, this.renderer, worldScene, this.sound, worldInteractionFactory, menu, this.runtimeVars, this.strings, renderableManager, this.messageBoxApi, this.config.discordUrl);\n        }\n        else {\n            const worldScene = uiInitResult.worldViewInitResult.worldScene;\n            const superWeaponFxHandler = uiInitResult.worldViewInitResult.superWeaponFxHandler;\n            const beaconFxHandler = uiInitResult.worldViewInitResult.beaconFxHandler;\n            const renderableManager = uiInitResult.worldViewInitResult.renderableManager;\n            const textColor = hud.getTextColor?.();\n            const worldInteractionFactory = new A.WorldInteractionFactory(localPlayer, game, game.unitSelection, renderableManager, this.uiScene, worldScene, this.pointer, this.renderer, this.keyBinds, this.generalOptions, this.runtimeVars.freeCamera, this.runtimeVars.debugPaths, this.config.devMode, document, this.minimap, this.strings, textColor, game.debugText, this.battleControlApi);\n            this.playerUi = new CombatantUi(game, localPlayer, this.isSinglePlayer, actionQueue, actionFactory, this.sidebarModel, this.renderer, worldScene, soundHandler, messageList, this.sound, eva, worldInteractionFactory, menu, this.pointer, this.runtimeVars, this.speedCheat, this.strings, undefined, renderableManager, superWeaponFxHandler, beaconFxHandler, this.messageBoxApi, this.config.discordUrl);\n        }\n        this.playerUi.init?.(hud);\n        this.disposables.add(this.playerUi, () => this.playerUi = undefined);\n        if (this.usesServerConnection()) {\n            const chatNetHandler = new ChatNetHandler(this.gservCon, this.wolService, messageList, chatHistory, new ChatMessageFormat(this.strings, localPlayer.name), localPlayer, game, this.replayRecorderInstance, this.mutedPlayers ?? new Set<string>());\n            chatNetHandler.init();\n            const worldInteraction = this.playerUi.worldInteraction;\n            const chatTypingHandler = new ChatTypingHandler(worldInteraction.keyboardHandler, worldInteraction.arrowScrollHandler, messageList, chatHistory);\n            this.chatTypingHandler = chatTypingHandler;\n            this.chatNetHandler = chatNetHandler;\n            this.disposables.add(() => {\n                this.chatTypingHandler = this.chatNetHandler = undefined;\n            });\n            this.initHudChatTypingEvents(chatTypingHandler, chatNetHandler, hud);\n        }\n    }\n    private initGameMenuEvents(menu: any, eva: any, game: any, localPlayer: any, actionQueue: any, actionFactory: any): void {\n        menu.onOpen.subscribe(() => {\n            this.pointer.unlock();\n            this.playerUi.worldInteraction.setEnabled(false);\n            if (this.isSinglePlayer) {\n                this.pausedAtSpeed = game.speed.value;\n                game.desiredSpeed.value = Number.EPSILON;\n                this.mixer.setMuted(ChannelType.Effect, true);\n                this.mixer.setMuted(ChannelType.Ambient, true);\n            }\n        });\n        menu.onQuit.subscribe(async () => {\n            console.log('[Quit] onQuit start', {\n                isSinglePlayer: this.isSinglePlayer,\n                pausedAtSpeed: this.pausedAtSpeed\n            });\n            if (!this.controller)\n                return;\n            if (this.isSinglePlayer && this.pausedAtSpeed) {\n                this.mixer.setMuted(ChannelType.Effect, false);\n                this.mixer.setMuted(ChannelType.Ambient, false);\n            }\n            if (!localPlayer.isObserver) {\n                console.log('[Quit] play EVA_BattleControlTerminated');\n                eva.play('EVA_BattleControlTerminated');\n            }\n            this.pointer.lock();\n            this.pointer.setVisible(false);\n            this.playerUi.dispose();\n            if (!localPlayer.isObserver && !this.isSinglePlayer && !this.lagState) {\n                actionQueue.push(actionFactory.create(ActionType.ResignGame));\n                await new Promise<void>((resolve) => {\n                    this.gameTurnMgr.onActionsSent.subscribeOnce(() => resolve());\n                });\n            }\n            if (this.isLanGame) {\n                this.lanMatchSession?.leaveRoom();\n            }\n            if (this.usesServerConnection()) {\n                try {\n                    this.gservCon.onClose.unsubscribe(this.onGservClose);\n                    this.gservCon.close();\n                }\n                catch (e) {\n                    console.warn('[Quit] gservCon close skipped', e);\n                }\n            }\n            this.gameTurnMgr.dispose();\n            if (this.replay) {\n                this.replay.finish(this.game.currentTick);\n                this.saveReplay(this.replay);\n            }\n            if (this.usesServerConnection()) {\n                this.sendGameRes(game, {\n                    disconnect: false,\n                    desync: false,\n                    quit: true,\n                    finished: false\n                });\n            }\n            if (!localPlayer.isObserver) {\n                this.logGame(game, false);\n            }\n            console.log('[Quit] waiting before navigate');\n            await sleep(2000);\n            console.log('[Quit] navigating to Score');\n            this.controller?.goToScreen(ScreenType.MainMenuRoot, {\n                route: new MainMenuRoute(MainMenuScreenType.Score, {\n                    game,\n                    localPlayer,\n                    singlePlayer: this.isSinglePlayer,\n                    tournament: this.isTournament,\n                    returnTo: this.returnTo ?? new MainMenuRoute(MainMenuScreenType.Home, undefined)\n                })\n            });\n        });\n        menu.onObserve.subscribe(() => {\n            this.pointer.lock();\n            this.playerUi.worldInteraction.setEnabled(true);\n            actionQueue.push(actionFactory.create(ActionType.ObserveGame));\n            this.logGame(game, false);\n        });\n        menu.onCancel.subscribe(() => {\n            this.pointer.lock();\n            this.playerUi.worldInteraction.setEnabled(true);\n            if (this.isSinglePlayer && this.pausedAtSpeed) {\n                game.desiredSpeed.value = this.pausedAtSpeed;\n                this.gameTurnMgr.doGameTurn(performance.now());\n                this.pausedAtSpeed = undefined;\n                this.mixer.setMuted(ChannelType.Effect, false);\n                this.mixer.setMuted(ChannelType.Ambient, false);\n            }\n        });\n    }\n    private async onGameEnd(game: any, localPlayer: any, eva: any, replay: any): Promise<void> {\n        if (this.gameEndHandled) {\n            return;\n        }\n        this.gameEndHandled = true;\n\n        let gameResultPopup: any;\n\n        try {\n            const isObserver = Boolean(localPlayer?.isObserver);\n            const isVictory = !localPlayer?.defeated ||\n                game?.alliances?.getAllies(localPlayer)?.some((ally: any) => !ally.defeated);\n\n            console.log('[GameScreen] onGameEnd', {\n                singlePlayer: this.isSinglePlayer,\n                isVictory,\n                localPlayer: localPlayer?.name,\n                status: game?.status,\n                gservConAvailable: Boolean(this.gservCon)\n            });\n\n            if (this.jsxRenderer && this.viewport) {\n                [gameResultPopup] = this.jsxRenderer.render(jsx(GameResultPopup, {\n                    type: isVictory && !isObserver\n                        ? GameResultType.MpVictory\n                        : GameResultType.MpDefeat,\n                    viewport: this.viewport.value\n                }));\n            }\n\n            this.pointer?.setVisible(false);\n            this.gameTurnMgr?.setErrorState?.();\n            this.gameAnimationLoop?.stop?.();\n            if (this.isLanGame) {\n                this.lanMatchSession?.leaveRoom();\n            }\n\n            if (this.usesServerConnection() && this.gservCon) {\n                this.gservCon.onClose.unsubscribe(this.onGservClose);\n                this.gservCon.close();\n            }\n\n            if (gameResultPopup) {\n                this.uiScene?.add(gameResultPopup);\n            }\n\n            if (!isObserver) {\n                eva?.play?.(isVictory ? 'EVA_YouAreVictorious' : 'EVA_YouHaveLost', true);\n            }\n\n            if (replay) {\n                replay.finish(game?.currentTick ?? 0);\n                this.saveReplay(replay);\n            }\n\n            if (this.usesServerConnection() && game) {\n                this.sendGameRes(game, {\n                    disconnect: false,\n                    desync: false,\n                    quit: false,\n                    finished: !game.alliances.getHostilePlayers().length\n                });\n            }\n\n            if (!isObserver && game) {\n                this.logGame(game, Boolean(isVictory));\n            }\n\n            await sleep(5000);\n\n            if (gameResultPopup) {\n                this.uiScene?.remove(gameResultPopup);\n                gameResultPopup.destroy?.();\n            }\n\n            const route = localPlayer\n                ? new MainMenuRoute(MainMenuScreenType.Score, {\n                    game,\n                    localPlayer,\n                    singlePlayer: this.isSinglePlayer,\n                    tournament: this.isTournament,\n                    returnTo: this.returnTo ?? new MainMenuRoute(MainMenuScreenType.Home, undefined)\n                })\n                : new MainMenuRoute(MainMenuScreenType.Home, undefined);\n\n            this.controller?.goToScreen(ScreenType.MainMenuRoot, { route });\n        }\n        catch (error) {\n            console.error('[GameScreen] onGameEnd failed', error);\n            if (gameResultPopup) {\n                this.uiScene?.remove(gameResultPopup);\n                gameResultPopup.destroy?.();\n            }\n            this.controller?.goToScreen(ScreenType.MainMenuRoot, {\n                route: new MainMenuRoute(MainMenuScreenType.Home, undefined)\n            });\n        }\n    }\n    private logGame(game: any, won: boolean): void {\n        (window as any).gtag?.('event', 'game_finish', {\n            singlePlayer: Number(this.isSinglePlayer),\n            numPlayers: game.gameOpts.humanPlayers.filter((p: any) => p.countryId !== OBS_COUNTRY_ID).length +\n                game.gameOpts.aiPlayers.filter((p: any) => !!p).length,\n            won: Number(won),\n            tournament: Number(this.isTournament),\n            duration: game.currentTime\n        });\n    }\n    private handleGservConError(error: any): void {\n        if (error instanceof OperationCanceledError) {\n            return;\n        }\n        let errorMessage = this.strings.get('WOL:MatchBadParameters');\n        if (error instanceof IrcConnection.SocketError) {\n            return;\n        }\n        if (error instanceof IrcConnection.ConnectError) {\n            errorMessage = this.strings.get('TS:ConnectFailed');\n        }\n        this.handleError(error, errorMessage);\n    }\n    private handleMapLoadError(error: any, mapName: string): void {\n        if (error instanceof OperationCanceledError || error instanceof IrcConnection.SocketError) {\n            return;\n        }\n        let errorMessage = this.strings.get('TXT_MAP_ERROR');\n        const message = typeof error === 'string' ? error : error.message;\n        if (message?.match(/memory|allocation/i)) {\n            errorMessage = this.strings.get('TS:GameInitOom');\n        }\n        this.handleError(error, errorMessage);\n    }\n    private handleGameLoadError(error: any, params: any, gameOpts: any): void {\n        if (error instanceof OperationCanceledError || error instanceof IrcConnection.SocketError) {\n            return;\n        }\n        let errorMessage = this.strings.get('TS:GameInitError');\n        const message = typeof error === 'string' ? error : error.message;\n        if (message?.match(/memory|allocation/i)) {\n            errorMessage = this.strings.get('TS:GameInitOom');\n        }\n        else if (!gameOpts.mapOfficial) {\n            errorMessage += '\\n\\n' + this.strings.get('TS:CustomMapCrash');\n        }\n        this.handleError(error, errorMessage);\n    }\n    private handleGameError(error: any, message: string, game: any, debugDataProvider?: () => Promise<any>, isCustomMap?: boolean): void {\n        const replay = this.replay;\n        if (replay) {\n            this.saveReplay(replay);\n        }\n        this.handleError(error, message, isCustomMap);\n        if (error === 'desync_error' && this.usesServerConnection()) {\n            this.sendGameRes(game, {\n                disconnect: false,\n                desync: true,\n                quit: false,\n                finished: false\n            });\n        }\n    }\n    private sendDebugInfo(error: any, { gameId, replay, map, official }: {\n        gameId?: string;\n        replay?: any;\n        map?: any;\n        official?: boolean;\n    } = {}, debugDataProvider?: () => Promise<any>): void {\n        console.error('Game error:', error, { gameId, official });\n    }\n    private sendGameRes(game: any, result: any): void {\n        console.log('Game result:', { game: game.id, result });\n    }\n    private getGameResClientInfo(result: any): any {\n        return {\n            clientVers: this.engineVersion,\n            avgFps: 0,\n            avgRtt: this.avgPing.calculate() ?? 0,\n            outOfSync: result.desync,\n            gameSku: this.wolService.getConfig().getClientSku(),\n            accountName: this.playerName,\n            suddenDisconnect: result.disconnect,\n            quit: result.quit,\n            finished: result.finished,\n            pingsRecv: 0,\n            pingsSent: 0\n        };\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/HudFactory.ts",
    "content": "import { Hud } from '@/gui/screen/game/component/Hud';\nimport { Engine } from '@/engine/Engine';\nexport class HudFactory {\n    constructor(private sideType: any, private viewport: any, private sidebarModel: any, private messageList: any, private chatHistory: any, private debugText: any, private debugTextEnabled: any, private localPlayer: any, private players: any, private stalemateDetectTrait: any, private countdownTimer: any, private cameoFilenames: any, private jsxRenderer: any, private strings: any, private commandBarButtons: any, private persistentHoverTags: any) { }\n    setSidebarModel(sidebarModel: any): void {\n        this.sidebarModel = sidebarModel;\n    }\n    setViewport(viewport: any): void {\n        this.viewport = viewport;\n    }\n    create(): Hud {\n        return new Hud(this.sideType, this.viewport, Engine.getImages() as any, Engine.getPalettes() as any, this.cameoFilenames, this.sidebarModel, this.messageList, this.chatHistory, this.debugText, this.debugTextEnabled, this.localPlayer, this.players, this.stalemateDetectTrait, this.countdownTimer, this.jsxRenderer, this.strings, this.commandBarButtons, this.persistentHoverTags);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/MapFileLoader.ts",
    "content": "import { FileNotFoundError } from '@/data/vfs/FileNotFoundError';\nimport { VirtualFile } from '@/data/vfs/VirtualFile';\nexport class MapFileLoader {\n    constructor(private resourceLoader: any, private vfs?: any) { }\n    async load(filename: string, cancellationToken?: any): Promise<VirtualFile> {\n        let mapFile: VirtualFile | undefined;\n        if (this.vfs) {\n            try {\n                mapFile = await this.vfs.openFileWithRfs(filename);\n            }\n            catch (error) {\n                if (!(error instanceof FileNotFoundError)) {\n                    console.error(error);\n                }\n            }\n        }\n        if (!mapFile) {\n            const bytes = await this.resourceLoader.loadBinary(filename, cancellationToken);\n            mapFile = VirtualFile.fromBytes(bytes, filename);\n        }\n        return mapFile;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/MedianPing.ts",
    "content": "export class MedianPing {\n    private reservoir: number[] = [];\n    private totalSamples: number = 0;\n    constructor(private reservoirSize: number = 100) { }\n    pushSample(sample: number): void {\n        this.totalSamples++;\n        if (this.reservoir.length < this.reservoirSize) {\n            this.reservoir.push(sample);\n        }\n        else {\n            const randomIndex = Math.floor(Math.random() * this.totalSamples);\n            if (randomIndex < this.reservoirSize) {\n                this.reservoir[randomIndex] = sample;\n            }\n        }\n    }\n    calculate(): number | undefined {\n        if (this.reservoir.length === 0) {\n            return undefined;\n        }\n        const sorted = [...this.reservoir].sort((a, b) => a - b);\n        const midIndex = Math.floor(sorted.length / 2);\n        if (sorted.length % 2 === 1) {\n            return sorted[midIndex];\n        }\n        else {\n            return (sorted[midIndex - 1] + sorted[midIndex]) / 2;\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/NetStats.ts",
    "content": "import Stats from 'stats.js';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nexport class NetStats {\n    private disposables = new CompositeDisposable();\n    constructor(private lockstep: any, private player: any, private renderer: any, private pingMonitor: any) { }\n    init(): void {\n        const stats = this.renderer.getStats();\n        const rttPanel = new Stats.Panel('ms RTT', '#ff8', '#221');\n        const maxRtt = 250;\n        const onNewPingSample = (rtt: number) => {\n            requestAnimationFrame(() => rttPanel.update(rtt, maxRtt));\n        };\n        this.pingMonitor.onNewSample.subscribe(onNewPingSample);\n        this.disposables.add(() => {\n            this.pingMonitor.onNewSample.unsubscribe(onNewPingSample);\n            const currentStats = this.renderer.getStats();\n            if (currentStats && rttPanel.dom) {\n                currentStats.dom.removeChild(rttPanel.dom);\n            }\n        });\n        stats.addPanel(rttPanel);\n        if (!this.player.isObserver) {\n            const latencyPanel = new Stats.Panel('ms LAT', '#f8f', '#212');\n            const actionTimestamps = new Map<any, number>();\n            this.lockstep.onActionsSent.subscribe((actionId: any) => {\n                actionTimestamps.set(actionId, performance.now());\n            });\n            this.lockstep.onActionsReceived.subscribe((actionId: any) => {\n                if (actionTimestamps.has(actionId)) {\n                    const latency = performance.now() - actionTimestamps.get(actionId)!;\n                    actionTimestamps.delete(actionId);\n                    requestAnimationFrame(() => latencyPanel.update(latency, 1000));\n                }\n            });\n            stats.addPanel(latencyPanel);\n            this.disposables.add(() => {\n                const currentStats = this.renderer.getStats();\n                if (currentStats && latencyPanel.dom) {\n                    currentStats.dom.removeChild(latencyPanel.dom);\n                }\n            });\n        }\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/ObserverUi.ts",
    "content": "import React from 'react';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { EventDispatcher } from '@/util/event';\nimport { SoundKey } from '@/engine/sound/SoundKey';\nimport { ChannelType } from '@/engine/sound/ChannelType';\nimport { KeyCommandType } from '@/gui/screen/game/worldInteraction/keyboard/KeyCommandType';\nexport class ObserverUi {\n    private disposables = new CompositeDisposable();\n    private _onPlayerChange = new EventDispatcher<ObserverUi, {\n        player: any;\n        sidebarModel: any;\n    }>();\n    public worldInteraction?: any;\n    get onPlayerChange() {\n        return this._onPlayerChange.asEvent();\n    }\n    constructor(private game: any, private player: any, private sidebarModel: any, private replay: any, private renderer: any, private worldScene: any, private sound: any, private worldInteractionFactory: any, private gameMenu: any, private runtimeVars: any, private strings: any, private renderableManager: any, private messageBoxApi: any, private discordUrl?: string) { }\n    init(hud: any): void {\n        this.worldInteraction = this.worldInteractionFactory.create();\n        this.worldInteraction.init();\n        this.disposables.add(this.worldInteraction);\n        this.initKeyboardCommands(this.worldInteraction);\n        this.initGameEventListeners();\n        this.initHudEventListeners(hud);\n    }\n    handleHudChange(hud: any): void {\n        this.initHudEventListeners(hud);\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n    private initGameEventListeners(): void {\n        const world = this.game.getWorld();\n        const updateSidebar = () => {\n        };\n        world.onObjectSpawned.subscribe(updateSidebar);\n        world.onObjectRemoved.subscribe(updateSidebar);\n        this.disposables.add(() => world.onObjectSpawned.unsubscribe(updateSidebar), () => world.onObjectRemoved.unsubscribe(updateSidebar));\n        this.disposables.add(this.game.events.subscribe((event: any) => {\n            if (event.type === 'PowerChange' && event.target === this.player) {\n                this.sidebarModel.powerGenerated = event.power;\n                this.sidebarModel.powerDrained = event.drain;\n            }\n        }));\n    }\n    changePlayer(newPlayer: any): void {\n        if (newPlayer === this.player) {\n            return;\n        }\n        this.player?.production.onQueueUpdate.unsubscribe(this.handleProductionQueueUpdate);\n        this.player = newPlayer;\n        this.player?.production.onQueueUpdate.subscribe(this.handleProductionQueueUpdate);\n        const oldSidebarModel = this.sidebarModel;\n        this.sidebarModel = newPlayer\n            ? this.createCombatantSidebarModel(newPlayer, this.game)\n            : this.createObserverSidebarModel(this.game, this.replay);\n        this.sidebarModel.selectTab(oldSidebarModel.activeTab.id);\n        this.sidebarModel.topTextLeftAlign = oldSidebarModel.topTextLeftAlign;\n        const shroud = newPlayer\n            ? this.game.mapShroudTrait.getPlayerShroud(newPlayer)\n            : undefined;\n        this.worldInteraction?.setShroud(shroud);\n        this._onPlayerChange.dispatch(this, {\n            player: newPlayer,\n            sidebarModel: this.sidebarModel,\n        });\n    }\n    private initHudEventListeners(hud: any): void {\n        hud.onSidebarTabClick.subscribe(() => {\n            this.sound.play(SoundKey.GUITabSound, ChannelType.Ui);\n        });\n        hud.onCommandBarButtonClick.subscribe((buttonType: any) => {\n            switch (buttonType) {\n                case 'BugReport':\n                    if (this.discordUrl) {\n                        this.gameMenu.open();\n                        this.messageBoxApi.show(React.createElement('div', {}, 'Bug Report'), this.strings.get('GUI:OK'));\n                    }\n                    break;\n            }\n        });\n    }\n    private initKeyboardCommands(worldInteraction: any): void {\n        const unitSelection = worldInteraction.unitSelectionHandler;\n        worldInteraction\n            .registerKeyCommand(KeyCommandType.Options, () => this.gameMenu.open())\n            .registerKeyCommand(KeyCommandType.Scoreboard, () => this.gameMenu.openDiplo())\n            .registerKeyCommand(KeyCommandType.VeterancyNav, () => unitSelection.selectByVeterancy())\n            .registerKeyCommand(KeyCommandType.HealthNav, () => unitSelection.selectByHealth())\n            .registerKeyCommand(KeyCommandType.ToggleFps, () => {\n            this.runtimeVars.fps.value = !this.runtimeVars.fps.value;\n        });\n        const playerCommands = [\n            KeyCommandType.TeamSelect_1,\n            KeyCommandType.TeamSelect_2,\n            KeyCommandType.TeamSelect_3,\n            KeyCommandType.TeamSelect_4,\n            KeyCommandType.TeamSelect_5,\n            KeyCommandType.TeamSelect_6,\n            KeyCommandType.TeamSelect_7,\n            KeyCommandType.TeamSelect_8,\n            KeyCommandType.TeamSelect_9,\n            KeyCommandType.TeamSelect_10,\n        ];\n        playerCommands.forEach((command, index) => {\n            worldInteraction.registerKeyCommand(command, () => {\n                const players = this.game.getNonNeutralPlayers();\n                if (players[index]) {\n                    this.changePlayer(players[index]);\n                }\n            });\n        });\n        const tabCommands = new Map([\n            [KeyCommandType.StructureTab, 'Structures'],\n            [KeyCommandType.DefenseTab, 'Armory'],\n            [KeyCommandType.InfantryTab, 'Infantry'],\n            [KeyCommandType.UnitTab, 'Vehicles'],\n        ]);\n        tabCommands.forEach((tabId, command) => {\n            worldInteraction.registerKeyCommand(command, () => {\n                this.sidebarModel.selectTab(tabId);\n            });\n        });\n        this.initCameraCommands(worldInteraction);\n    }\n    private initCameraCommands(worldInteraction: any): void {\n        const cameraLocations = new Map();\n        [\n            KeyCommandType.SetView1,\n            KeyCommandType.SetView2,\n            KeyCommandType.SetView3,\n            KeyCommandType.SetView4,\n        ].forEach((command, index) => {\n            worldInteraction.registerKeyCommand(command, () => {\n                const currentPos = this.worldScene.cameraPan.getPosition();\n                cameraLocations.set(index, currentPos);\n            });\n        });\n        [\n            KeyCommandType.View1,\n            KeyCommandType.View2,\n            KeyCommandType.View3,\n            KeyCommandType.View4,\n        ].forEach((command, index) => {\n            worldInteraction.registerKeyCommand(command, () => {\n                const location = cameraLocations.get(index);\n                if (location) {\n                    this.worldScene.cameraPan.setPosition(location);\n                }\n            });\n        });\n        worldInteraction.registerKeyCommand(KeyCommandType.CenterBase, () => {\n            if (this.player) {\n                const baseLocation = this.findPlayerBase(this.player);\n                if (baseLocation) {\n                    this.worldScene.cameraPan.setPosition(baseLocation);\n                }\n            }\n        });\n    }\n    private findPlayerBase(player: any): any {\n        const buildings = player.getBuildings();\n        if (buildings.length > 0) {\n            return buildings[0].position;\n        }\n        return null;\n    }\n    private handleProductionQueueUpdate = (queue: any): void => {\n        if (this.sidebarModel.updateFromQueue) {\n            this.sidebarModel.updateFromQueue(queue);\n        }\n    };\n    private createCombatantSidebarModel(player: any, game: any): any {\n        return {};\n    }\n    private createObserverSidebarModel(game: any, replay: any): any {\n        return {};\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/PingMonitor.ts",
    "content": "import { IrcConnection } from '@/network/IrcConnection';\nimport { EventDispatcher } from '@/util/event';\nexport class PingMonitor {\n    private _onNewSample = new EventDispatcher<PingMonitor, number>();\n    private isDisposed = false;\n    private pingTimeoutId?: number;\n    get onNewSample() {\n        return this._onNewSample.asEvent();\n    }\n    constructor(private gameTurnMgr: any, private gservCon: any, private avgPing: any, private pingIntervalMillis: number = 1000, private pingTimeoutSeconds: number = 5) { }\n    monitor(): void {\n        this.isDisposed = false;\n        if (!this.pingTimeoutId) {\n            this.pingTimeoutId = window.setTimeout(() => this.updatePing(), this.pingIntervalMillis);\n        }\n    }\n    setPingInterval(intervalMillis: number): void {\n        if (intervalMillis !== this.pingIntervalMillis) {\n            this.pingIntervalMillis = intervalMillis;\n            if (this.pingTimeoutId) {\n                clearTimeout(this.pingTimeoutId);\n                this.updatePing();\n            }\n        }\n    }\n    private async updatePing(): Promise<void> {\n        this.pingTimeoutId = undefined;\n        if (!this.gameTurnMgr.getErrorState() && this.gservCon.isOpen()) {\n            let pingTime: number;\n            try {\n                pingTime = await this.gservCon.ping(this.pingTimeoutSeconds);\n                if (this.isDisposed || this.pingTimeoutId) {\n                    return;\n                }\n            }\n            catch (error) {\n                if (!(error instanceof IrcConnection.NoReplyError)) {\n                    console.error(error);\n                }\n                pingTime = 1000 * this.pingTimeoutSeconds;\n            }\n            this.avgPing.pushSample(pingTime);\n            this._onNewSample.dispatch(this, pingTime);\n            this.pingTimeoutId = window.setTimeout(() => this.updatePing(), this.pingIntervalMillis);\n        }\n    }\n    dispose(): void {\n        if (this.pingTimeoutId) {\n            clearTimeout(this.pingTimeoutId);\n        }\n        this.isDisposed = true;\n        this._onNewSample = new EventDispatcher();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/PlayerUi.ts",
    "content": "export {};\n"
  },
  {
    "path": "src/gui/screen/game/SoundHandler.ts",
    "content": "import { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { EventType } from '@/game/event/EventType';\nimport { SoundKey } from '@/engine/sound/SoundKey';\nimport { ChannelType } from '@/engine/sound/ChannelType';\nimport { Coords } from '@/game/Coords';\nimport { PowerupType } from '@/game/type/PowerupType';\nimport { SuperWeaponType } from '@/game/type/SuperWeaponType';\nimport { RadarEventType } from '@/game/rules/general/RadarRules';\n\nconst detectedSuperWeaponEvaByType = new Map([\n    [SuperWeaponType.MultiMissile, 'EVA_NuclearSiloDetected'],\n    [SuperWeaponType.IronCurtain, 'EVA_IronCurtainDetected'],\n    [SuperWeaponType.ChronoSphere, 'EVA_ChronosphereDetected'],\n    [SuperWeaponType.LightningStorm, 'EVA_WeatherDeviceReady'],\n]);\n\nconst superWeaponReadyEvaByType = new Map([\n    [SuperWeaponType.MultiMissile, 'EVA_NuclearMissileReady'],\n    [SuperWeaponType.IronCurtain, 'EVA_IronCurtainReady'],\n    [SuperWeaponType.ChronoSphere, 'EVA_ChronosphereReady'],\n    [SuperWeaponType.LightningStorm, 'EVA_LightningStormReady'],\n    [SuperWeaponType.ParaDrop, 'EVA_ReinforcementsReady'],\n    [SuperWeaponType.AmerParaDrop, 'EVA_ReinforcementsReady'],\n]);\n\nconst superWeaponActivateEvaByType = new Map([\n    [SuperWeaponType.MultiMissile, 'EVA_NuclearMissileLaunched'],\n    [SuperWeaponType.IronCurtain, 'EVA_IronCurtainActivated'],\n    [SuperWeaponType.ChronoSphere, 'EVA_ChronosphereActivated'],\n    [SuperWeaponType.LightningStorm, 'EVA_LightningStormCreated'],\n]);\n\nconst superWeaponActivateSoundByType = new Map([\n    [SuperWeaponType.MultiMissile, SoundKey.DigSound],\n]);\n\nconst superWeaponActivateMessageByType = new Map([\n    [SuperWeaponType.LightningStorm, 'TXT_LIGHTNING_STORM_APPROACHING'],\n]);\n\nconst crateSoundByType = new Map([\n    [PowerupType.Veteran, SoundKey.CratePromoteSound],\n    [PowerupType.Money, SoundKey.CrateMoneySound],\n    [PowerupType.Reveal, SoundKey.CrateRevealSound],\n    [PowerupType.Firepower, SoundKey.CrateFireSound],\n    [PowerupType.Armor, SoundKey.CrateArmourSound],\n    [PowerupType.Speed, SoundKey.CrateSpeedSound],\n    [PowerupType.Unit, SoundKey.CrateUnitSound],\n]);\n\nconst crateEvaByType = new Map([\n    [PowerupType.Armor, 'EVA_UnitArmorUpgraded'],\n    [PowerupType.Firepower, 'EVA_UnitFirePowerUpgraded'],\n    [PowerupType.Speed, 'EVA_UnitSpeedUpgraded'],\n]);\n\nexport class SoundHandler {\n    private lastAvailableObjectNames: string[] = [];\n    private lastQueueStatuses = new Map();\n    private triggerSoundHandles = new Map();\n    private disposables = new CompositeDisposable();\n    private lastFeedbackTime?: number;\n    constructor(private game: any, private worldSound: any, private eva: any, private sound: any, private gameEvents: any, private messageList: any, private strings: any, private player: any) { }\n    init(): void {\n        this.disposables.add(this.gameEvents.subscribe((event: any) => this.handleGameEvent(event)));\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n    private handleGameEvent(event: any): void {\n        switch (event.type) {\n            case EventType.Cheer:\n                this.sound.play(SoundKey.CheerSound, ChannelType.Effect);\n                break;\n            case EventType.UnitDeployUndeploy:\n                const isUndeploy = event.deployType === 'undeploy';\n                const unit = event.unit;\n                const deploySound = isUndeploy ? unit.rules.undeploySound : unit.rules.deploySound;\n                if (deploySound) {\n                    this.worldSound.playEffect(deploySound, unit, unit.owner);\n                }\n                break;\n            case EventType.WeaponFire:\n                this.handleWeaponFireSound(event);\n                break;\n            case EventType.InflictDamage:\n                this.handleDamageSound(event);\n                break;\n            case EventType.RadarEvent:\n                this.handleRadarEventSound(event);\n                break;\n            case EventType.SuperWeaponReady:\n                this.handleSuperWeaponReadySound(event);\n                break;\n            case EventType.SuperWeaponActivate:\n                this.handleSuperWeaponActivateSound(event);\n                break;\n            case EventType.LightningStormManifest:\n                this.handleLightningStormManifestSound(event);\n                break;\n            case EventType.WarheadDetonate:\n                this.handleWarheadDetonateSound(event);\n                break;\n            case EventType.ObjectDestroy:\n                this.handleObjectDestroySound(event);\n                break;\n            case EventType.ObjectSpawn:\n                this.handleObjectSpawnSound(event);\n                break;\n            case EventType.BuildingPlace:\n                this.handleBuildingPlaceSound(event);\n                break;\n            case EventType.PlayerDefeated:\n                this.handlePlayerDefeatedSound(event);\n                break;\n            case EventType.UnitPromote:\n                this.handleUnitPromoteSound(event);\n                break;\n            case EventType.CratePickup:\n                this.handleCratePickupSound(event);\n                break;\n            default:\n                break;\n        }\n    }\n    private handleWeaponFireSound(event: any): void {\n        const weapon = event.weapon;\n        const gameObject = event.gameObject;\n        if (weapon.rules.report?.length) {\n            const volume = weapon.warhead.rules.electricAssault ? 0.25 : 1;\n            const soundIndex = Math.floor(Math.random() * weapon.rules.report.length);\n            this.worldSound.playEffect(weapon.rules.report[soundIndex], gameObject.position.worldPosition, gameObject.owner, volume);\n        }\n    }\n    private handleDamageSound(event: any): void {\n        if (event.target.isBuilding() && !event.target.wallTrait) {\n            const damagePercent = (event.damageHitPoints / event.target.healthTrait.maxHitPoints) * 100;\n            const rules = this.game.rules.audioVisual;\n            const redThreshold = 100 * rules.conditionRed;\n            const yellowThreshold = 100 * rules.conditionYellow;\n            const health = event.target.healthTrait.health;\n            if ((health <= yellowThreshold && yellowThreshold < health + damagePercent) ||\n                (health <= redThreshold && redThreshold < health + damagePercent)) {\n                this.worldSound.playEffect(SoundKey.BuildingDamageSound, event.target, event.target.owner);\n            }\n        }\n    }\n    private handleRadarEventSound(event: any): void {\n        if (event.radarEventType === RadarEventType.BaseUnderAttack || event.radarEventType === 'BaseUnderAttack') {\n            if (event.target === this.player) {\n                this.eva.play('EVA_OurBaseIsUnderAttack');\n                this.sound.play(SoundKey.BaseUnderAttackSound, ChannelType.Effect);\n            }\n            else if (this.player && this.game.alliances.areAllied(this.player, event.target)) {\n                this.eva.play('EVA_OurAllyIsUnderAttack');\n                this.sound.play(SoundKey.BaseUnderAttackSound, ChannelType.Effect);\n            }\n        }\n        else if (event.radarEventType === RadarEventType.HarvesterUnderAttack || event.radarEventType === 'HarvesterUnderAttack') {\n            if (event.target === this.player) {\n                this.eva.play('EVA_OreMinerUnderAttack');\n            }\n        }\n        else if ((event.radarEventType === RadarEventType.EnemyObjectSensed || event.radarEventType === 'EnemyObjectSensed') && event.target === this.player) {\n            const building = this.game.map.getGroundObjectsOnTile(event.tile).find((object: any) => object.isBuilding() && object.superWeaponTrait);\n            const superWeaponType = building?.superWeaponTrait?.getSuperWeapon(building)?.rules.type;\n            const eva = detectedSuperWeaponEvaByType.get(superWeaponType);\n            if (eva) {\n                this.eva.play(eva);\n            }\n        }\n    }\n    private handleSuperWeaponReadySound(event: any): void {\n        if (event.target.owner === this.player) {\n            const eva = event.target.rules?.type !== undefined\n                ? superWeaponReadyEvaByType.get(event.target.rules.type)\n                : undefined;\n            if (eva) {\n                this.eva.play(eva);\n            }\n        }\n    }\n    private handleSuperWeaponActivateSound(event: any): void {\n        if (!event.noSfxWarning) {\n            const eva = superWeaponActivateEvaByType.get(event.target);\n            if (eva) {\n                this.eva.play(eva, true);\n            }\n            const sound = superWeaponActivateSoundByType.get(event.target);\n            if (sound) {\n                this.worldSound.playEffect(sound, Coords.tile3dToWorld(event.atTile.rx, event.atTile.ry, event.atTile.z), event.owner);\n            }\n        }\n        const message = superWeaponActivateMessageByType.get(event.target);\n        if (message) {\n            this.messageList.addSystemMessage(this.strings.get(message), this.player ?? 'grey');\n        }\n    }\n    private handleLightningStormManifestSound(event: any): void {\n        this.messageList.addSystemMessage(this.strings.get('TXT_LIGHTNING_STORM'), this.player ?? 'grey');\n        this.worldSound.playEffect(SoundKey.StormSound, Coords.tile3dToWorld(event.target.rx, event.target.ry, event.target.z));\n    }\n    private handleWarheadDetonateSound(event: any): void {\n        if (event.isLightningStrike) {\n            this.worldSound.playEffect(SoundKey.LightningSounds, event.position);\n        }\n    }\n    private handleObjectDestroySound(event: any): void {\n        const target = event.target;\n        let sound: string | undefined;\n        if (target.isTechno()) {\n            sound = target.rules.dieSound;\n            if (!sound && target.isBuilding()) {\n                sound = SoundKey.BuildingDieSound as any;\n            }\n        }\n        if (sound) {\n            this.worldSound.playEffect(sound, target.position.worldPosition, target.owner);\n        }\n        if (target.isUnit() && !target.rules.spawned && target.owner === this.player) {\n            this.eva.play('EVA_UnitLost');\n        }\n    }\n    private handleObjectSpawnSound(event: any): void {\n        const gameObject = event.gameObject;\n        if (gameObject.isTechno() && gameObject.rules.createSound) {\n            this.worldSound.playEffect(gameObject.rules.createSound, gameObject, gameObject.owner);\n        }\n    }\n    private handleBuildingPlaceSound(event: any): void {\n        const building = event.target;\n        this.worldSound.playEffect(SoundKey.BuildingSlam, building, building.owner);\n    }\n    private handlePlayerDefeatedSound(event: any): void {\n        const player = event.target;\n        if (player === this.player && !this.player.isObserver) {\n            return;\n        }\n        if (!player.resigned) {\n            const playerName = player.isAi\n                ? this.strings.get(`AI_${player.aiDifficulty}`)\n                : player.name;\n            this.eva.play(player !== this.player ? 'EVA_PlayerDefeated' : 'EVA_YouHaveLost');\n            this.messageList.addSystemMessage(this.strings.get('TXT_PLAYER_DEFEATED', playerName), player);\n        }\n    }\n    private handleUnitPromoteSound(event: any): void {\n        if (event.target.owner === this.player) {\n            const isElite = event.target.veteranLevel === 'Elite';\n            this.sound.play(isElite ? SoundKey.UpgradeEliteSound : SoundKey.UpgradeVeteranSound, ChannelType.Effect);\n            this.eva.play('EVA_UnitPromoted', true);\n        }\n    }\n    private handleCratePickupSound(event: any): void {\n        const crateType = event.target?.type;\n        let sound = crateSoundByType.get(crateType);\n        if (!sound && crateType === PowerupType.HealBase) {\n            sound = this.game.rules.crateRules.healCrateSound;\n        }\n        const eva = crateEvaByType.get(crateType);\n        const isHostilePickup = this.player &&\n            !this.player.isObserver &&\n            event.player !== this.player &&\n            !this.game.alliances.areAllied(event.player, this.player);\n        if (isHostilePickup) {\n            return;\n        }\n        if (sound) {\n            const position = Coords.tile3dToWorld(event.tile.rx, event.tile.ry, event.tile.z);\n            this.worldSound.playEffect(sound, position, event.player);\n        }\n        if (eva) {\n            this.eva.play(eva);\n        }\n    }\n    handleOrderPushed(unit: any, orderType: any, feedbackType: any): void {\n        const now = Date.now();\n        if (!this.lastFeedbackTime || now - this.lastFeedbackTime >= 250) {\n            let sound: string | undefined;\n            switch (feedbackType) {\n                case 'Attack':\n                    sound = unit.rules.voiceAttack;\n                    break;\n                case 'Move':\n                    sound = unit.rules.voiceMove;\n                    break;\n                case 'Capture':\n                    sound = unit.rules.voiceCapture || unit.rules.voiceSpecialAttack;\n                    break;\n            }\n            if (sound) {\n                this.sound.play(sound, ChannelType.Effect);\n                this.lastFeedbackTime = now;\n            }\n        }\n    }\n    handleSelectionChangeEvent(event: any): void {\n        if (event.selection.length && event.selection[0].owner === this.player) {\n            const now = Date.now();\n            const canPlayFeedback = !this.lastFeedbackTime || now - this.lastFeedbackTime >= 250;\n            if (canPlayFeedback) {\n                this.lastFeedbackTime = now;\n                event.selection.forEach((unit: any) => {\n                    if (unit.rules.voiceSelect) {\n                        this.sound.play(unit.rules.voiceSelect, ChannelType.Effect);\n                    }\n                });\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/TauntHandler.ts",
    "content": "import { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nexport class TauntHandler {\n    private lastTauntTimeByPlayer = new Map<string, number>();\n    private disposables = new CompositeDisposable();\n    constructor(private gservCon: any, private localPlayer: any, private game: any, private replayRecorder: any, private tauntsEnabled: any, private tauntPlayback: any, private mutedPlayers: Set<string>) { }\n    init(): void {\n        this.gservCon.onTaunt.subscribe(this.handleMessage);\n        this.disposables.add(() => this.gservCon.onTaunt.unsubscribe(this.handleMessage));\n    }\n    private handleMessage = (message: any): void => {\n        if (!this.tauntsEnabled.value) {\n            return;\n        }\n        const player = this.game.getPlayerByName(message.from);\n        if (!player.country) {\n            return;\n        }\n        if (this.mutedPlayers.has(player.name)) {\n            return;\n        }\n        if (this.checkAndUpdateLastTauntTime(player.name)) {\n            this.recordReplayEvent(player, message.tauntNo);\n            this.tauntPlayback\n                .playTaunt(player, message.tauntNo)\n                .catch((error: any) => console.error(error));\n        }\n    };\n    sendTaunt(tauntNumber: number): void {\n        if (!this.checkAndUpdateLastTauntTime(this.localPlayer.name)) {\n            return;\n        }\n        if (!this.gservCon.isOpen()) {\n            return;\n        }\n        this.gservCon.sendTaunt(tauntNumber);\n        this.recordReplayEvent(this.localPlayer, tauntNumber);\n        this.tauntPlayback\n            .playTaunt(this.localPlayer, tauntNumber)\n            .catch((error: any) => console.error(error));\n    }\n    private checkAndUpdateLastTauntTime(playerName: string): boolean {\n        const currentTime = Date.now();\n        const lastTauntTime = this.lastTauntTimeByPlayer.get(playerName);\n        if (lastTauntTime && currentTime - lastTauntTime <= 5000) {\n            return false;\n        }\n        this.lastTauntTimeByPlayer.set(playerName, currentTime);\n        return true;\n    }\n    private recordReplayEvent(player: any, tauntNumber: number): void {\n        this.replayRecorder.recordTaunt(this.game.currentTick, player.name, tauntNumber);\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/TauntPlayback.ts",
    "content": "import { ChannelType } from '@/engine/sound/ChannelType';\nimport { pad } from '@/util/string';\nexport class TauntPlayback {\n    private static readonly COUNTRY_CODES = new Map([\n        ['Americans', 'am'],\n        ['French', 'fr'],\n        ['Germans', 'ge'],\n        ['British', 'br'],\n        ['Russians', 'ru'],\n        ['Confederation', 'cu'],\n        ['Africans', 'li'],\n        ['Arabs', 'ir'],\n        ['Alliance', 'ko'],\n    ]);\n    constructor(private audioSystem: any, private taunts: any) { }\n    async playTaunt(player: any, tauntNumber: number): Promise<void> {\n        const fileName = this.getTauntFileName(player.country.name, tauntNumber);\n        const tauntFile = await this.taunts.get(fileName);\n        if (tauntFile) {\n            this.audioSystem.playWavFile(tauntFile, ChannelType.Voice);\n        }\n        else {\n            console.warn(`Taunt file \"${fileName}\" not found.`);\n        }\n    }\n    private getTauntFileName(countryName: string, tauntNumber: number): string {\n        const countryCode = TauntPlayback.COUNTRY_CODES.get(countryName);\n        return `tau${countryCode}${pad(tauntNumber, '00')}.wav`;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/TooltipTextResolver.ts",
    "content": "function resolveUiNameText(uiName: string | undefined, strings: any): string | undefined {\n    let resolved: string | undefined;\n    if (uiName !== undefined && uiName !== '') {\n        if (uiName.includes('{')) {\n            resolved = uiName.replace(/\\{([^}]+)\\}/g, (_match, key) => strings.get(key));\n        }\n        else if (strings.has(uiName) || uiName.match(/^NOSTR:/i)) {\n            resolved = strings.get(uiName);\n        }\n    }\n    return resolved;\n}\nfunction resolveHoverTooltipText(hover: any, strings: any, debugMode = false): string | undefined {\n    let tooltip: string | undefined;\n    if (hover?.entity) {\n        const uiName = hover.entity.getUiName?.();\n        tooltip = resolveUiNameText(uiName, strings);\n        if (debugMode && tooltip !== undefined) {\n            tooltip += ` (ID: ${hover.entity.gameObject.id})`;\n        }\n    }\n    else if (hover?.uiObject) {\n        tooltip = hover.uiObject.userData.tooltip;\n    }\n    return tooltip;\n}\nfunction resolveSidebarItemTooltipText(item: any, sidebarModel: any, strings: any): string | undefined {\n    if (!item?.target?.rules) {\n        return undefined;\n    }\n    const name = strings.get(item.target.rules.uiName);\n    if (item.target.rules.cost === undefined) {\n        return name;\n    }\n    let cost = item.target.rules.cost;\n    if (typeof sidebarModel?.computePurchaseCost === 'function') {\n        cost = sidebarModel.computePurchaseCost(item.target.rules);\n    }\n    return `${name}\\n$${cost}`;\n}\nexport { resolveUiNameText, resolveHoverTooltipText, resolveSidebarItemTooltipText };\n"
  },
  {
    "path": "src/gui/screen/game/WorldView.ts",
    "content": "import { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { WorldScene } from '@/engine/renderable/WorldScene';\nimport { WorldViewportHelper } from '@/engine/util/WorldViewportHelper';\nimport { MapTileIntersectHelper } from '@/engine/util/MapTileIntersectHelper';\nimport { WorldSound } from '@/engine/sound/WorldSound';\nimport { Engine } from '@/engine/Engine';\nimport { MapPanningHelper } from '@/engine/util/MapPanningHelper';\nimport { IsoCoords } from '@/engine/IsoCoords';\nimport { ImageFinder } from '@/engine/ImageFinder';\nimport { MapRenderable } from '@/engine/renderable/entity/map/MapRenderable';\nimport { RenderableFactory } from '@/engine/renderable/entity/RenderableFactory';\nimport { RenderableManager } from '@/engine/RenderableManager';\nimport { ChronoFxHandler } from '@/engine/renderable/fx/handler/ChronoFxHandler';\nimport { Lighting } from '@/engine/Lighting';\nimport { LightingDirector } from '@/engine/gfx/lighting/LightingDirector';\nimport { WarheadDetonateFxHandler } from '@/engine/renderable/fx/handler/WarheadDetonateFxHandler';\nimport { SuperWeaponFxHandler } from '@/engine/renderable/fx/handler/SuperWeaponFxHandler';\nimport { CrateFxHandler } from '@/engine/renderable/fx/handler/CrateFxHandler';\nimport { BeaconFxHandler } from '@/engine/renderable/fx/handler/BeaconFxHandler';\nimport { VxlBuilderFactory } from '@/engine/renderable/builder/VxlBuilderFactory';\nexport class WorldView {\n    private disposables = new CompositeDisposable();\n    private worldScene?: WorldScene;\n    private worldSound?: WorldSound;\n    private mapRenderable?: MapRenderable;\n    private renderableManager?: RenderableManager;\n    constructor(private hudDimensions: {\n        width: number;\n        height: number;\n    }, private game: any, private sound: any, private renderer: any, private runtimeVars: any, private minimap: any, private strings: any, private generalOptions: any, private vxlGeometryPool: any, private buildingImageDataCache: any) { }\n    init(localPlayer: any, viewport: any, theater: any): any {\n        const mapScreenBounds = this.computeMapScreenBounds(this.game.map.mapBounds.getLocalSize());\n        const worldViewport = this.computeWorldViewport(viewport, mapScreenBounds);\n        try {\n            console.log('[WorldView.init]', {\n                hud: this.hudDimensions,\n                viewport,\n                mapScreenBounds,\n                worldViewport\n            });\n        }\n        catch { }\n        const worldScene = WorldScene.factory(worldViewport, this.runtimeVars.freeCamera, this.generalOptions.graphics.shadows);\n        this.disposables.add(worldScene);\n        this.worldScene = worldScene;\n        this.updatePanLimits(this.game.map, worldScene.cameraPan, worldViewport);\n        const startLocationIndex = (!localPlayer || localPlayer.isObserver)\n            ? this.game.getCombatants()[0].startLocation\n            : localPlayer.startLocation;\n        const startPos = this.game.map.startingLocations[startLocationIndex];\n        const panningHelper = new MapPanningHelper(this.game.map);\n        worldScene.cameraPan.setPan(panningHelper.computeCameraPanFromTile(startPos.x, startPos.y));\n        try {\n            console.log('[WorldView.init] startLocation', { startLocationIndex, startPos });\n        }\n        catch { }\n        const fullSize = this.game.map.mapBounds.getFullSize();\n        const lightFocus = IsoCoords.screenTileToWorld(fullSize.width / 2, fullSize.height / 2);\n        worldScene.setLightFocusPoint(lightFocus.x, lightFocus.y);\n        const viewportHelper = new WorldViewportHelper(worldScene);\n        const tileIntersectHelper = new MapTileIntersectHelper(this.game.map, worldScene);\n        const playerShroud = localPlayer ? this.game.mapShroudTrait.getPlayerShroud(localPlayer) : undefined;\n        const worldSound = new WorldSound(this.sound, localPlayer, playerShroud, viewportHelper, tileIntersectHelper, this.game.getWorld(), worldScene as any, this.renderer);\n        worldSound.init();\n        this.disposables.add(worldSound, () => (this.worldSound = undefined));\n        this.worldSound = worldSound;\n        const lighting = new Lighting(this.game.mapLightingTrait);\n        this.disposables.add(lighting);\n        worldScene.applyLighting(lighting);\n        const lightingDirector = new LightingDirector(lighting, this.renderer, this.game.speed);\n        lightingDirector.init();\n        this.disposables.add(lightingDirector);\n        const images = Engine.getImages();\n        const voxels = Engine.getVoxels();\n        const voxelAnims = Engine.getVoxelAnims();\n        const palettes = Engine.getPalettes();\n        const imageFinder = new ImageFinder(images as any, theater);\n        const mapRenderable = new MapRenderable(this.game.map, playerShroud, this.game.mapRadiationTrait, lighting, theater, this.game.rules, this.game.art, imageFinder, worldScene.camera, this.runtimeVars.debugWireframes, this.game.speed, worldSound, true);\n        (worldScene as any).add(mapRenderable as any);\n        try {\n            console.log('[WorldView.init] MapRenderable added');\n        }\n        catch { }\n        const debugRoot = ((window as any).__ra2debug ??= {});\n        debugRoot.worldView = this;\n        debugRoot.worldScene = worldScene;\n        debugRoot.mapRenderable = mapRenderable;\n        debugRoot.game = this.game;\n        debugRoot.theater = theater;\n        this.disposables.add(mapRenderable, () => (this.mapRenderable = undefined));\n        this.mapRenderable = mapRenderable;\n        const useInstancing = this.renderer.supportsInstancing?.() ?? false;\n        const vxlBuilderFactory = new VxlBuilderFactory(this.vxlGeometryPool, useInstancing, worldScene.camera);\n        const renderableFactory = new RenderableFactory(localPlayer, this.game.getUnitSelection(), this.game.alliances, this.game.rules, this.game.art, mapRenderable, imageFinder, palettes, voxels, voxelAnims, theater, worldScene.camera, lighting, lightingDirector as any, this.runtimeVars.debugWireframes, this.runtimeVars.debugText, this.game.speed, worldSound, this.strings, this.generalOptions.flyerHelper, this.generalOptions.hiddenObjects, vxlBuilderFactory, this.buildingImageDataCache, true, useInstancing);\n        const renderableManager = new RenderableManager(this.game.getWorld(), worldScene, worldScene.camera as any, renderableFactory);\n        renderableManager.init();\n        this.disposables.add(renderableManager, () => (this.renderableManager = undefined));\n        this.renderableManager = renderableManager;\n        const chronoFx = new ChronoFxHandler(this.game, renderableManager as any);\n        chronoFx.init();\n        this.disposables.add(chronoFx);\n        const warheadFx = new WarheadDetonateFxHandler(this.game, renderableManager as any);\n        warheadFx.init();\n        this.disposables.add(warheadFx);\n        const superWeaponFxHandler = new SuperWeaponFxHandler(this.game, renderableManager as any, lightingDirector as any);\n        superWeaponFxHandler.init();\n        this.disposables.add(superWeaponFxHandler);\n        const crateFxHandler = new CrateFxHandler(this.game, renderableManager as any);\n        crateFxHandler.init();\n        this.disposables.add(crateFxHandler);\n        const beaconFxHandler = new BeaconFxHandler(this.game, localPlayer, renderableManager as any, this.renderer, worldSound);\n        beaconFxHandler.init();\n        this.disposables.add(beaconFxHandler);\n        const handleLightingChange = (lightingData: any) => {\n            worldScene.applyLighting(lighting);\n            renderableManager.updateLighting();\n            mapRenderable.updateLighting(lightingData);\n        };\n        lighting.onChange.subscribe(handleLightingChange);\n        this.disposables.add(() => lighting.onChange.unsubscribe(handleLightingChange));\n        this.minimap.initWorld(worldScene);\n        const onBoundsResize = () => {\n            this.handleMapBoundsOrViewportChange(viewport);\n            this.minimap.forceRerender();\n        };\n        this.game.map.mapBounds.onLocalResize.subscribe(onBoundsResize);\n        this.disposables.add(() => this.game.map.mapBounds.onLocalResize.unsubscribe(onBoundsResize));\n        return {\n            worldScene,\n            worldSound,\n            renderableManager,\n            superWeaponFxHandler,\n            beaconFxHandler\n        };\n    }\n    handleViewportChange(viewport: any): void {\n        this.handleMapBoundsOrViewportChange(viewport);\n    }\n    changeLocalPlayer(player: any): void {\n        const shroud = player ? this.game.mapShroudTrait.getPlayerShroud(player) : undefined;\n        this.worldSound?.changeLocalPlayer(player, shroud);\n        this.mapRenderable?.setShroud(shroud);\n    }\n    private handleMapBoundsOrViewportChange(viewport: any): void {\n        if (!this.worldScene)\n            return;\n        const mapScreenBounds = this.computeMapScreenBounds(this.game.map.mapBounds.getLocalSize());\n        const newViewport = this.computeWorldViewport(viewport, mapScreenBounds);\n        this.worldScene.updateViewport(newViewport);\n        this.updatePanLimits(this.game.map, this.worldScene.cameraPan, newViewport);\n    }\n    private computeWorldViewport(viewport: any, mapScreenBounds: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }) {\n        const availWidth = Math.max(1, viewport.width - this.hudDimensions.width);\n        const availHeight = Math.max(1, viewport.height - this.hudDimensions.height);\n        const width = Math.max(1, Math.min(mapScreenBounds.width, availWidth));\n        const height = Math.max(1, Math.min(mapScreenBounds.height, availHeight));\n        return {\n            x: viewport.x,\n            y: viewport.y,\n            width,\n            height,\n        };\n    }\n    private updatePanLimits(map: any, cameraPan: any, worldViewport: any): void {\n        const p = new MapPanningHelper(map);\n        const mapScreenBounds = this.computeMapScreenBounds(map.mapBounds.getLocalSize());\n        cameraPan.setPanLimits(p.computeCameraPanLimits(worldViewport, mapScreenBounds));\n    }\n    private computeMapScreenBounds(localSize: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }) {\n        const topLeft = IsoCoords.screenTileToScreen(localSize.x, localSize.y);\n        const bottomRight = IsoCoords.screenTileToScreen(localSize.x + localSize.width, localSize.y + localSize.height - 1);\n        return { x: topLeft.x, y: topLeft.y, width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y };\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/GameResultPopup.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nexport enum GameResultType {\n    SpVictory = 0,\n    SpDefeat = 1,\n    MpVictory = 2,\n    MpDefeat = 3\n}\nexport type GameResultPopupProps = UiComponentProps & {\n    viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    type: GameResultType;\n};\nexport class GameResultPopup extends UiComponent<GameResultPopupProps> {\n    createUiObject() {\n        const { viewport } = this.props;\n        const obj = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        obj.setPosition(viewport.x, viewport.y);\n        obj.getHtmlContainer().setSize(viewport.width, viewport.height);\n        return obj;\n    }\n    defineChildren() {\n        const { viewport, type } = this.props;\n        return jsx(\"sprite\", {\n            image: \"grfxtxt.shp\",\n            palette: \"grfxtxt.pal\",\n            ref: (e: any) => {\n                const size = e.getSize();\n                e.setPosition((viewport.width - size.width) / 2, (viewport.height - size.height) / 2);\n            },\n            frame: type,\n        });\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/Hud.ts",
    "content": "import * as jsx from \"@/gui/jsx/jsx\";\nimport { ShpFile } from \"@/data/ShpFile\";\nimport { SideType } from \"@/game/SideType\";\nimport { SidebarCard } from \"@/gui/screen/game/component/hud/SidebarCard\";\nimport { SidebarTabs } from \"@/gui/screen/game/component/hud/SidebarTabs\";\nimport { SidebarIconButton } from \"@/gui/screen/game/component/hud/SidebarIconButton\";\nimport { SidebarMenu } from \"@/gui/screen/game/component/hud/SidebarMenu\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { EventDispatcher } from \"@/util/event\";\nimport { GameMenuContentArea } from \"@/gui/screen/game/component/hud/GameMenuContentArea\";\nimport { SidebarPower } from \"@/gui/screen/game/component/hud/SidebarPower\";\nimport { SidebarCredits } from \"@/gui/screen/game/component/hud/SidebarCredits\";\nimport { SidebarRadar } from \"@/gui/screen/game/component/hud/SidebarRadar\";\nimport { CombatantSidebarModel } from \"@/gui/screen/game/component/hud/viewmodel/CombatantSidebarModel\";\nimport { SidebarGameTime } from \"@/gui/screen/game/component/hud/SidebarGameTime\";\nimport { Messages } from \"@/gui/screen/game/component/hud/Messages\";\nimport { SuperWeaponTimers } from \"@/gui/screen/game/component/hud/SuperWeaponTimers\";\nimport { ShpAggregator } from \"@/engine/renderable/builder/ShpAggregator\";\nimport { CommandBarButtonType } from \"@/gui/screen/game/component/hud/commandBar/CommandBarButtonType\";\nimport { commandButtonConfigs } from \"@/gui/screen/game/component/hud/commandBar/commandButtonConfigs\";\nimport { isNotNullOrUndefined } from \"@/util/typeGuard\";\nimport { DebugText } from \"@/gui/screen/game/component/hud/DebugText\";\nimport { ReplayStatsOverlay } from \"@/gui/screen/game/component/hud/ReplayStatsOverlay\";\ndeclare const THREE: any;\ninterface Viewport {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface SidebarModel {\n    repairMode: boolean;\n    sellMode: boolean;\n    activeTab: {\n        items: any[];\n    };\n    selectTab(id: any): void;\n}\nexport class Hud extends UiObject {\n    private sideType: SideType;\n    private viewport: Viewport;\n    private images: Map<string, any>;\n    private palettes: Map<string, any>;\n    private cameoFilenames: string[];\n    private sidebarModel: SidebarModel;\n    private messageList: any;\n    private chatHistory: any;\n    private debugTextValue: any;\n    private debugTextEnabled: any;\n    private localPlayer: any;\n    private players: any;\n    private stalemateDetectTrait: any;\n    private countdownTimer: any;\n    private jsxRenderer: any;\n    private strings: any;\n    private commandBarButtonTypes: CommandBarButtonType[];\n    private persistentHoverTags: any;\n    private _onDiploButtonClick: EventDispatcher<this, void>;\n    private _onOptButtonClick: EventDispatcher<this, void>;\n    private _onRepairButtonClick: EventDispatcher<this, void>;\n    private _onSellButtonClick: EventDispatcher<this, void>;\n    private _onSidebarSlotClick: EventDispatcher<this, any>;\n    private _onSidebarTabClick: EventDispatcher<this, any>;\n    private _onCreditsTick: EventDispatcher<this, any>;\n    private _onMessagesTick: EventDispatcher<this>;\n    private _onMessageSubmit: EventDispatcher<this, any>;\n    private _onMessageCancel: EventDispatcher<this>;\n    private _onScrollButtonClick: EventDispatcher<this, any>;\n    private _onCommandBarButtonClick: EventDispatcher<this, CommandBarButtonType>;\n    private commandBarButtons: any[] = [];\n    public sidebarWidth: number;\n    private repeaterCount: number;\n    private repeaterHeight: number;\n    public actionBarHeight: number;\n    private sidebarTop?: any;\n    private sidebarRadar?: any;\n    private sideCameoRepeaters?: any;\n    private sidebarButtonsContainer?: any;\n    private sidebarMenuContainer?: any;\n    private sidebarMenu?: any;\n    private sidebarCard?: any;\n    private sidebarPower?: any;\n    private repairButton?: any;\n    private sellButton?: any;\n    private pgDnButton?: any;\n    private pgUpButton?: any;\n    private messages?: any;\n    private debugText?: any;\n    private superWeaponTimers?: any;\n    private replayStatsOverlay?: any;\n    private menuContentContainer?: any;\n    private menuContentContainerInner?: any;\n    private menuContent?: any;\n    constructor(sideType: SideType, viewport: Viewport, images: Map<string, any>, palettes: Map<string, any>, cameoFilenames: string[], sidebarModel: SidebarModel, messageList: any, chatHistory: any, debugTextValue: any, debugTextEnabled: any, localPlayer: any, players: any, stalemateDetectTrait: any, countdownTimer: any, jsxRenderer: any, strings: any, commandBarButtonTypes: CommandBarButtonType[], persistentHoverTags: any) {\n        super(new THREE.Object3D(), new HtmlContainer());\n        this.sideType = sideType;\n        this.viewport = viewport;\n        this.images = images;\n        this.palettes = palettes;\n        this.cameoFilenames = cameoFilenames;\n        this.sidebarModel = sidebarModel;\n        this.messageList = messageList;\n        this.chatHistory = chatHistory;\n        this.debugTextValue = debugTextValue;\n        this.debugTextEnabled = debugTextEnabled;\n        this.localPlayer = localPlayer;\n        this.players = players;\n        this.stalemateDetectTrait = stalemateDetectTrait;\n        this.countdownTimer = countdownTimer;\n        this.jsxRenderer = jsxRenderer;\n        this.strings = strings;\n        this.commandBarButtonTypes = commandBarButtonTypes;\n        this.persistentHoverTags = persistentHoverTags;\n        this._onDiploButtonClick = new EventDispatcher();\n        this._onOptButtonClick = new EventDispatcher();\n        this._onRepairButtonClick = new EventDispatcher();\n        this._onSellButtonClick = new EventDispatcher();\n        this._onSidebarSlotClick = new EventDispatcher();\n        this._onSidebarTabClick = new EventDispatcher();\n        this._onCreditsTick = new EventDispatcher();\n        this._onMessagesTick = new EventDispatcher();\n        this._onMessageSubmit = new EventDispatcher();\n        this._onMessageCancel = new EventDispatcher();\n        this._onScrollButtonClick = new EventDispatcher();\n        this._onCommandBarButtonClick = new EventDispatcher();\n        this.commandBarButtons = [];\n        this.init();\n    }\n    get onDiploButtonClick() {\n        return this._onDiploButtonClick.asEvent();\n    }\n    get onOptButtonClick() {\n        return this._onOptButtonClick.asEvent();\n    }\n    get onRepairButtonClick() {\n        return this._onRepairButtonClick.asEvent();\n    }\n    get onSellButtonClick() {\n        return this._onSellButtonClick.asEvent();\n    }\n    get onSidebarSlotClick() {\n        return this._onSidebarSlotClick.asEvent();\n    }\n    get onSidebarTabClick() {\n        return this._onSidebarTabClick.asEvent();\n    }\n    get onCreditsTick() {\n        return this._onCreditsTick.asEvent();\n    }\n    get onMessagesTick() {\n        return this._onMessagesTick.asEvent();\n    }\n    get onMessageSubmit() {\n        return this._onMessageSubmit.asEvent();\n    }\n    get onMessageCancel() {\n        return this._onMessageCancel.asEvent();\n    }\n    get onScrollButtonClick() {\n        return this._onScrollButtonClick.asEvent();\n    }\n    get onCommandBarButtonClick() {\n        return this._onCommandBarButtonClick.asEvent();\n    }\n    private getImage(name: string): any {\n        const image = this.images.get(name);\n        if (!image)\n            throw new Error(`Missing image \"${name}\"`);\n        return image;\n    }\n    private init(): void {\n        const sidebarPalette = this.palettes.get(\"sidebar.pal\");\n        if (!sidebarPalette)\n            throw new Error('Missing palette \"sidebar.pal\"');\n        const creditsImg = this.getImage(\"credits.shp\");\n        const topImg = this.getImage(\"top.shp\");\n        const radarImg = this.getImage(\"radar.shp\");\n        const side1Img = this.getImage(\"side1.shp\");\n        const side2Img = this.getImage(\"side2.shp\");\n        const side2bImg = this.getImage(\"side2b.shp\");\n        const side3Img = this.getImage(\"side3.shp\");\n        const addonImg = this.getImage(\"addon.shp\");\n        const tab00Img = this.getImage(\"tab00.shp\");\n        const tab01Img = this.getImage(\"tab01.shp\");\n        const tab02Img = this.getImage(\"tab02.shp\");\n        const tab03Img = this.getImage(\"tab03.shp\");\n        const diplobtnImg = this.getImage(\"diplobtn.shp\");\n        const optbtnImg = this.getImage(\"optbtn.shp\");\n        const repairImg = this.getImage(\"repair.shp\");\n        const sellImg = this.getImage(\"sell.shp\");\n        const rUpImg = this.getImage(\"r-up.shp\");\n        const rDnImg = this.getImage(\"r-dn.shp\");\n        const slotClockImg = this.getImage(\"gclock2.shp\");\n        const commandButtonImages = [\n            ...new Set(this.commandBarButtonTypes.map((type) => commandButtonConfigs.find((config) => config.type === type)?.icon)),\n        ]\n            .map((icon) => (icon ? this.images.get(icon) : undefined))\n            .filter(isNotNullOrUndefined);\n        const aggregator = new ShpAggregator();\n        const aggregatedImageData = aggregator.aggregate([diplobtnImg, optbtnImg, repairImg, sellImg, tab00Img, tab01Img, tab02Img, tab03Img, rDnImg, rUpImg, ...commandButtonImages].map((img) => ShpAggregator.getShpFrameInfo(img, false)), \"agg_hud.shp\");\n        this.sidebarWidth = creditsImg.width;\n        const sidebarBounds = {\n            x: this.viewport.width - this.sidebarWidth,\n            y: 0,\n            width: this.sidebarWidth,\n            height: this.viewport.height,\n        };\n        const topHeight = creditsImg.height + topImg.height;\n        const radarBottom = topHeight + radarImg.height;\n        const side1Bottom = topHeight + radarImg.height + side1Img.height;\n        this.repeaterCount = Math.floor((sidebarBounds.height - side1Bottom - side3Img.height) / side2Img.height);\n        this.repeaterHeight = side2Img.height;\n        const side3Top = topHeight + radarImg.height + side1Img.height + this.repeaterHeight * this.repeaterCount;\n        const lendcapImg = this.getImage(\"lendcap.shp\");\n        this.actionBarHeight = lendcapImg.height;\n        const actionBarY = this.viewport.y + this.viewport.height - this.actionBarHeight;\n        const bttnbkgdImg = this.getImage(\"bttnbkgd.shp\");\n        const rendcapImg = this.getImage(\"rendcap.shp\");\n        const availableWidth = sidebarBounds.x - lendcapImg.width - rendcapImg.width;\n        const buttonBackgroundCount = Math.floor(availableWidth / bttnbkgdImg.width);\n        const remainderWidth = availableWidth % bttnbkgdImg.width;\n        let clippedBttnbkgd: any;\n        if (remainderWidth) {\n            clippedBttnbkgd = bttnbkgdImg.clip(remainderWidth, bttnbkgdImg.height);\n        }\n        let diploButtonOffset = { x: 12, y: 4 };\n        if (this.sideType === SideType.Nod) {\n            diploButtonOffset = { x: 14, y: 5 };\n        }\n        let repairButtonOffset = { x: 20, y: 8 };\n        if (this.sideType === SideType.Nod) {\n            repairButtonOffset = { x: 34, y: 7 };\n        }\n        let tabSpacing = 1;\n        let tabOffset = { x: 26, y: -3 };\n        if (this.sideType === SideType.Nod) {\n            tabSpacing = 0;\n            tabOffset = { x: 20, y: -2 };\n        }\n        const cameoPalette = this.palettes.get(\"cameo.pal\");\n        if (!cameoPalette)\n            throw new Error('Missing palette \"cameo.pal\"');\n        const cameoImages = this.buildCameoFile();\n        const cameoNameToIdMap = this.createCameoNameToIdMap();\n        const sidebarSlotSize = { width: slotClockImg.width, height: slotClockImg.height };\n        const sidebarCardOffset = { x: 22, y: 1 };\n        const sidebarCardPosition = this.sideType === SideType.GDI\n            ? { x: 5, y: 2 }\n            : { x: 0, y: 0 };\n        const scrollButtonX = 38;\n        const scrollButtonY = 7;\n        const powerImg = this.getImage(\"powerp.shp\");\n        const textColor = this.getTextColor();\n        this.add(...this.jsxRenderer.render(jsx.jsx(\"fragment\", null, jsx.jsx(\"container\", { x: sidebarBounds.x, y: sidebarBounds.y }, jsx.jsx(\"sprite-batch\", null, jsx.jsx(\"sprite\", { static: true, image: creditsImg, palette: sidebarPalette }), jsx.jsx(\"container\", { ref: (ref: any) => (this.sidebarTop = ref), zIndex: 1 }, this.sidebarModel instanceof CombatantSidebarModel\n            ? jsx.jsx(SidebarCredits, {\n                sidebarModel: this.sidebarModel,\n                height: creditsImg.height,\n                width: creditsImg.width,\n                textColor: textColor,\n                onTick: (data: any) => this._onCreditsTick.dispatch(this, data),\n            })\n            : jsx.jsx(SidebarGameTime, {\n                sidebarModel: this.sidebarModel,\n                height: creditsImg.height,\n                width: creditsImg.width,\n                textColor: textColor,\n            })), jsx.jsx(\"sprite\", {\n            static: true,\n            image: topImg,\n            palette: sidebarPalette,\n            y: creditsImg.height,\n        }), jsx.jsx(\"sprite\", {\n            static: true,\n            image: radarImg,\n            palette: sidebarPalette,\n            y: topHeight,\n        }), jsx.jsx(SidebarRadar, {\n            image: radarImg,\n            palette: sidebarPalette,\n            y: topHeight,\n            sidebarModel: this.sidebarModel instanceof CombatantSidebarModel\n                ? this.sidebarModel\n                : undefined,\n            zIndex: 1,\n            ref: (ref: any) => (this.sidebarRadar = ref),\n        }), jsx.jsx(\"sprite\", {\n            static: true,\n            image: side1Img,\n            palette: sidebarPalette,\n            y: radarBottom,\n        }), new Array(this.repeaterCount).fill(0).map((_, index) => jsx.jsx(\"sprite\", {\n            static: true,\n            image: side2bImg,\n            palette: sidebarPalette,\n            y: side1Bottom + this.repeaterHeight * index,\n        })), jsx.jsx(\"sprite-batch\", { ref: (ref: any) => (this.sideCameoRepeaters = ref) }, new Array(this.repeaterCount).fill(0).map((_, index) => jsx.jsx(\"sprite\", {\n            static: true,\n            image: side2Img,\n            palette: sidebarPalette,\n            y: side1Bottom + this.repeaterHeight * index,\n            zIndex: 1,\n        }))), jsx.jsx(SidebarPower, {\n            sidebarModel: this.sidebarModel,\n            powerImg: powerImg,\n            palette: sidebarPalette,\n            x: sidebarCardPosition.x,\n            y: side1Bottom,\n            height: this.repeaterHeight * this.repeaterCount + sidebarCardPosition.y,\n            ref: (ref: any) => (this.sidebarPower = ref),\n            zIndex: 2,\n            strings: this.strings,\n        }), jsx.jsx(SidebarCard, {\n            slotSize: sidebarSlotSize,\n            cameoImages: cameoImages,\n            cameoPalette: cameoPalette,\n            cameoNameToIdMap: cameoNameToIdMap,\n            sidebarModel: this.sidebarModel,\n            slots: 2 * this.repeaterCount,\n            onSlotClick: (event: any) => this._onSidebarSlotClick.dispatch(this, event),\n            x: sidebarCardOffset.x,\n            y: side1Bottom + sidebarCardOffset.y,\n            strings: this.strings,\n            textColor: textColor,\n            persistentHoverTags: this.persistentHoverTags,\n            ref: (ref: any) => (this.sidebarCard = ref),\n            zIndex: 2,\n        }), jsx.jsx(\"container\", {\n            ref: (ref: any) => (this.sidebarMenuContainer = ref),\n            x: sidebarCardOffset.x - 1,\n            y: side1Bottom + sidebarCardOffset.y,\n            zIndex: 2,\n        }), jsx.jsx(\"sprite\", {\n            static: true,\n            image: side3Img,\n            palette: sidebarPalette,\n            y: side3Top,\n        }), jsx.jsx(\"sprite\", {\n            static: true,\n            image: addonImg,\n            palette: sidebarPalette,\n            y: side3Top + side3Img.height,\n        }))), jsx.jsx(\"container\", {\n            x: sidebarBounds.x,\n            y: sidebarBounds.y,\n            ref: (ref: any) => (this.sidebarButtonsContainer = ref),\n            zIndex: 2,\n        }, jsx.jsx(SidebarIconButton, {\n            image: aggregatedImageData.file,\n            palette: sidebarPalette,\n            imageFrameOffset: aggregatedImageData.imageIndexes.get(diplobtnImg),\n            x: diploButtonOffset.x,\n            y: creditsImg.height + diploButtonOffset.y,\n            onClick: () => this._onDiploButtonClick.dispatch(this, undefined),\n            tooltip: this.strings.get(\"Tip:DiplomacyButton\"),\n        }), jsx.jsx(SidebarIconButton, {\n            image: aggregatedImageData.file,\n            palette: sidebarPalette,\n            imageFrameOffset: aggregatedImageData.imageIndexes.get(optbtnImg),\n            x: diploButtonOffset.x + diplobtnImg.width,\n            y: creditsImg.height + diploButtonOffset.y,\n            onClick: () => this._onOptButtonClick.dispatch(this, undefined),\n            tooltip: this.strings.get(\"Tip:OptionsButton\"),\n        }), jsx.jsx(SidebarIconButton, {\n            image: aggregatedImageData.file,\n            palette: sidebarPalette,\n            imageFrameOffset: aggregatedImageData.imageIndexes.get(repairImg),\n            x: repairButtonOffset.x,\n            y: radarBottom + repairButtonOffset.y,\n            toggle: this.sidebarModel.repairMode,\n            ref: (ref: any) => (this.repairButton = ref),\n            onClick: () => this._onRepairButtonClick.dispatch(this, undefined),\n            tooltip: this.strings.get(\"TXT_REPAIR_MODE\"),\n        }), jsx.jsx(SidebarIconButton, {\n            image: aggregatedImageData.file,\n            palette: sidebarPalette,\n            imageFrameOffset: aggregatedImageData.imageIndexes.get(sellImg),\n            x: repairButtonOffset.x + repairImg.width,\n            y: radarBottom + repairButtonOffset.y,\n            toggle: this.sidebarModel.sellMode,\n            ref: (ref: any) => (this.sellButton = ref),\n            onClick: () => this._onSellButtonClick.dispatch(this, undefined),\n            tooltip: this.strings.get(\"TXT_SELL_MODE\"),\n        }), jsx.jsx(SidebarTabs, {\n            aggregatedImageData: aggregatedImageData,\n            images: [tab00Img, tab01Img, tab02Img, tab03Img],\n            palette: sidebarPalette,\n            sidebarModel: this.sidebarModel,\n            tabSpacing: tabSpacing,\n            onTabClick: (event: any) => {\n                this.sidebarModel.selectTab(event.id);\n                this._onSidebarTabClick.dispatch(this, event.id);\n            },\n            strings: this.strings,\n            x: tabOffset.x,\n            y: side1Bottom - tab00Img.height + tabOffset.y,\n        }), jsx.jsx(SidebarIconButton, {\n            image: aggregatedImageData.file,\n            palette: sidebarPalette,\n            disabled: true,\n            imageFrameOffset: aggregatedImageData.imageIndexes.get(rDnImg),\n            x: scrollButtonX,\n            y: side3Top + scrollButtonY,\n            ref: (ref: any) => (this.pgDnButton = ref),\n            onClick: () => this._onScrollButtonClick.dispatch(this, this.sidebarCard.pageDown()),\n        }), jsx.jsx(SidebarIconButton, {\n            image: aggregatedImageData.file,\n            palette: sidebarPalette,\n            disabled: true,\n            imageFrameOffset: aggregatedImageData.imageIndexes.get(rUpImg),\n            x: scrollButtonX + rDnImg.width,\n            y: side3Top + scrollButtonY,\n            ref: (ref: any) => (this.pgUpButton = ref),\n            onClick: () => this._onScrollButtonClick.dispatch(this, this.sidebarCard.pageUp()),\n        })), jsx.jsx(\"container\", { x: this.viewport.x, y: actionBarY }, jsx.jsx(\"container\", { x: lendcapImg.width, zIndex: 1 }, this.renderCommandBarButtons(aggregatedImageData, this.commandBarButtonTypes, bttnbkgdImg.width, buttonBackgroundCount)), jsx.jsx(\"sprite-batch\", null, jsx.jsx(\"sprite\", { static: true, image: lendcapImg, palette: sidebarPalette }), new Array(buttonBackgroundCount).fill(0).map((_, index) => jsx.jsx(\"sprite\", {\n            static: true,\n            image: bttnbkgdImg,\n            palette: sidebarPalette,\n            x: lendcapImg.width + bttnbkgdImg.width * index,\n        })), clippedBttnbkgd\n            ? jsx.jsx(\"sprite\", {\n                static: true,\n                image: clippedBttnbkgd,\n                palette: sidebarPalette,\n                x: lendcapImg.width + buttonBackgroundCount * bttnbkgdImg.width,\n            })\n            : [], jsx.jsx(\"sprite\", {\n            static: true,\n            image: rendcapImg,\n            palette: sidebarPalette,\n            x: sidebarBounds.x - rendcapImg.width,\n        }))), jsx.jsx(Messages, {\n            messages: this.messageList,\n            chatHistory: this.chatHistory,\n            width: sidebarBounds.x - 10,\n            height: 200,\n            ref: (ref: any) => (this.messages = ref),\n            strings: this.strings,\n            onMessageTick: () => this._onMessagesTick.dispatch(this),\n            onMessageSubmit: (message: any) => this._onMessageSubmit.dispatch(this, message),\n            onMessageCancel: () => this._onMessageCancel.dispatch(this),\n        }), jsx.jsx(DebugText, {\n            text: this.debugTextValue,\n            visible: this.debugTextEnabled,\n            color: new THREE.Color(0xffffff),\n            x: 20,\n            y: 200,\n            width: Math.floor(sidebarBounds.x / 2),\n            height: 200,\n            ref: (ref: any) => (this.debugText = ref),\n        }), jsx.jsx(SuperWeaponTimers, {\n            localPlayer: this.localPlayer,\n            players: this.players,\n            stalemateDetectTrait: this.stalemateDetectTrait,\n            countdownTimer: this.countdownTimer,\n            strings: this.strings,\n            width: 200,\n            height: 500,\n            x: sidebarBounds.x - 200,\n            y: actionBarY - 500,\n            ref: (ref: any) => (this.superWeaponTimers = ref),\n        }), !this.localPlayer ? jsx.jsx(ReplayStatsOverlay, {\n            players: this.players,\n            strings: this.strings,\n            width: Math.min(sidebarBounds.x - 10, 920),\n            height: 400,\n            x: 5,\n            y: 5,\n            zIndex: 5,\n            ref: (ref: any) => (this.replayStatsOverlay = ref),\n        }) : [], jsx.jsx(GameMenuContentArea, {\n            hidden: true,\n            screenSize: this.viewport,\n            viewport: {\n                x: this.viewport.x,\n                y: this.viewport.y,\n                width: sidebarBounds.x,\n                height: actionBarY,\n            },\n            images: this.images,\n            ref: (ref: any) => (this.menuContentContainer = ref.getUiObject()),\n            innerRef: (ref: any) => (this.menuContentContainerInner = ref),\n        }))));\n    }\n    public getTextColor(): string {\n        return this.sideType === SideType.GDI\n            ? \"rgb(165,211,255)\"\n            : \"yellow\";\n    }\n    createSidebarMenu(buttons: any[]): any {\n        return this.jsxRenderer.render(jsx.jsx(SidebarMenu, {\n            buttonImg: this.getImage(\"sidebttn.shp\"),\n            buttonPal: \"sidebar.pal\",\n            menuHeight: this.repeaterHeight * this.repeaterCount - 2,\n            buttons: buttons,\n        }))[0];\n    }\n    showSidebarMenu(buttons: any[]): void {\n        this.destroySidebarMenu();\n        this.sidebarMenu = this.createSidebarMenu(buttons);\n        this.sidebarMenuContainer.add(this.sidebarMenu);\n        this.sideCameoRepeaters.setVisible(false);\n        this.remove(this.sidebarButtonsContainer);\n        this.sidebarCard.hide();\n        this.sidebarPower.hide();\n        this.sidebarTop?.setVisible(false);\n        this.sidebarRadar?.hide();\n        this.commandBarButtons?.forEach((button) => button.getUiObject().setVisible(false));\n        this.messages.getUiObject().setVisible(false);\n        this.debugText.getUiObject().setVisible(false);\n        this.superWeaponTimers.getUiObject().setVisible(false);\n        this.replayStatsOverlay?.getUiObject().setVisible(false);\n    }\n    hideSidebarMenu(): void {\n        this.sideCameoRepeaters.setVisible(true);\n        this.destroySidebarMenu();\n        this.add(this.sidebarButtonsContainer);\n        this.sidebarCard.show();\n        this.sidebarPower.show();\n        this.sidebarTop?.setVisible(true);\n        this.sidebarRadar?.show();\n        this.commandBarButtons?.forEach((button) => button.getUiObject().setVisible(true));\n        this.messages.getUiObject().setVisible(true);\n        this.debugText.getUiObject().setVisible(true);\n        this.superWeaponTimers.getUiObject().setVisible(true);\n        this.replayStatsOverlay?.getUiObject().setVisible(true);\n    }\n    setMenuContentComponent(component: any): void {\n        const container = this.menuContentContainerInner;\n        if (this.menuContent) {\n            container.remove(this.menuContent);\n            this.menuContent.destroy();\n            this.menuContent = undefined;\n        }\n        if (component) {\n            container.add(component);\n            this.menuContent = component;\n        }\n    }\n    setMinimap(minimap: any): void {\n        this.sidebarRadar.setMinimap(minimap);\n    }\n    toggleMenuContentVisibility(visible: boolean): void {\n        this.menuContentContainer.setVisible(visible);\n    }\n    private renderCommandBarButtons(aggregatedImageData: any, buttonTypes: CommandBarButtonType[], buttonWidth: number, maxButtons: number): any[] {\n        let xOffset = 0;\n        const buttons: any[] = [];\n        for (const buttonType of buttonTypes.slice(0, maxButtons)) {\n            if (buttonType !== CommandBarButtonType.Separator) {\n                const config = commandButtonConfigs.find((config) => config.type === buttonType);\n                if (config) {\n                    const image = this.images.get(config.icon);\n                    if (image) {\n                        const frameOffset = aggregatedImageData.imageIndexes.get(image);\n                        buttons.push(jsx.jsx(SidebarIconButton, {\n                            image: frameOffset !== undefined ? aggregatedImageData.file : image,\n                            imageFrameOffset: frameOffset,\n                            palette: \"sidebar.pal\",\n                            tooltip: config.tooltip(this.strings),\n                            x: xOffset,\n                            onClick: () => {\n                                this._onCommandBarButtonClick.dispatch(this, buttonType);\n                            },\n                            ref: (ref: any) => this.commandBarButtons.push(ref),\n                        }));\n                        xOffset += image.width;\n                    }\n                    else {\n                        console.warn(`Missing image for command bar button \"${CommandBarButtonType[buttonType]}\"`);\n                    }\n                }\n                else {\n                    console.warn(`Unknown command bar button type \"${buttonType}\"`);\n                }\n            }\n            else {\n                xOffset += buttonWidth;\n            }\n        }\n        return buttons;\n    }\n    private buildCameoFile(): ShpFile {\n        const cameoFile = new ShpFile();\n        cameoFile.filename = \"agg_cameos.shp\";\n        this.cameoFilenames.forEach((filename) => {\n            const image = this.getImage(filename);\n            if (!cameoFile.width)\n                cameoFile.width = image.width;\n            if (!cameoFile.height)\n                cameoFile.height = image.height;\n            cameoFile.addImage(image.getImage(0));\n        });\n        return cameoFile;\n    }\n    private createCameoNameToIdMap(): Map<string, number> {\n        const map = new Map<string, number>();\n        for (let i = 0; i < this.cameoFilenames.length; ++i) {\n            map.set(this.cameoFilenames[i], i);\n        }\n        return map;\n    }\n    private destroySidebarMenu(): void {\n        if (this.sidebarMenu) {\n            this.sidebarMenuContainer.remove(this.sidebarMenu);\n            this.sidebarMenu.destroy();\n        }\n    }\n    update(deltaTime: number): void {\n        super.update(deltaTime);\n        this.repairButton?.setToggleState(this.sidebarModel.repairMode);\n        this.sellButton?.setToggleState(this.sidebarModel.sellMode);\n        const hasMoreItems = this.sidebarModel.activeTab.items.length - 2 * this.repeaterCount > 0;\n        this.pgUpButton?.setDisabled(!hasMoreItems);\n        this.pgDnButton?.setDisabled(!hasMoreItems);\n    }\n    destroy(): void {\n        this.sidebarButtonsContainer.destroy();\n        this.destroySidebarMenu();\n        this.sidebarRadar.setMinimap(undefined);\n        super.destroy();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/Minimap.tsx",
    "content": "import { UiObject } from \"../../../UiObject\";\nimport { SpriteUtils } from \"../../../../engine/gfx/SpriteUtils\";\nimport * as geometry from \"../../../../util/geometry\";\nimport { MinimapRenderer } from \"../../../../engine/renderable/entity/map/MinimapRenderer\";\nimport { MapTileIntersectHelper } from \"../../../../engine/util/MapTileIntersectHelper\";\nimport { IsoCoords } from \"../../../../engine/IsoCoords\";\nimport { CompositeDisposable } from \"../../../../util/disposable/CompositeDisposable\";\nimport { EventDispatcher } from \"../../../../util/event\";\nimport { EventType } from \"../../../../game/event/EventType\";\nimport { MinimapPing, RadarRules as MinimapPingRadarRules } from \"./MinimapPing\";\nimport { RadarEventType } from \"../../../../game/rules/general/RadarRules\";\nimport { MinimapModel } from \"../../../../engine/renderable/entity/map/MinimapModel\";\nimport { GameSpeed } from \"../../../../game/GameSpeed\";\nimport * as THREE from \"three\";\ninterface Size {\n    width: number;\n    height: number;\n}\ninterface Position {\n    x: number;\n    y: number;\n}\ninterface Rect extends Position, Size {\n}\ninterface PingColorConfig {\n    high: number;\n    low: number;\n}\ninterface MinimapPingData {\n    obj: MinimapPing;\n    startTime: number | undefined;\n    duration: number;\n}\ninterface RenderResult {\n    mesh: THREE.Mesh;\n    texture: THREE.Texture;\n    wrapperObj: THREE.Object3D;\n    canvasLayoutSize: Size;\n}\ninterface TileUpdateEvent {\n    tiles: any[];\n}\ninterface ShroudUpdateEvent {\n    type: string;\n    coords?: any[];\n}\ninterface ObjectChangeEvent {\n    target: any;\n}\ninterface RadarEvent {\n    target: any;\n    tile: any;\n    radarEventType: RadarEventType;\n}\ninterface PointerEvent {\n    button: number;\n    isTouch: boolean;\n    intersection: {\n        uv: {\n            x: number;\n            y: number;\n        };\n    };\n}\ninterface ExtendedRadarRules extends MinimapPingRadarRules {\n    getEventVisibilityDuration(eventType: RadarEventType): number;\n}\nconst PING_COLORS = new Map<RadarEventType, PingColorConfig>([\n    [\n        RadarEventType.EnemyObjectSensed,\n        { high: 16776960, low: 8684544 },\n    ],\n    [RadarEventType.GenericNonCombat, { high: 65535, low: 33924 }],\n]);\nexport class Minimap extends UiObject {\n    private game: any;\n    private localPlayer: any;\n    private borderColor: number;\n    private radarRules: ExtendedRadarRules;\n    private disposables: CompositeDisposable;\n    private tilesForRecalc: Set<any>;\n    private tilesForRedraw: Set<any>;\n    private needsFullRedraw: boolean;\n    private pings: MinimapPingData[];\n    private _onClick: EventDispatcher<any>;\n    private _onRightClick: EventDispatcher<any>;\n    private _onMouseOver: EventDispatcher<any>;\n    private _onMouseMove: EventDispatcher<any>;\n    private _onMouseOut: EventDispatcher<any>;\n    private shroud: any;\n    private minimapModel: MinimapModel;\n    private fitSize?: Size;\n    private mesh?: THREE.Mesh;\n    private texture?: THREE.Texture;\n    private wrapperObj?: THREE.Object3D;\n    private size?: Size;\n    private worldScene?: any;\n    private mapTileIntersectHelper?: MapTileIntersectHelper;\n    private minimapRenderer?: MinimapRenderer;\n    private lastCanvasUpdate?: number;\n    private lastPan?: Position;\n    private lastViewport?: Rect;\n    private lastZoom?: number;\n    private viewportOutline?: THREE.Line;\n    private queuedHoverUv?: {\n        x: number;\n        y: number;\n    };\n    private handleTileUpdate: (event: TileUpdateEvent) => void;\n    private handleShroudUpdate: (event: ShroudUpdateEvent, helper: any) => void;\n    private handleObjectChange: (event: ObjectChangeEvent) => void;\n    private handleRadarEvent: (event: RadarEvent) => void;\n    get onClick() {\n        return this._onClick.asEvent();\n    }\n    get onRightClick() {\n        return this._onRightClick.asEvent();\n    }\n    get onMouseOver() {\n        return this._onMouseOver.asEvent();\n    }\n    get onMouseMove() {\n        return this._onMouseMove.asEvent();\n    }\n    get onMouseOut() {\n        return this._onMouseOut.asEvent();\n    }\n    constructor(game: any, localPlayer: any, borderColor: number, radarRules: ExtendedRadarRules) {\n        super(new THREE.Object3D());\n        this.game = game;\n        this.localPlayer = localPlayer;\n        this.borderColor = borderColor;\n        this.radarRules = radarRules;\n        this.disposables = new CompositeDisposable();\n        this.tilesForRecalc = new Set();\n        this.tilesForRedraw = new Set();\n        this.needsFullRedraw = false;\n        this.pings = [];\n        this._onClick = new EventDispatcher();\n        this._onRightClick = new EventDispatcher();\n        this._onMouseOver = new EventDispatcher();\n        this._onMouseMove = new EventDispatcher();\n        this._onMouseOut = new EventDispatcher();\n        this.handleTileUpdate = ({ tiles }: TileUpdateEvent) => {\n            tiles.forEach((tile) => {\n                this.tilesForRecalc.add(tile);\n                this.tilesForRedraw.add(tile);\n            });\n        };\n        this.handleShroudUpdate = (event: ShroudUpdateEvent, mapTileIntersectHelper: any) => {\n            if (event.type === \"incremental\") {\n                event.coords?.forEach((coord) => {\n                    for (const tile of mapTileIntersectHelper.findTilesAtShroudCoords(coord, this.map.tiles)) {\n                        this.tilesForRedraw.add(tile);\n                    }\n                });\n            }\n            else {\n                this.needsFullRedraw = true;\n            }\n        };\n        this.handleObjectChange = (event: ObjectChangeEvent) => {\n            if (event.target.isSpawned) {\n                this.map.tileOccupation\n                    .calculateTilesForGameObject(event.target.tile, event.target)\n                    .forEach((tile: any) => {\n                    this.tilesForRecalc.add(tile);\n                    this.tilesForRedraw.add(tile);\n                });\n            }\n        };\n        this.handleRadarEvent = (event: RadarEvent) => {\n            if (event.target === this.localPlayer) {\n                const canvasPos = this.minimapRenderer!.dxyToCanvas(event.tile.dx, event.tile.dy);\n                const colorConfig = PING_COLORS.get(event.radarEventType);\n                const ping = new MinimapPing(this.radarRules, colorConfig?.high ?? 16711935, colorConfig?.low ?? 8650884);\n                ping.setPosition(this.wrapperObj!.position.x + canvasPos.x, this.wrapperObj!.position.y + canvasPos.y);\n                this.pings.push({\n                    obj: ping,\n                    startTime: undefined,\n                    duration: this.radarRules.getEventVisibilityDuration(event.radarEventType),\n                });\n            }\n        };\n        this.shroud = this.localPlayer &&\n            game.mapShroudTrait.getPlayerShroud(this.localPlayer);\n        this.minimapModel = new MinimapModel(game.map.tiles, game.map.tileOccupation, this.shroud, this.localPlayer, this.game.alliances, this.game.rules.general.paradrop);\n    }\n    get map() {\n        return this.game.map;\n    }\n    setFitSize(size: Size): void {\n        const oldFitSize = this.fitSize;\n        this.fitSize = size;\n        if (size.width !== oldFitSize?.width || size.height !== oldFitSize?.height) {\n            this.forceRerender();\n        }\n    }\n    forceRerender(): void {\n        if (this.wrapperObj && this.fitSize) {\n            this.get3DObject().remove(this.wrapperObj);\n            this.destroyMesh();\n            const { mesh, texture, wrapperObj, canvasLayoutSize } = this.renderMinimap(this.fitSize);\n            this.mesh = mesh;\n            this.texture = texture;\n            this.wrapperObj = wrapperObj;\n            this.size = canvasLayoutSize;\n            this.get3DObject().add(wrapperObj);\n            this.setupListeners(mesh);\n            this.lastViewport = undefined;\n        }\n    }\n    initWorld(worldScene: any): void {\n        this.worldScene = worldScene;\n        this.mapTileIntersectHelper = new MapTileIntersectHelper(this.map, worldScene);\n    }\n    changeLocalPlayer(localPlayer: any): void {\n        this.localPlayer = localPlayer;\n        this.shroud?.onChange.unsubscribe(this.handleShroudUpdate);\n        this.shroud = this.localPlayer &&\n            this.game.mapShroudTrait.getPlayerShroud(this.localPlayer);\n        this.shroud?.onChange.subscribe(this.handleShroudUpdate);\n        this.minimapModel = new MinimapModel(this.game.map.tiles, this.game.map.tileOccupation, this.shroud, this.localPlayer, this.game.alliances, this.game.rules.general.paradrop);\n        this.forceRerender();\n    }\n    create3DObject(): void {\n        super.create3DObject();\n        if (!this.mesh) {\n            const fitSize = this.fitSize;\n            if (!fitSize) {\n                throw new Error(\"setFitSize must be called before first render\");\n            }\n            const { mesh, texture, wrapperObj, canvasLayoutSize } = this.renderMinimap(fitSize);\n            this.mesh = mesh;\n            this.texture = texture;\n            this.wrapperObj = wrapperObj;\n            this.size = canvasLayoutSize;\n            this.get3DObject().add(wrapperObj);\n            this.setupListeners(this.mesh);\n            this.map.tileOccupation.onChange.subscribe(this.handleTileUpdate);\n            this.disposables.add(() => this.map.tileOccupation.onChange.unsubscribe(this.handleTileUpdate));\n            this.shroud?.onChange.subscribe(this.handleShroudUpdate);\n            this.disposables.add(this.game.events.subscribe(EventType.ObjectOwnerChange, this.handleObjectChange), this.game.events.subscribe(EventType.ObjectDisguiseChange, this.handleObjectChange), this.game.events.subscribe(EventType.ObjectDestroy, (event: any) => {\n                if (event.target.isBuilding()) {\n                    const target = event.target;\n                    if (target.rules.leaveRubble) {\n                        this.map.tileOccupation\n                            .calculateTilesForGameObject(target.tile, target)\n                            .forEach((tile: any) => {\n                            this.tilesForRecalc.add(tile);\n                            this.tilesForRedraw.add(tile);\n                        });\n                    }\n                }\n            }), this.game.events.subscribe(EventType.RadarEvent, this.handleRadarEvent));\n        }\n    }\n    renderMinimap(fitSize: Size): RenderResult {\n        this.minimapRenderer = new MinimapRenderer(this.map, this.minimapModel, fitSize, `#${this.borderColor.toString(16).padStart(6, '0')}`, 2);\n        this.minimapModel.computeAllColors();\n        const canvas = this.minimapRenderer.renderFull();\n        const canvasLayoutSize = { width: 0.5 * canvas.width, height: 0.5 * canvas.height };\n        const position = this.computeMinimapPosition(fitSize, canvasLayoutSize);\n        const texture = this.createTexture(canvas);\n        const mesh = this.createMesh(texture, canvasLayoutSize.width, canvasLayoutSize.height);\n        const wrapperObj = new THREE.Object3D();\n        wrapperObj.matrixAutoUpdate = false;\n        wrapperObj.position.x = position.x;\n        wrapperObj.position.y = position.y;\n        wrapperObj.updateMatrix();\n        wrapperObj.add(mesh);\n        return { mesh, texture, wrapperObj, canvasLayoutSize };\n    }\n    computeMinimapPosition(fitSize: Size, canvasSize: Size): Position {\n        return {\n            x: Math.floor((fitSize.width - canvasSize.width) / 2),\n            y: Math.floor((fitSize.height - canvasSize.height) / 2),\n        };\n    }\n    createTexture(canvas: HTMLCanvasElement): THREE.Texture {\n        const texture = new THREE.Texture(canvas);\n        texture.needsUpdate = true;\n        texture.flipY = false;\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        return texture;\n    }\n    createMesh(texture: THREE.Texture, width: number, height: number): THREE.Mesh {\n        const geometry = SpriteUtils.createRectGeometry(width, height);\n        SpriteUtils.addRectUvs(geometry, { x: 0, y: 0, width, height }, { width, height });\n        geometry.translate(width / 2, height / 2, 0);\n        const material = new THREE.MeshBasicMaterial({\n            map: texture,\n            side: THREE.DoubleSide,\n        });\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.matrixAutoUpdate = false;\n        mesh.frustumCulled = false;\n        return mesh;\n    }\n    setupListeners(mesh: THREE.Mesh): void {\n        const pointerEvents = (this as any).pointerEvents;\n        if (!pointerEvents) {\n            throw new Error(\"Must call setPointerEvents before rendering\");\n        }\n        this.disposables.add(pointerEvents.addEventListener(mesh, \"click\", (event: PointerEvent) => {\n            const tile = this.computeIntersectionTile(event.intersection.uv);\n            if (tile) {\n                if (event.button === 2 || event.isTouch) {\n                    this._onRightClick.dispatch(this, tile);\n                }\n                else if (event.button === 0) {\n                    this._onClick.dispatch(this, tile);\n                }\n            }\n        }), pointerEvents.addEventListener(mesh, \"mouseover\", () => this._onMouseOver.dispatch(this)), pointerEvents.addEventListener(mesh, \"mousemove\", (event: PointerEvent) => this.queuedHoverUv = event.intersection.uv), pointerEvents.addEventListener(mesh, \"mouseout\", () => this._onMouseOut.dispatch(this)));\n    }\n    computeIntersectionTile(uv: {\n        x: number;\n        y: number;\n    }): any {\n        return this.canvasCoordsToTile(uv.x * this.size!.width, uv.y * this.size!.height);\n    }\n    canvasCoordsToTile(x: number, y: number): any {\n        const coords = this.minimapRenderer!.canvasToDxy(x, y);\n        coords.x = Math.round(coords.x);\n        coords.y = Math.round(coords.y);\n        return this.map.tiles.getByDisplayCoords(coords.x, coords.y + ((coords.x % 2) - (coords.y % 2)));\n    }\n    update(time: number): void {\n        super.update(time);\n        if (!this.lastCanvasUpdate || time - this.lastCanvasUpdate >= 1000 / 30) {\n            if (this.tilesForRecalc.size) {\n                this.minimapModel.updateColors(Array.from(this.tilesForRecalc));\n                this.tilesForRecalc.clear();\n            }\n            if (this.needsFullRedraw) {\n                this.minimapRenderer!.renderFull();\n                this.texture!.needsUpdate = true;\n                this.needsFullRedraw = false;\n                this.tilesForRedraw.clear();\n            }\n            if (this.tilesForRedraw.size) {\n                this.lastCanvasUpdate = time;\n                this.minimapRenderer!.renderIncremental(Array.from(this.tilesForRedraw));\n                this.texture!.needsUpdate = true;\n                this.tilesForRedraw.clear();\n            }\n            if (this.worldScene) {\n                const pan = this.worldScene.cameraPan.getPan();\n                const viewport = this.worldScene.viewport;\n                const zoom = this.worldScene.cameraZoom?.getZoom?.() ?? 1;\n                const viewportChanged = !this.lastViewport || !geometry.rectEquals(viewport, this.lastViewport);\n                const zoomChanged = this.lastZoom === undefined || Math.abs(zoom - this.lastZoom) > 0.001;\n                if (!geometry.pointEquals(pan, this.lastPan) || viewportChanged || zoomChanged) {\n                    this.lastPan = pan;\n                    this.lastViewport = viewport;\n                    this.lastZoom = zoom;\n                    const origin = IsoCoords.worldToScreen(0, 0);\n                    const visibleRect = {\n                        x: origin.x + pan.x - viewport.width / (2 * zoom),\n                        y: origin.y + pan.y - viewport.height / (2 * zoom),\n                        width: viewport.width / zoom,\n                        height: viewport.height / zoom,\n                    };\n                    const topLeftTile = IsoCoords.screenToScreenTile(visibleRect.x, visibleRect.y);\n                    const bottomRightTile = IsoCoords.screenToScreenTile(\n                        visibleRect.x + visibleRect.width,\n                        visibleRect.y + visibleRect.height,\n                    );\n                    const viewportRect = {\n                        x: topLeftTile.x,\n                        y: topLeftTile.y,\n                        width: bottomRightTile.x - topLeftTile.x,\n                        height: bottomRightTile.y - topLeftTile.y,\n                    };\n                    if (!this.viewportOutline || viewportChanged || zoomChanged) {\n                        const topLeft = this.minimapRenderer!.dxyToCanvas(viewportRect.x, viewportRect.y);\n                        const bottomRight = this.minimapRenderer!.dxyToCanvas(viewportRect.x + viewportRect.width, viewportRect.y + viewportRect.height);\n                        const outlineSize = {\n                            width: bottomRight.x - topLeft.x,\n                            height: bottomRight.y - topLeft.y,\n                        };\n                        if (this.viewportOutline) {\n                            this.updateOutlineSize(this.viewportOutline, outlineSize.width, outlineSize.height);\n                        }\n                        else {\n                            this.viewportOutline = this.createViewportOutline(outlineSize.width, outlineSize.height);\n                            this.viewportOutline.matrixAutoUpdate = false;\n                            this.wrapperObj!.add(this.viewportOutline);\n                        }\n                    }\n                    const outlinePosition = this.minimapRenderer!.dxyToCanvas(viewportRect.x, viewportRect.y);\n                    this.viewportOutline!.position.x = Math.max(2, Math.floor(outlinePosition.x));\n                    this.viewportOutline!.position.y = Math.max(1, Math.floor(outlinePosition.y));\n                    this.viewportOutline!.updateMatrix();\n                }\n            }\n            if (this.queuedHoverUv) {\n                const tile = this.computeIntersectionTile(this.queuedHoverUv);\n                if (tile) {\n                    this._onMouseMove.dispatch(this, tile);\n                }\n                this.queuedHoverUv = undefined;\n            }\n            this.pings.forEach((ping) => {\n                if (ping.startTime) {\n                    const elapsed = time - ping.startTime;\n                    const adjustedDuration = ping.duration /\n                        ((GameSpeed.BASE_TICKS_PER_SECOND / 1000) * this.game.speed.value);\n                    if (elapsed > adjustedDuration) {\n                        this.remove(ping.obj);\n                        ping.obj.destroy();\n                        this.pings.splice(this.pings.indexOf(ping), 1);\n                    }\n                }\n                else {\n                    ping.startTime = time;\n                    this.add(ping.obj);\n                }\n            });\n        }\n    }\n    createViewportOutline(width: number, height: number): THREE.Line {\n        const geometry = new THREE.BufferGeometry();\n        const vertices = new Float32Array([\n            0, 0, 0,\n            0, height, 0,\n            width, height, 0,\n            width, 0, 0,\n            0, 0, 0,\n        ]);\n        geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));\n        const material = new THREE.LineBasicMaterial({\n            color: this.borderColor,\n            transparent: true,\n            side: THREE.DoubleSide,\n        });\n        return new THREE.Line(geometry, material);\n    }\n    updateOutlineSize(outline: THREE.Line, width: number, height: number): void {\n        const geometry = outline.geometry as THREE.BufferGeometry;\n        const vertices = new Float32Array([\n            0, 0, 0,\n            0, height, 0,\n            width, height, 0,\n            width, 0, 0,\n            0, 0, 0,\n        ]);\n        geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));\n        geometry.attributes.position.needsUpdate = true;\n        geometry.computeBoundingBox();\n        geometry.computeBoundingSphere();\n    }\n    destroy(): void {\n        super.destroy();\n        this.destroyMesh();\n        this.shroud?.onChange.unsubscribe(this.handleShroudUpdate);\n        this.disposables.dispose();\n    }\n    destroyMesh(): void {\n        if (this.mesh) {\n            this.mesh.geometry.dispose();\n            if (Array.isArray(this.mesh.material)) {\n                this.mesh.material.forEach(material => material.dispose());\n            }\n            else {\n                this.mesh.material.dispose();\n            }\n        }\n        this.texture?.dispose();\n        this.destroyViewportOutline();\n    }\n    destroyViewportOutline(): void {\n        if (this.viewportOutline) {\n            this.wrapperObj?.remove(this.viewportOutline);\n            this.viewportOutline.geometry.dispose();\n            if (Array.isArray(this.viewportOutline.material)) {\n                this.viewportOutline.material.forEach(material => material.dispose());\n            }\n            else {\n                this.viewportOutline.material.dispose();\n            }\n            this.viewportOutline = undefined;\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/MinimapPing.ts",
    "content": "import * as THREE from \"three\";\nimport { UiObject } from \"@/gui/UiObject\";\nexport interface RadarRules {\n    eventMinRadius: number;\n    eventSpeed: number;\n    eventRotationSpeed: number;\n    eventColorSpeed: number;\n}\nexport class MinimapPing extends UiObject {\n    radarRules: RadarRules;\n    colorLerpFactor: number;\n    hiColor: THREE.Color;\n    lowColor: THREE.Color;\n    matHiColor: THREE.Color;\n    matLowColor: THREE.Color;\n    lastUpdate?: number;\n    constructor(radarRules: RadarRules, hiColor: number | string, lowColor: number | string) {\n        super();\n        this.radarRules = radarRules;\n        this.colorLerpFactor = 0;\n        this.hiColor = new THREE.Color(hiColor);\n        this.lowColor = new THREE.Color(lowColor);\n        this.matHiColor = this.hiColor.clone();\n        this.matLowColor = this.lowColor.clone();\n        const minRadius = radarRules.eventMinRadius;\n        const ping = this.createPingRectLine(minRadius, minRadius, this.matHiColor, this.matLowColor);\n        ping.name = \"minimap_ping\";\n        ping.scale.x = 15;\n        ping.scale.y = 15;\n        this.set3DObject(ping);\n        this.get3DObject().matrixAutoUpdate = true;\n    }\n    createPingRectLine(width: number, height: number, color1: THREE.Color, color2: THREE.Color): THREE.LineSegments {\n        const verts = [\n            -0.5 * width, -0.5 * height, 0,\n            -0.5 * width, 0.5 * height, 0,\n            -0.5 * width, 0.5 * height, 0,\n            0.5 * width, 0.5 * height, 0,\n            0.5 * width, 0.5 * height, 0,\n            0.5 * width, -0.5 * height, 0,\n            0.5 * width, -0.5 * height, 0,\n            -0.5 * width, -0.5 * height, 0,\n        ];\n        const geometry = new THREE.BufferGeometry();\n        geometry.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));\n        const material = new THREE.LineBasicMaterial({\n            color: color1.getHex(),\n            side: THREE.DoubleSide,\n        });\n        return new THREE.LineSegments(geometry, material);\n    }\n    override get3DObject(): THREE.Object3D {\n        return super.get3DObject();\n    }\n    override update(now: number): void {\n        super.update(now);\n        if (!this.lastUpdate)\n            this.lastUpdate = now;\n        let t = ((now - this.lastUpdate) / 1000) * 60;\n        this.lastUpdate = now;\n        const obj = this.get3DObject();\n        const shrinkSpeed = this.radarRules.eventSpeed / this.radarRules.eventMinRadius;\n        obj.scale.x = Math.max(1, obj.scale.x - shrinkSpeed * t);\n        obj.scale.y = Math.max(1, obj.scale.y - shrinkSpeed * t);\n        obj.rotation.z += this.radarRules.eventRotationSpeed * t;\n        if (obj.scale.x === 1) {\n            obj.rotation.z = Math.min(obj.rotation.z, (Math.floor(obj.rotation.z / (Math.PI / 2)) * Math.PI) / 2);\n        }\n        this.colorLerpFactor = (this.colorLerpFactor + this.radarRules.eventColorSpeed * t) % 2;\n        let lerpT = Math.min(1, this.colorLerpFactor) - Math.max(0, this.colorLerpFactor - 1);\n        this.matHiColor.copy(this.hiColor).lerp(this.lowColor, lerpT);\n        this.matLowColor.copy(this.lowColor).lerp(this.hiColor, lerpT);\n    }\n    override destroy(): void {\n        super.destroy();\n        const obj = this.get3DObject() as any;\n        if (obj.material)\n            obj.material.dispose();\n        if (obj.geometry)\n            obj.geometry.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/DebugText.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { SpriteUtils } from \"@/engine/gfx/SpriteUtils\";\nimport { CanvasUtils } from \"@/engine/gfx/CanvasUtils\";\ntype ColorType = {\n    r: number;\n    g: number;\n    b: number;\n    getHexString: () => string;\n};\ninterface DebugTextProps extends UiComponentProps {\n    x?: number;\n    y?: number;\n    width: number;\n    height: number;\n    zIndex?: number;\n    color: ColorType;\n    text: {\n        value: string;\n    };\n    visible: {\n        value: boolean;\n    };\n}\nexport class DebugText extends UiComponent<DebugTextProps> {\n    declare ctx: CanvasRenderingContext2D | null;\n    declare texture: THREE.Texture;\n    declare mesh: THREE.Mesh;\n    declare lastUpdate?: number;\n    declare lastText?: string;\n    createUiObject(): UiObject {\n        const obj = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        obj.setPosition(this.props.x || 0, this.props.y || 0);\n        const width = this.props.width;\n        const height = this.props.height;\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = width;\n        canvas.height = height;\n        this.ctx = canvas.getContext(\"2d\", { alpha: true });\n        this.texture = this.createTexture(canvas);\n        this.mesh = this.createMesh(width, height);\n        return obj;\n    }\n    createTexture(canvas: HTMLCanvasElement): THREE.Texture {\n        const texture = new THREE.Texture(canvas);\n        texture.needsUpdate = true;\n        texture.flipY = false;\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        return texture;\n    }\n    createMesh(width: number, height: number): THREE.Mesh {\n        const geometry = SpriteUtils.createRectGeometry(width, height);\n        SpriteUtils.addRectUvs(geometry, { x: 0, y: 0, width, height }, { width, height });\n        geometry.translate(width / 2, height / 2, 0);\n        const material = new THREE.MeshBasicMaterial({\n            map: this.texture,\n            side: THREE.DoubleSide,\n            transparent: true,\n        });\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.frustumCulled = false;\n        return mesh;\n    }\n    defineChildren() {\n        return jsx(\"mesh\", { zIndex: this.props.zIndex }, this.mesh);\n    }\n    onFrame(t: number) {\n        if (!this.lastUpdate || t - this.lastUpdate >= 1000 / 30) {\n            this.lastUpdate = t;\n            const text = this.props.text.value;\n            if (this.props.visible.value !== this.getUiObject().isVisible()) {\n                this.getUiObject().setVisible(this.props.visible.value);\n            }\n            if (this.lastText !== text) {\n                this.lastText = text;\n                const lines = text.split(/\\r?\\n/g);\n                this.drawLines(lines);\n            }\n        }\n    }\n    drawLines(lines: string[]) {\n        if (!this.ctx)\n            return;\n        this.ctx.clearRect(0, 0, this.props.width, this.props.height);\n        const maxLineLen = Math.floor((110 * this.props.width) / 600);\n        let y = 0;\n        for (const line of lines) {\n            for (const wrapped of this.wrapText(line, maxLineLen)) {\n                y += this.drawLine(wrapped, this.props.color, y);\n            }\n        }\n        this.texture.needsUpdate = true;\n    }\n    drawLine(text: string, color: ColorType, y: number): number {\n        const style = {\n            fontFamily: \"'Fira Sans Condensed', Arial, sans-serif\",\n            fontSize: 12,\n            fontWeight: \"400\",\n            paddingTop: 6,\n            height: 20,\n        };\n        const outlineColor = 0.5 < 0.299 * color.r + 0.587 * color.g + 0.114 * color.b\n            ? \"black\"\n            : \"white\";\n        return CanvasUtils.drawText(this.ctx!, text, 0, y, {\n            color: \"#\" + color.getHexString(),\n            outlineColor,\n            outlineWidth: 2,\n            ...style,\n            paddingLeft: 4,\n            paddingRight: 4,\n        }).height;\n    }\n    wrapText(text: string, maxLen: number): string[] {\n        const lines: string[] = [];\n        while (text.length > maxLen) {\n            let idx = text.slice(0, maxLen).search(/\\s[^\\s]*$/);\n            if (idx === -1 || idx === 0)\n                idx = Math.min(text.length, maxLen);\n            lines.push(text.substr(0, idx));\n            text = text.slice(idx);\n        }\n        if (text.length)\n            lines.push(text);\n        return lines;\n    }\n    onDispose() {\n        (this.mesh.geometry as THREE.BufferGeometry).dispose();\n        (this.mesh.material as THREE.Material).dispose();\n        this.texture.dispose();\n    }\n}\nexport default DebugText;\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/GameMenuContentArea.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { SpriteUtils } from \"@/engine/gfx/SpriteUtils\";\ntype GameMenuContentAreaProps = UiComponentProps & {\n    viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    screenSize: {\n        width: number;\n        height: number;\n    };\n    images: Map<string, any>;\n    innerRef?: any;\n    hidden?: boolean;\n};\nexport class GameMenuContentArea extends UiComponent<GameMenuContentAreaProps> {\n    createUiObject({ viewport, hidden }: GameMenuContentAreaProps): UiObject {\n        const obj = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        obj.setPosition(viewport.x, viewport.y);\n        obj.getHtmlContainer().setSize(viewport.width, viewport.height);\n        obj.setVisible(!hidden);\n        return obj;\n    }\n    defineChildren() {\n        const { viewport, screenSize, images, innerRef, } = this.props;\n        let size = \"lg\";\n        if (screenSize.width < 1024 || screenSize.height < 768)\n            size = \"md\";\n        if (screenSize.width < 800 || screenSize.height < 600)\n            size = \"sm\";\n        const bkgd = images.get(`bkgd${size}.shp`);\n        const x = bkgd ? (viewport.width - bkgd.width) / 2 : 0;\n        const y = bkgd ? (viewport.height - bkgd.height) / 2 : 0;\n        const width = (bkgd || viewport).width;\n        const height = (bkgd || viewport).height;\n        return jsx(\"fragment\", null, jsx(\"mesh\", null, this.createMask(viewport)), jsx(\"container\", { zIndex: 1, x, y, width, height, ref: innerRef }, bkgd && jsx(\"sprite\", { image: bkgd, palette: \"uibkgd.pal\" })));\n    }\n    createMask(viewport: {\n        width: number;\n        height: number;\n    }) {\n        const geometry = SpriteUtils.createRectGeometry(viewport.width, viewport.height);\n        geometry.translate(viewport.width / 2, viewport.height / 2, 0);\n        const material = new THREE.MeshBasicMaterial({\n            color: 0,\n            opacity: 0.75,\n            transparent: true,\n            side: THREE.DoubleSide,\n        });\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.frustumCulled = false;\n        return mesh;\n    }\n}\nexport default GameMenuContentArea;\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/HudChat.tsx",
    "content": "import React from \"react\";\nimport { ChatInput } from \"@/gui/component/ChatInput\";\nimport { RECIPIENT_ALL, RECIPIENT_TEAM } from \"@/network/gservConfig\";\ntype HudChatProps = {\n    messageList: any;\n    chatHistory: any;\n    strings: any;\n    onSubmit: (e: any) => void;\n    onCancel: () => void;\n};\nexport const HudChat: React.FC<HudChatProps & {\n    isComposing: boolean;\n    localPlayer?: {\n        color: {\n            asHexString: () => string;\n        };\n    };\n}> = ({ messageList, chatHistory, strings, onSubmit, onCancel, isComposing, localPlayer, }) => {\n    if (!isComposing)\n        return null;\n    const forceColor = localPlayer?.color.asHexString() ?? \"white\";\n    return (<ChatInput chatHistory={chatHistory} channels={[RECIPIENT_ALL, RECIPIENT_TEAM]} className=\"game-chat-input\" forceColor={forceColor} noCycleHint={true} submitEmpty={true} strings={strings} onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {\n            if (e.key === \"Escape\")\n                e.preventDefault();\n            e.stopPropagation();\n            (e.nativeEvent as KeyboardEvent & {\n                stopImmediatePropagation?: () => void;\n            }).stopImmediatePropagation?.();\n        }} onKeyUp={(e: React.KeyboardEvent<HTMLInputElement>) => {\n            e.stopPropagation();\n            (e.nativeEvent as KeyboardEvent & {\n                stopImmediatePropagation?: () => void;\n            }).stopImmediatePropagation?.();\n        }} onSubmit={(e: any) => {\n            e.value.length ? onSubmit(e) : onCancel();\n        }} onCancel={onCancel} onBlur={onCancel}/>);\n};\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/Messages.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { SpriteUtils } from \"@/engine/gfx/SpriteUtils\";\nimport { CanvasUtils } from \"@/engine/gfx/CanvasUtils\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { HudChat } from \"./HudChat\";\nimport { ChatRecipientType } from \"@/network/chat/ChatMessage\";\nimport { RECIPIENT_ALL } from \"@/network/gservConfig\";\ntype Message = {\n    color: string;\n    text: string;\n    animate: boolean;\n    time: number;\n};\ntype MessagesProps = UiComponentProps & {\n    x?: number;\n    y?: number;\n    width: number;\n    height: number;\n    zIndex?: number;\n    strings: any;\n    messages: {\n        getAll: () => Message[];\n        prune: () => void;\n        isComposing: boolean;\n    };\n    chatHistory: any;\n    onMessageSubmit: (e: any) => void;\n    onMessageCancel: () => void;\n    onMessageTick?: () => void;\n};\nexport class Messages extends UiComponent<MessagesProps> {\n    declare ctx: CanvasRenderingContext2D | null;\n    declare texture: THREE.Texture;\n    declare mesh: THREE.Mesh;\n    declare inputContainer: any;\n    declare inputComponent: any;\n    declare lastUpdate?: number;\n    declare lastMessageTime?: number;\n    declare lastMessageCount?: number;\n    declare lastComposing?: boolean;\n    createUiObject(): UiObject {\n        const obj = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        obj.setPosition(this.props.x || 0, this.props.y || 0);\n        const width = this.props.width;\n        const height = this.props.height;\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = width;\n        canvas.height = height;\n        this.ctx = canvas.getContext(\"2d\", { alpha: true });\n        this.texture = this.createTexture(canvas);\n        this.mesh = this.createMesh(width, height);\n        return obj;\n    }\n    createTexture(canvas: HTMLCanvasElement): THREE.Texture {\n        const texture = new THREE.Texture(canvas);\n        texture.needsUpdate = true;\n        texture.flipY = false;\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        return texture;\n    }\n    createMesh(width: number, height: number): THREE.Mesh {\n        const geometry = SpriteUtils.createRectGeometry(width, height);\n        SpriteUtils.addRectUvs(geometry, { x: 0, y: 0, width, height }, { width, height });\n        geometry.translate(width / 2, height / 2, 0);\n        const material = new THREE.MeshBasicMaterial({\n            map: this.texture,\n            side: THREE.DoubleSide,\n            transparent: true,\n        });\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.frustumCulled = false;\n        return mesh;\n    }\n    defineChildren() {\n        return jsx(\"fragment\", null, jsx(\"container\", { hidden: true, ref: (e: any) => (this.inputContainer = e) }, jsx(HtmlView, {\n            component: HudChat,\n            props: {\n                strings: this.props.strings,\n                messageList: this.props.messages,\n                chatHistory: this.props.chatHistory,\n                onSubmit: this.props.onMessageSubmit,\n                onCancel: this.props.onMessageCancel,\n            },\n            innerRef: (e: any) => (this.inputComponent = e),\n        })), jsx(\"mesh\", { zIndex: this.props.zIndex }, this.mesh));\n    }\n    onFrame(now: number) {\n        if (!this.lastUpdate || now - this.lastUpdate >= 1000 / 30) {\n            this.lastUpdate = now;\n            this.props.messages.prune();\n            const messages = this.props.messages.getAll();\n            const nowTime = Date.now();\n            const lastMsgTime = messages[messages.length - 1]?.time;\n            const msgCount = messages.length;\n            const isComposing = this.props.messages.isComposing;\n            if (this.lastComposing !== isComposing ||\n                this.lastMessageTime !== lastMsgTime ||\n                msgCount !== this.lastMessageCount ||\n                (lastMsgTime && nowTime - lastMsgTime <= 2000)) {\n                this.lastMessageTime = lastMsgTime;\n                this.lastMessageCount = msgCount;\n                this.lastComposing = isComposing;\n                this.drawMessages(isComposing, messages, nowTime);\n                this.inputContainer.setVisible(isComposing);\n                this.inputComponent.refresh();\n            }\n        }\n    }\n    drawMessages(isComposing: boolean, messages: Message[], now: number) {\n        if (!this.ctx)\n            return;\n        this.ctx.clearRect(0, 0, this.props.width, this.props.height);\n        const maxLineLength = Math.floor((110 * this.props.width) / 600);\n        let needsTick = false;\n        let y = 0;\n        let msgList = messages;\n        if (isComposing) {\n            y = 20;\n            const composeTarget = this.props.chatHistory.lastComposeTarget.value;\n            if (!(composeTarget.type === ChatRecipientType.Channel &&\n                composeTarget.name === RECIPIENT_ALL)) {\n                msgList = [\n                    {\n                        color: \"gray\",\n                        text: this.props.strings.get(\"TS:ChatCycleHint\", \"Tab\"),\n                        animate: false,\n                        time: Date.now(),\n                    },\n                    ...messages,\n                ];\n            }\n        }\n        for (const msg of msgList) {\n            const animDuration = Math.min(1000, 10 * msg.text.length);\n            const animProgress = msg.animate ? Math.min(1, (now - msg.time) / animDuration) : 1;\n            let charsToShow = Math.round(animProgress * msg.text.length);\n            if (animProgress < 1)\n                needsTick = true;\n            for (let line of this.wrapText(msg.text, maxLineLength)) {\n                if (line.length > charsToShow) {\n                    line = line.slice(0, charsToShow);\n                    charsToShow = 0;\n                }\n                else {\n                    charsToShow -= line.length;\n                }\n                y += this.drawLine(line, msg.color, y);\n            }\n        }\n        this.texture.needsUpdate = true;\n        if (needsTick)\n            this.props.onMessageTick?.();\n    }\n    drawLine(text: string, color: string, y: number): number {\n        return CanvasUtils.drawText(this.ctx, text, 0, y, {\n            color,\n            fontFamily: \"'Fira Sans Condensed', Arial, sans-serif\",\n            fontSize: 13,\n            fontWeight: \"500\",\n            paddingTop: 5,\n            height: 20,\n            backgroundColor: \"rgba(0, 0, 0, .75)\",\n            paddingLeft: 4,\n            paddingRight: 4,\n        }).height;\n    }\n    wrapText(text: string, maxLen: number): string[] {\n        const lines: string[] = [];\n        while (text.length > maxLen) {\n            let idx = text.slice(0, maxLen).search(/\\s[^\\s]*$/);\n            if (idx === -1 || idx === 0)\n                idx = Math.min(text.length, maxLen);\n            lines.push(text.substr(0, idx));\n            text = text.slice(idx);\n        }\n        if (text.length)\n            lines.push(text);\n        return lines;\n    }\n    onDispose() {\n        this.mesh.geometry.dispose();\n        (this.mesh.material as THREE.Material).dispose();\n        this.texture.dispose();\n    }\n}\nexport default Messages;\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/ReplayStatsOverlay.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { SpriteUtils } from \"@/engine/gfx/SpriteUtils\";\nimport { QueueType, QueueStatus } from \"@/game/player/production/ProductionQueue\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { formatTimeDuration } from \"@/util/format\";\n\ntype Player = {\n    name: string;\n    credits: number;\n    defeated: boolean;\n    resigned: boolean;\n    color: {\n        asHexString: () => string;\n    };\n    powerTrait?: {\n        power: number;\n        drain: number;\n    };\n    superWeaponsTrait?: {\n        getAll: () => Array<{\n            name: string;\n            status: number; // SuperWeaponStatus\n            getTimerSeconds: () => number;\n            getChargeProgress: () => number;\n            rules: {\n                showTimer: boolean;\n                uiName: string;\n                rechargeTime: number;\n            };\n        }>;\n    };\n    production?: {\n        getAllQueues: () => Array<{\n            type: QueueType;\n            status: QueueStatus;\n            getFirst: () =>\n                | {\n                      rules: { name: string; uiName?: string };\n                      quantity: number;\n                      progress: number;\n                  }\n                | undefined;\n            getAll: () => Array<{\n                rules: { name: string; uiName?: string };\n                quantity: number;\n                progress: number;\n            }>;\n        }>;\n    };\n    getOwnedObjectsByType: (type: ObjectType, includeLimbo?: boolean) => any[];\n    getOwnedObjects: (includeLimbo?: boolean) => any[];\n};\n\ninterface ReplayStatsOverlayProps extends UiComponentProps {\n    x?: number;\n    y?: number;\n    zIndex?: number;\n    width: number;\n    height: number;\n    players: Player[];\n    strings: {\n        get: (key: string) => string;\n    };\n}\n\nconst FONT = \"'Fira Sans Condensed', Arial, sans-serif\";\nconst LINE_HEIGHT = 16;\nconst SECTION_GAP = 4;\nconst COL_WIDTH = 220;\nconst PADDING = 6;\n\nconst QUEUE_TYPE_LABELS: Record<number, string> = {\n    [QueueType.Structures]: \"建筑\",\n    [QueueType.Armory]: \"防御\",\n    [QueueType.Infantry]: \"步兵\",\n    [QueueType.Vehicles]: \"载具\",\n    [QueueType.Aircrafts]: \"空军\",\n    [QueueType.Ships]: \"海军\",\n};\n\nexport class ReplayStatsOverlay extends UiComponent<ReplayStatsOverlayProps> {\n    declare ctx: CanvasRenderingContext2D;\n    declare texture: THREE.Texture;\n    declare mesh: THREE.Mesh;\n    lastUpdate?: number;\n\n    createUiObject() {\n        const obj = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        obj.setPosition(this.props.x || 0, this.props.y || 0);\n        const { width, height } = this.props;\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = width;\n        canvas.height = height;\n        this.ctx = canvas.getContext(\"2d\", { alpha: true })!;\n        this.texture = this.createTexture(canvas);\n        this.mesh = this.createMesh(width, height);\n        return obj;\n    }\n\n    createTexture(canvas: HTMLCanvasElement) {\n        const texture = new THREE.Texture(canvas);\n        texture.needsUpdate = true;\n        texture.flipY = false;\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        return texture;\n    }\n\n    createMesh(width: number, height: number) {\n        const geometry = SpriteUtils.createRectGeometry(width, height);\n        SpriteUtils.addRectUvs(\n            geometry,\n            { x: 0, y: 0, width, height },\n            { width, height },\n        );\n        geometry.translate(width / 2, height / 2, 0);\n        const material = new THREE.MeshBasicMaterial({\n            map: this.texture,\n            side: THREE.DoubleSide,\n            transparent: true,\n        });\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.frustumCulled = false;\n        return mesh;\n    }\n\n    defineChildren() {\n        return jsx(\"mesh\", { zIndex: this.props.zIndex }, this.mesh);\n    }\n\n    onFrame(now: number) {\n        // Update at ~5 fps to avoid perf impact\n        if (!this.lastUpdate || now - this.lastUpdate >= 200) {\n            this.lastUpdate = now;\n            this.render();\n        }\n    }\n\n    private render() {\n        const ctx = this.ctx;\n        const { width, height, players } = this.props;\n        ctx.clearRect(0, 0, width, height);\n\n        const activePlayers = players.filter(\n            (p) => !p.defeated && !p.resigned,\n        );\n        if (activePlayers.length === 0) return;\n\n        // Layout: place player panels in columns\n        const numCols = Math.min(activePlayers.length, 4);\n        const colW = Math.min(COL_WIDTH, Math.floor((width - PADDING * 2) / numCols));\n\n        for (let i = 0; i < activePlayers.length; i++) {\n            const player = activePlayers[i];\n            const col = i % numCols;\n            const row = Math.floor(i / numCols);\n            const x = PADDING + col * (colW + SECTION_GAP);\n            const baseY = PADDING + row * 200; // rough estimate per player block\n            this.renderPlayer(ctx, player, x, baseY, colW);\n        }\n\n        this.texture.needsUpdate = true;\n    }\n\n    private renderPlayer(\n        ctx: CanvasRenderingContext2D,\n        player: Player,\n        x: number,\n        startY: number,\n        colWidth: number,\n    ) {\n        let y = startY;\n\n        // ── Player name header with colored underline ──\n        const color = player.color.asHexString();\n        ctx.fillStyle = \"rgba(0, 0, 0, 0.6)\";\n        ctx.fillRect(x, y, colWidth, LINE_HEIGHT + 2);\n        ctx.fillStyle = color;\n        ctx.font = `bold 12px ${FONT}`;\n        ctx.textBaseline = \"top\";\n        ctx.fillText(player.name, x + 4, y + 2);\n        // Thin colored underline\n        ctx.fillStyle = color;\n        ctx.fillRect(x, y + LINE_HEIGHT, colWidth, 2);\n        y += LINE_HEIGHT + 2 + SECTION_GAP;\n\n        // ── Credits & Power ──\n        const credits = Math.floor(player.credits);\n        const powerTotal = player.powerTrait?.power ?? 0;\n        const powerDrain = player.powerTrait?.drain ?? 0;\n        const powerColor =\n            powerDrain > powerTotal ? \"#ff4444\" : \"#88ff88\";\n\n        ctx.fillStyle = \"rgba(0, 0, 0, 0.5)\";\n        ctx.fillRect(x, y, colWidth, LINE_HEIGHT);\n        ctx.font = `11px ${FONT}`;\n        ctx.fillStyle = \"#ffd700\";\n        ctx.fillText(`$${credits}`, x + 4, y + 2);\n        ctx.fillStyle = powerColor;\n        const powerText = `⚡${powerTotal}/${powerDrain}`;\n        ctx.fillText(powerText, x + colWidth / 2, y + 2);\n        y += LINE_HEIGHT + 1;\n\n        // ── Unit Counts ──\n        const buildings = player.getOwnedObjectsByType(\n            ObjectType.Building,\n        ).length;\n        const infantry = player.getOwnedObjectsByType(\n            ObjectType.Infantry,\n        ).length;\n        const vehicles = player.getOwnedObjectsByType(\n            ObjectType.Vehicle,\n        ).length;\n        const aircraft = player.getOwnedObjectsByType(\n            ObjectType.Aircraft,\n        ).length;\n        const totalUnits = buildings + infantry + vehicles + aircraft;\n\n        ctx.fillStyle = \"rgba(0, 0, 0, 0.5)\";\n        ctx.fillRect(x, y, colWidth, LINE_HEIGHT);\n        ctx.font = `11px ${FONT}`;\n        ctx.fillStyle = \"#cccccc\";\n        const countsText = `🏠${buildings} 🚶${infantry} 🚛${vehicles}`;\n        ctx.fillText(countsText, x + 4, y + 2);\n        if (aircraft > 0) {\n            ctx.fillText(`✈${aircraft}`, x + colWidth - 40, y + 2);\n        }\n        // Total on right side\n        ctx.fillStyle = \"#aaaaaa\";\n        ctx.textAlign = \"right\";\n        ctx.fillText(`Σ${totalUnits}`, x + colWidth - 4, y + 2);\n        ctx.textAlign = \"left\";\n        y += LINE_HEIGHT + 1;\n\n        // ── Production Queues ──\n        if (player.production) {\n            const queues = player.production.getAllQueues();\n            for (const queue of queues) {\n                const first = queue.getFirst();\n                if (\n                    queue.status === QueueStatus.Idle ||\n                    !first\n                )\n                    continue;\n\n                const label =\n                    QUEUE_TYPE_LABELS[queue.type] || \"?\";\n                const itemName = this.resolveUiName(first.rules);\n                const progress = Math.floor(first.progress * 100);\n                const statusStr =\n                    queue.status === QueueStatus.OnHold\n                        ? \" ⏸\"\n                        : queue.status === QueueStatus.Ready\n                          ? \" ✓\"\n                          : \"\";\n\n                ctx.fillStyle = \"rgba(0, 0, 0, 0.45)\";\n                ctx.fillRect(x, y, colWidth, LINE_HEIGHT);\n\n                // Label\n                ctx.fillStyle = \"#999999\";\n                ctx.font = `10px ${FONT}`;\n                ctx.fillText(label, x + 4, y + 3);\n\n                // Item name + progress\n                ctx.fillStyle = \"#dddddd\";\n                ctx.font = `11px ${FONT}`;\n                ctx.fillText(`${itemName}`, x + 36, y + 2);\n\n                // Progress bar\n                if (\n                    queue.status === QueueStatus.Active &&\n                    first.progress > 0\n                ) {\n                    const barX = x + colWidth - 54;\n                    const barW = 40;\n                    const barH = 8;\n                    const barY = y + 4;\n                    ctx.fillStyle = \"rgba(255, 255, 255, 0.15)\";\n                    ctx.fillRect(barX, barY, barW, barH);\n                    ctx.fillStyle = color;\n                    ctx.globalAlpha = 0.7;\n                    ctx.fillRect(\n                        barX,\n                        barY,\n                        barW * first.progress,\n                        barH,\n                    );\n                    ctx.globalAlpha = 1;\n                    ctx.fillStyle = \"#ffffff\";\n                    ctx.font = `9px ${FONT}`;\n                    ctx.textAlign = \"center\";\n                    ctx.fillText(\n                        `${progress}%`,\n                        barX + barW / 2,\n                        barY,\n                    );\n                    ctx.textAlign = \"left\";\n                } else {\n                    ctx.fillStyle = \"#aaaaaa\";\n                    ctx.textAlign = \"right\";\n                    ctx.font = `10px ${FONT}`;\n                    ctx.fillText(statusStr, x + colWidth - 4, y + 3);\n                    ctx.textAlign = \"left\";\n                }\n\n                // Multiple items indicator\n                const allItems = queue.getAll();\n                if (allItems.length > 1 || first.quantity > 1) {\n                    const totalQ = allItems.reduce(\n                        (sum, item) => sum + item.quantity,\n                        0,\n                    );\n                    if (totalQ > 1) {\n                        ctx.fillStyle = \"#aaaaaa\";\n                        ctx.font = `9px ${FONT}`;\n                        ctx.fillText(\n                            `×${totalQ}`,\n                            x + 80,\n                            y + 3,\n                        );\n                    }\n                }\n\n                y += LINE_HEIGHT;\n            }\n        }\n\n        // ── Superweapon Countdowns ──\n        if (player.superWeaponsTrait) {\n            const superWeapons = player.superWeaponsTrait.getAll();\n            for (const sw of superWeapons) {\n                if (!sw.rules.showTimer) continue;\n                const seconds = Math.floor(sw.getTimerSeconds());\n                const label = this.props.strings.get(sw.rules.uiName);\n                const isReady = seconds <= 0;\n                const progress = sw.getChargeProgress();\n\n                ctx.fillStyle = \"rgba(0, 0, 0, 0.45)\";\n                ctx.fillRect(x, y, colWidth, LINE_HEIGHT);\n\n                ctx.font = `11px ${FONT}`;\n\n                if (isReady) {\n                    // Ready - flash\n                    const flash =\n                        Math.floor(Date.now() / 500) % 2 === 0;\n                    ctx.fillStyle = flash ? \"#ff4444\" : \"#ffaa00\";\n                    ctx.fillText(`☢ ${label} READY`, x + 4, y + 2);\n                } else {\n                    ctx.fillStyle = \"#ff8800\";\n                    ctx.fillText(`☢ ${label}`, x + 4, y + 2);\n\n                    // Timer\n                    ctx.fillStyle = \"#ffcc66\";\n                    ctx.textAlign = \"right\";\n                    ctx.fillText(\n                        formatTimeDuration(seconds, false),\n                        x + colWidth - 48,\n                        y + 2,\n                    );\n                    ctx.textAlign = \"left\";\n\n                    // Small progress bar\n                    const barX = x + colWidth - 44;\n                    const barW = 40;\n                    const barH = 6;\n                    const barY = y + 5;\n                    ctx.fillStyle = \"rgba(255, 255, 255, 0.12)\";\n                    ctx.fillRect(barX, barY, barW, barH);\n                    ctx.fillStyle = \"#ff6600\";\n                    ctx.globalAlpha = 0.8;\n                    ctx.fillRect(\n                        barX,\n                        barY,\n                        barW * progress,\n                        barH,\n                    );\n                    ctx.globalAlpha = 1;\n                }\n\n                y += LINE_HEIGHT;\n            }\n        }\n    }\n\n    /**\n     * Resolve a rules object's uiName to a localized display string.\n     * Falls back to the internal code name if no localized string is found.\n     */\n    private resolveUiName(rules: { name: string; uiName?: string }): string {\n        const uiName = (rules as any).uiName;\n        if (uiName && uiName !== \"\") {\n            const resolved = this.props.strings.get(uiName);\n            if (resolved && resolved !== uiName) {\n                return resolved;\n            }\n        }\n        return rules.name;\n    }\n\n    onDispose() {\n        this.mesh.geometry.dispose();\n        (this.mesh.material as THREE.Material).dispose();\n        this.texture.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/SidebarCard.ts",
    "content": "import * as jsx from \"@/gui/jsx/jsx\";\nimport * as SidebarModel from \"@/gui/screen/game/component/hud/viewmodel/SidebarModel\";\nimport { SidebarItemStatus } from \"@/gui/screen/game/component/hud/viewmodel/SidebarModel\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { OverlayUtils } from \"@/engine/gfx/OverlayUtils\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { clamp } from \"@/util/math\";\nimport { ObjectArt } from \"@/game/art/ObjectArt\";\nimport { resolveSidebarItemTooltipText } from \"@/gui/screen/game/TooltipTextResolver\";\ndeclare const THREE: any;\nenum LabelType {\n    Ready = 0,\n    OnHold = 1\n}\ninterface SidebarCardProps extends UiComponentProps {\n    x?: number;\n    y?: number;\n    zIndex?: number;\n    slots: number;\n    slotSize?: {\n        width: number;\n        height: number;\n    };\n    cameoImages: any;\n    cameoPalette: string;\n    sidebarModel: any;\n    onSlotClick?: (event: any) => void;\n    textColor: string;\n    cameoNameToIdMap: Map<string, number>;\n    strings: any;\n    persistentHoverTags?: {\n        value: boolean;\n    };\n}\ninterface SlotClickEvent {\n    target: any;\n    button: number;\n    altKey: boolean;\n    ctrlKey: boolean;\n    metaKey: boolean;\n    shiftKey: boolean;\n    isTouch: boolean;\n    touchDuration: number;\n}\nexport class SidebarCard extends UiComponent<SidebarCardProps> {\n    static readonly MAX_QUANTITY = 99;\n    static readonly labelImageCache = new Map<string, any[]>();\n    static readonly quantityImageCache = new Map<string, any[]>();\n    private slotContainers: any[] = [];\n    private slotObjects: any[] = [];\n    private progressOverlays: any[] = [];\n    private visible: boolean = true;\n    private labelObjects: any[] = [];\n    private quantityObjects: any[] = [];\n    private tagObjects: any[] = [];\n    private justCreated: boolean = true;\n    private lastItemCount: number = 0;\n    private pagingOffset: number = 0;\n    private declare slotOutline: UiObject;\n    private declare labelImages: any[];\n    private declare quantityImages: any[];\n    private declare tagImages: any[];\n    private declare tagFrameByText: Map<string, number>;\n    private lastActiveTab?: any;\n    private hoverSlotIndex?: number;\n    constructor(props: SidebarCardProps) {\n        super(props);\n        this.handleWheel = (e: any) => {\n            this.scrollToOffset(this.pagingOffset + (0 < e.wheelDeltaY ? 2 : -2));\n        };\n    }\n    private handleWheel: (e: any) => void;\n    createUiObject(): UiObject {\n        const uiObject = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        uiObject.setPosition(this.props.x || 0, this.props.y || 0);\n        uiObject.onFrame.subscribe(() => this.handleFrame());\n        this.slotOutline = new UiObject(this.createSlotOutline());\n        this.slotOutline.setVisible(false);\n        this.slotOutline.setZIndex((this.props.zIndex ?? 0) + 1);\n        uiObject.add(this.slotOutline);\n        let labelImages = SidebarCard.labelImageCache.get(this.props.textColor);\n        if (!labelImages) {\n            labelImages = this.createLabelImages(this.props.textColor);\n            SidebarCard.labelImageCache.set(this.props.textColor, labelImages);\n        }\n        this.labelImages = labelImages;\n        let quantityImages = SidebarCard.quantityImageCache.get(this.props.textColor);\n        if (!quantityImages) {\n            quantityImages = this.createQuantityImages(this.props.textColor);\n            SidebarCard.quantityImageCache.set(this.props.textColor, quantityImages);\n        }\n        this.quantityImages = quantityImages;\n        this.tagImages = [\n            this.createTextBox(\"\", this.props.textColor, {\n                fontSize: 12,\n                fontWeight: \"400\",\n                paddingTop: 2,\n                paddingBottom: 2,\n                paddingLeft: 2,\n                paddingRight: 2,\n            }),\n        ];\n        this.tagFrameByText = new Map();\n        this.tagFrameByText.set(\"\", 0);\n        return uiObject;\n    }\n    defineChildren(): any[] {\n        const { slots, cameoImages, cameoPalette, sidebarModel, onSlotClick, zIndex, } = this.props;\n        const slotSize = this.getSlotSize();\n        const horizontalSpacing = 3;\n        const verticalSpacing = 2;\n        const children = [];\n        for (let slotIndex = 0; slotIndex < slots; slotIndex++) {\n            const position = {\n                x: (horizontalSpacing + slotSize.width) * (slotIndex % 2),\n                y: (verticalSpacing + slotSize.height) * Math.floor(slotIndex / 2),\n            };\n            children.push(jsx.jsx(\"container\", {\n                x: position.x,\n                y: position.y,\n                zIndex: zIndex,\n                ref: (element: any) => this.slotContainers.push(element),\n                onWheel: this.handleWheel,\n                onClick: (event: any) => {\n                    const item = sidebarModel.activeTab.items[this.getItemIndexAtSlot(slotIndex)];\n                    if (item && !item.disabled) {\n                        onSlotClick?.(this.createSlotClickEvent(item, event));\n                    }\n                },\n                onMouseEnter: () => {\n                    const item = sidebarModel.activeTab.items[this.getItemIndexAtSlot(slotIndex)];\n                    if (item) {\n                        if (!item.disabled) {\n                            this.slotOutline.setPosition(position.x, position.y);\n                        }\n                        this.slotOutline.setVisible(!item.disabled);\n                        this.hoverSlotIndex = slotIndex;\n                    }\n                },\n                onMouseLeave: () => {\n                    if (this.hoverSlotIndex === slotIndex) {\n                        this.slotOutline.setVisible(false);\n                        this.hoverSlotIndex = undefined;\n                    }\n                },\n            }, jsx.jsx(\"sprite\", {\n                image: \"gclock2.shp\",\n                palette: \"sidebar.pal\",\n                zIndex: 1,\n                frame: 0,\n                opacity: 0.5,\n                transparent: true,\n                ref: (element: any) => this.progressOverlays.push(element),\n            }), jsx.jsx(\"sprite\", {\n                images: this.tagImages,\n                zIndex: 0.5,\n                x: slotSize.width / 2,\n                y: slotSize.height / 2,\n                transparent: true,\n                ref: (element: any) => this.tagObjects.push(element),\n            }), jsx.jsx(\"sprite\", {\n                images: this.labelImages,\n                zIndex: 2,\n                x: slotSize.width / 2,\n                transparent: true,\n                ref: (element: any) => this.labelObjects.push(element),\n            }), jsx.jsx(\"sprite\", {\n                images: this.quantityImages,\n                zIndex: 2,\n                x: slotSize.width,\n                alignX: 1,\n                alignY: -1,\n                transparent: true,\n                ref: (element: any) => this.quantityObjects.push(element),\n            }), jsx.jsx(\"sprite\", {\n                image: cameoImages,\n                palette: cameoPalette,\n                ref: (element: any) => this.slotObjects.push(element),\n            })));\n        }\n        return children;\n    }\n    createSlotClickEvent(item: any, event: any): SlotClickEvent {\n        return {\n            target: item.target,\n            button: event.button,\n            altKey: event.altKey,\n            ctrlKey: event.ctrlKey,\n            metaKey: event.metaKey,\n            shiftKey: event.shiftKey,\n            isTouch: event.isTouch,\n            touchDuration: event.touchDuration,\n        };\n    }\n    handleFrame(): void {\n        const { sidebarModel, slots } = this.props;\n        const obj3D = this.getUiObject().get3DObject();\n        obj3D.visible = this.visible;\n        if (this.justCreated ||\n            sidebarModel.activeTab.needsUpdate ||\n            this.lastActiveTab !== sidebarModel.activeTab) {\n            this.justCreated = false;\n            const itemCount = sidebarModel.activeTab.items.length;\n            if (this.lastActiveTab !== sidebarModel.activeTab ||\n                this.lastItemCount !== itemCount) {\n                if (this.lastItemCount > itemCount) {\n                    this.pagingOffset = 0;\n                }\n                this.lastItemCount = itemCount;\n            }\n            this.lastActiveTab = sidebarModel.activeTab;\n            sidebarModel.activeTab.needsUpdate = false;\n            this.updateSlots(sidebarModel.activeTab.items, slots);\n        }\n    }\n    updateSlots(items: any[], slotCount: number): void {\n        for (let slotIndex = 0; slotIndex < slotCount; slotIndex++) {\n            const item = items[this.getItemIndexAtSlot(slotIndex)];\n            const slotObject = this.slotObjects[slotIndex];\n            const progressOverlay = this.progressOverlays[slotIndex];\n            const labelObject = this.labelObjects[slotIndex];\n            const quantityObject = this.quantityObjects[slotIndex];\n            const tagObject = this.tagObjects[slotIndex];\n            if (items.length - this.pagingOffset <= slotIndex) {\n                slotObject.get3DObject().visible = false;\n                progressOverlay.get3DObject().visible = false;\n                labelObject.get3DObject().visible = false;\n                quantityObject.get3DObject().visible = false;\n                tagObject.get3DObject().visible = false;\n            }\n            else {\n                this.updateCameo(item, slotObject);\n                this.updatePersistentTag(item, tagObject);\n                this.updateProgressOverlay(item, progressOverlay);\n                this.updateStatusText(item, labelObject);\n                this.updateQuantities(item, quantityObject);\n                this.updateTooltip(item, this.slotContainers[slotIndex]);\n            }\n        }\n    }\n    updateCameo(item: any, slotObject: any): void {\n        const cameoNameToIdMap = this.props.cameoNameToIdMap;\n        let cameoName = item.cameo + \".shp\";\n        let frameId = cameoNameToIdMap.get(cameoName);\n        if (frameId === undefined) {\n            cameoName = (ObjectArt as any).MISSING_CAMEO + \".shp\";\n            frameId = cameoNameToIdMap.get(cameoName);\n        }\n        if (frameId === undefined) {\n            throw new Error(`Missing cameo placeholder image \"${(ObjectArt as any).MISSING_CAMEO}.shp\"`);\n        }\n        slotObject.setFrame(frameId);\n        slotObject.get3DObject().visible = true;\n        slotObject.setLightMult(item.disabled ? 0.5 : 1);\n    }\n    updateProgressOverlay(item: any, progressOverlay: any): void {\n        let frame = 0;\n        if ([SidebarItemStatus.Started, SidebarItemStatus.OnHold].includes(item.status)) {\n            const frameCount = progressOverlay.getFrameCount();\n            frame = Math.max(1, Math.ceil(item.progress * (frameCount - 1))) % frameCount;\n        }\n        progressOverlay.setFrame(frame);\n        progressOverlay.get3DObject().visible = frame > 0;\n    }\n    updateStatusText(item: any, labelObject: any): void {\n        const isVisible = [SidebarItemStatus.Ready, SidebarItemStatus.OnHold].includes(item.status);\n        if (!labelObject || !labelObject.get3DObject)\n            return;\n        labelObject.get3DObject().visible = isVisible;\n        if (typeof labelObject.setFrame !== 'function' || typeof labelObject.setPosition !== 'function')\n            return;\n        const labelAlign = (labelObject as any).builder?.setAlign ? (labelObject as any).builder.setAlign.bind((labelObject as any).builder) : undefined;\n        const slotSize = this.getSlotSize();\n        if (item.status === SidebarItemStatus.Ready) {\n            labelObject.setFrame(LabelType.Ready);\n            labelObject.setPosition(slotSize.width / 2, labelObject.getPosition().y);\n            if (labelAlign)\n                labelAlign(0, -1);\n        }\n        else if (item.status === SidebarItemStatus.OnHold) {\n            labelObject.setFrame(LabelType.OnHold);\n            const xPos = item.quantity > 1 ? 0 : slotSize.width / 2;\n            labelObject.setPosition(xPos, labelObject.getPosition().y);\n            if (labelAlign)\n                labelAlign(item.quantity > 1 ? -1 : 0, -1);\n        }\n    }\n    updateQuantities(item: any, quantityObject: any): void {\n        const threshold = item.status === SidebarItemStatus.InQueue ? 0 : 1;\n        if (item.quantity > threshold) {\n            const frame = item.quantity > SidebarCard.MAX_QUANTITY\n                ? SidebarCard.MAX_QUANTITY\n                : item.quantity - 1;\n            if (quantityObject && typeof quantityObject.setFrame === 'function') {\n                quantityObject.setFrame(frame);\n            }\n            quantityObject?.setVisible?.(true);\n            if (quantityObject && !quantityObject.setVisible && quantityObject.get3DObject) {\n                const obj = quantityObject.get3DObject();\n                if (obj)\n                    obj.visible = true;\n            }\n        }\n        else {\n            quantityObject?.setVisible?.(false);\n            if (quantityObject && !quantityObject.setVisible && quantityObject.get3DObject) {\n                const obj = quantityObject.get3DObject();\n                if (obj)\n                    obj.visible = false;\n            }\n        }\n    }\n    updateTooltip(item: any, container: any): void {\n        const tooltip = resolveSidebarItemTooltipText(item, this.props.sidebarModel, this.props.strings);\n        container.setTooltip(tooltip);\n    }\n    ensureTagFrame(text?: string): number {\n        const resolvedText = text ?? \"\";\n        const existingFrame = this.tagFrameByText.get(resolvedText);\n        if (existingFrame !== undefined) {\n            return existingFrame;\n        }\n        const frame = this.tagImages.length;\n        const image = this.createTextBox(resolvedText, this.props.textColor, {\n            fontSize: 12,\n            fontWeight: \"400\",\n            paddingTop: 2,\n            paddingBottom: 2,\n            paddingLeft: 2,\n            paddingRight: 2,\n        });\n        this.tagImages = [...this.tagImages, image];\n        this.tagFrameByText.set(resolvedText, frame);\n        this.tagObjects.forEach((tagObject) => {\n            const builder = tagObject?.builder as any;\n            if (!builder) {\n                return;\n            }\n            const currentFrame = builder.getFrame?.() ?? 0;\n            builder.images = this.tagImages;\n            builder.atlas = undefined;\n            builder.initTexture?.();\n            builder.frameGeometries?.forEach((geometry: any) => geometry.dispose());\n            builder.frameGeometries?.clear?.();\n            if (builder.mesh) {\n                if (builder.mesh.material) {\n                    builder.mesh.material.map = builder.atlas?.getTexture?.();\n                    builder.mesh.material.needsUpdate = true;\n                }\n                builder.frameNo = -1;\n                builder.setFrame(Math.min(currentFrame, builder.frameCount - 1));\n            }\n        });\n        return frame;\n    }\n    updatePersistentTag(item: any, tagObject: any): void {\n        if (!tagObject) {\n            return;\n        }\n        if (!this.props.persistentHoverTags?.value) {\n            tagObject.setVisible(false);\n            return;\n        }\n        const tooltip = resolveSidebarItemTooltipText(item, this.props.sidebarModel, this.props.strings);\n        if (!tooltip) {\n            tagObject.setVisible(false);\n            return;\n        }\n        const frame = this.ensureTagFrame(tooltip);\n        tagObject.setFrame(frame);\n        tagObject.setVisible(true);\n    }\n    getItemIndexAtSlot(slotIndex: number): number {\n        return slotIndex + this.pagingOffset;\n    }\n    getCameoSize(): {\n        width: number;\n        height: number;\n    } {\n        return {\n            width: this.props.cameoImages.width,\n            height: this.props.cameoImages.height,\n        };\n    }\n    getSlotSize(): {\n        width: number;\n        height: number;\n    } {\n        return this.props.slotSize ?? this.getCameoSize();\n    }\n    createSlotOutline(): any {\n        const slotSize = this.getSlotSize();\n        const width = slotSize.width;\n        const height = slotSize.height;\n        const geometry = new THREE.BufferGeometry();\n        const positions = new Float32Array([\n            0, 0, 0,\n            0, height, 0,\n            width, height, 0,\n            width, 0, 0,\n            0, 0, 0,\n        ]);\n        geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n        const material = new THREE.LineBasicMaterial({\n            color: this.props.textColor,\n            transparent: true,\n            side: THREE.DoubleSide,\n        });\n        return new THREE.Line(geometry, material);\n    }\n    hide(): void {\n        this.visible = false;\n    }\n    show(): void {\n        this.visible = true;\n    }\n    scrollToOffset(offset: number): boolean {\n        const oldOffset = this.pagingOffset;\n        const maxOffset = Math.max(0, this.props.sidebarModel.activeTab.items.length - this.props.slots);\n        this.pagingOffset = clamp(offset, 0, maxOffset);\n        if (this.pagingOffset % 2) {\n            this.pagingOffset++;\n        }\n        this.updateSlots(this.props.sidebarModel.activeTab.items, this.props.slots);\n        return oldOffset !== this.pagingOffset;\n    }\n    pageDown(): boolean {\n        return this.scrollToOffset(this.pagingOffset + this.props.slots);\n    }\n    pageUp(): boolean {\n        return this.scrollToOffset(this.pagingOffset - this.props.slots);\n    }\n    createLabelImages(textColor: string): any[] {\n        const labels = [\n            { text: this.props.strings.get(\"TXT_READY\"), type: LabelType.Ready },\n            { text: this.props.strings.get(\"TXT_HOLD\"), type: LabelType.OnHold },\n        ];\n        return labels.map((label) => this.createTextBox(label.text, textColor));\n    }\n    createQuantityImages(textColor: string): any[] {\n        const style = { paddingRight: 2 };\n        const images = new Array(SidebarCard.MAX_QUANTITY)\n            .fill(0)\n            .map((_, index) => this.createTextBox(\"\" + (index + 1), textColor, style));\n        images.push(this.createTextBox(\"∞\", textColor, style));\n        return images;\n    }\n    createTextBox(text: string, color: string, additionalStyle?: any): any {\n        const style = {\n            color,\n            backgroundColor: \"rgba(0, 0, 0, .5)\",\n            fontFamily: \"'Fira Sans Condensed', Arial, sans-serif\",\n            fontSize: 12,\n            fontWeight: \"500\",\n            paddingTop: 5,\n            paddingBottom: 5,\n            paddingLeft: 2,\n            paddingRight: 4,\n            ...additionalStyle,\n        };\n        if (typeof text === \"string\" && text.includes(\"\\n\")) {\n            const lines = text.split(/\\r?\\n/);\n            const fontSize = Math.max(1, style.fontSize ?? 12);\n            const lineSpacing = 2;\n            const canvas = document.createElement(\"canvas\");\n            const alphaContext = canvas.getContext(\"2d\", {\n                alpha: !style.backgroundColor || !!style.backgroundColor.match(/^rgba/),\n            });\n            if (!alphaContext) {\n                throw new Error(\"Failed to create sidebar tag canvas context\");\n            }\n            alphaContext.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`;\n            let maxWidth = 0;\n            const capHeight = alphaContext.measureText(\"A\");\n            const lineHeight = Math.ceil(capHeight.actualBoundingBoxAscent + capHeight.actualBoundingBoxDescent || fontSize * 1.2);\n            for (const line of lines) {\n                const metrics = alphaContext.measureText(line);\n                maxWidth = Math.max(maxWidth, Math.ceil(Math.max(metrics.width, Math.abs(metrics.actualBoundingBoxLeft || 0) +\n                    Math.abs(metrics.actualBoundingBoxRight || 0))));\n            }\n            const paddingLeft = style.paddingLeft ?? 0;\n            const paddingRight = style.paddingRight ?? 0;\n            const paddingTop = style.paddingTop ?? 0;\n            const paddingBottom = style.paddingBottom ?? 0;\n            const textHeight = lines.length * lineHeight + Math.max(0, lines.length - 1) * lineSpacing;\n            canvas.width = Math.max(1, maxWidth + paddingLeft + paddingRight);\n            canvas.height = Math.max(1, textHeight + paddingTop + paddingBottom);\n            const context = canvas.getContext(\"2d\", {\n                alpha: !style.backgroundColor || !!style.backgroundColor.match(/^rgba/),\n            });\n            if (!context) {\n                throw new Error(\"Failed to create sidebar tag render context\");\n            }\n            if (style.backgroundColor) {\n                context.fillStyle = style.backgroundColor;\n                context.fillRect(0, 0, canvas.width, canvas.height);\n            }\n            context.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`;\n            context.fillStyle = style.color;\n            context.textAlign = \"center\";\n            context.textBaseline = \"top\";\n            const centerX = canvas.width / 2;\n            const topY = (canvas.height - textHeight) / 2;\n            const maxTextWidth = Math.max(1, canvas.width - paddingLeft - paddingRight);\n            for (let index = 0; index < lines.length; index += 1) {\n                context.fillText(lines[index], centerX + 0.5, topY + index * (lineHeight + lineSpacing) + 0.5, maxTextWidth);\n            }\n            return canvas;\n        }\n        return OverlayUtils.createTextBox(text, style);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/SidebarCredits.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { UiText } from \"@/gui/component/UiText\";\ntype SidebarModel = {\n    credits: number;\n    topTextLeftAlign: boolean;\n};\ntype SidebarCreditsProps = UiComponentProps & {\n    textColor: string;\n    width: number;\n    height: number;\n    zIndex?: number;\n    sidebarModel: SidebarModel;\n    onTick: (direction: \"up\" | \"down\") => void;\n};\nexport class SidebarCredits extends UiComponent<SidebarCreditsProps> {\n    text!: UiText;\n    targetCredits?: number;\n    renderedCredits?: number;\n    tickSpeed?: number;\n    lastUpdate?: number;\n    lastLeftAligned?: boolean;\n    createUiObject(): UiObject {\n        return new UiObject(new THREE.Object3D(), new HtmlContainer());\n    }\n    defineChildren() {\n        const { textColor, width, height, zIndex } = this.props;\n        return jsx(UiText, {\n            ref: (e: UiText) => (this.text = e),\n            value: \"\",\n            textColor,\n            width,\n            height,\n            zIndex,\n        });\n    }\n    onFrame(now: number) {\n        const { sidebarModel: { credits, topTextLeftAlign }, } = this.props;\n        if (this.targetCredits !== credits) {\n            this.targetCredits = credits;\n            const diff = Math.abs(credits - (this.renderedCredits ?? 0));\n            const t = Math.min(1, diff / 5000);\n            const duration = 300 + (2000 - 300) * t;\n            this.tickSpeed = diff / duration;\n        }\n        const tickSpeed = this.tickSpeed ?? 0;\n        if (!this.lastUpdate || now - this.lastUpdate >= 50) {\n            let delta = this.lastUpdate ? now - this.lastUpdate : 0;\n            this.lastUpdate = now;\n            if (this.renderedCredits !== credits) {\n                if (this.renderedCredits === undefined) {\n                    this.renderedCredits = 0;\n                }\n                else {\n                    let diff = credits - this.renderedCredits;\n                    let step = tickSpeed * delta;\n                    if (Math.abs(diff) >= step) {\n                        this.renderedCredits += Math.sign(diff) * step;\n                    }\n                    else {\n                        this.renderedCredits += diff;\n                    }\n                    this.props.onTick(Math.sign(diff) === 1 ? \"up\" : \"down\");\n                }\n                this.text.setValue(\"\" + Math.floor(this.renderedCredits!));\n            }\n            if (topTextLeftAlign !== this.lastLeftAligned) {\n                if (topTextLeftAlign) {\n                    this.text.setTextAlign(\"left\");\n                    this.text.getUiObject().setPosition(15, 0);\n                }\n                else {\n                    this.text.setTextAlign(\"center\");\n                    this.text.getUiObject().setPosition(0, 0);\n                }\n                this.lastLeftAligned = topTextLeftAlign;\n            }\n        }\n    }\n}\nexport default SidebarCredits;\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/SidebarGameTime.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { UiText } from \"@/gui/component/UiText\";\nimport { formatTimeDuration } from \"@/util/format\";\ntype SidebarModel = {\n    currentGameTime: number;\n    replayTime?: number;\n    topTextLeftAlign: boolean;\n};\ntype SidebarGameTimeProps = UiComponentProps & {\n    textColor: string;\n    width: number;\n    height: number;\n    zIndex?: number;\n    sidebarModel: SidebarModel;\n};\nexport class SidebarGameTime extends UiComponent<SidebarGameTimeProps> {\n    text!: UiText;\n    lastUpdate?: number;\n    lastGameTime?: number;\n    lastLeftAligned?: boolean;\n    createUiObject(): UiObject {\n        return new UiObject(new THREE.Object3D(), new HtmlContainer());\n    }\n    defineChildren() {\n        const { textColor, width, height, zIndex } = this.props;\n        return jsx(UiText, {\n            ref: (e: UiText) => (this.text = e),\n            value: \"\",\n            textColor,\n            width,\n            height,\n            zIndex,\n        });\n    }\n    onFrame(now: number) {\n        const { sidebarModel: { currentGameTime, replayTime, topTextLeftAlign }, } = this.props;\n        if (!this.lastUpdate || now - this.lastUpdate >= 50) {\n            this.lastUpdate = now;\n            if (this.lastGameTime !== currentGameTime) {\n                this.text.setValue(formatTimeDuration(currentGameTime) +\n                    (replayTime ? \" / \" + formatTimeDuration(replayTime) : \"\"));\n                this.lastGameTime = currentGameTime;\n            }\n            if (topTextLeftAlign !== this.lastLeftAligned) {\n                if (topTextLeftAlign) {\n                    this.text.setTextAlign(\"left\");\n                    this.text.getUiObject().setPosition(15, 0);\n                }\n                else {\n                    this.text.setTextAlign(\"center\");\n                    this.text.getUiObject().setPosition(0, 0);\n                }\n                this.lastLeftAligned = topTextLeftAlign;\n            }\n        }\n    }\n}\nexport default SidebarGameTime;\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/SidebarIconButton.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { UiObject } from \"@/gui/UiObject\";\nexport type SidebarIconButtonProps = UiComponentProps & {\n    image: any;\n    imageFrameOffset?: number;\n    palette?: any;\n    x?: number;\n    y?: number;\n    onClick?: () => void;\n    tooltip?: string;\n    toggle?: boolean;\n    disabled?: boolean;\n};\nexport class SidebarIconButton extends UiComponent<SidebarIconButtonProps> {\n    sprite: any;\n    toggle?: boolean;\n    disabled: boolean;\n    handleMouseDown: () => void;\n    onDocumentMouseUp: () => void;\n    constructor(props: SidebarIconButtonProps) {\n        super(props);\n        this.toggle = this.props.toggle;\n        this.disabled = !!this.props.disabled;\n        this.handleMouseDown = () => {\n            if (this.disabled)\n                return;\n            if (this.toggle === undefined) {\n                this.sprite.setFrame((this.props.imageFrameOffset ?? 0) + 1);\n            }\n            document.addEventListener(\"mouseup\", this.onDocumentMouseUp);\n            document.addEventListener(\"touchend\", this.onDocumentMouseUp);\n            document.addEventListener(\"touchcancel\", this.onDocumentMouseUp);\n        };\n        this.onDocumentMouseUp = () => {\n            if (this.toggle === undefined) {\n                this.sprite.setFrame((this.props.imageFrameOffset ?? 0) + 0);\n            }\n            document.removeEventListener(\"mouseup\", this.onDocumentMouseUp);\n            document.removeEventListener(\"touchend\", this.onDocumentMouseUp);\n            document.removeEventListener(\"touchcancel\", this.onDocumentMouseUp);\n        };\n    }\n    createUiObject(): UiObject {\n        return new UiObject(new THREE.Object3D());\n    }\n    defineChildren() {\n        const { image, imageFrameOffset, palette, x, y, onClick, tooltip, } = this.props;\n        return jsx(\"sprite\", {\n            image,\n            palette,\n            x,\n            y,\n            frame: this.getBaseFrameNo(imageFrameOffset ?? 0),\n            onClick: (e: any) => e.button === 0 && !this.disabled && onClick?.(),\n            onMouseDown: this.handleMouseDown,\n            tooltip,\n            ref: (e: any) => (this.sprite = e),\n        });\n    }\n    getBaseFrameNo(offset: number): number {\n        return offset + (this.disabled ? 2 : this.toggle ? 1 : 0);\n    }\n    setToggleState(toggle: boolean) {\n        if (this.toggle !== toggle) {\n            this.toggle = toggle;\n            this.sprite.setFrame(this.getBaseFrameNo(this.props.imageFrameOffset ?? 0));\n        }\n    }\n    setDisabled(disabled: boolean) {\n        if (disabled !== this.disabled) {\n            this.disabled = disabled;\n            this.sprite.setFrame(this.getBaseFrameNo(this.props.imageFrameOffset ?? 0));\n        }\n    }\n    onDispose() {\n        document.removeEventListener(\"mouseup\", this.onDocumentMouseUp);\n        document.removeEventListener(\"touchend\", this.onDocumentMouseUp);\n        document.removeEventListener(\"touchcancel\", this.onDocumentMouseUp);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/SidebarMenu.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx, createRef } from \"@/gui/jsx/jsx\";\nimport { MenuButton } from \"@/gui/component/MenuButton\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\ntype SidebarMenuButton = {\n    label: string;\n    disabled?: boolean;\n    isBottom?: boolean;\n    onClick?: () => void;\n};\ntype SidebarMenuProps = UiComponentProps & {\n    buttons: SidebarMenuButton[];\n    buttonImg: any;\n    buttonPal?: any;\n    menuHeight: number;\n};\nexport class SidebarMenu extends UiComponent<SidebarMenuProps> {\n    createUiObject(): UiObject {\n        return new UiObject(new THREE.Object3D(), new HtmlContainer());\n    }\n    defineChildren() {\n        return this.props.buttons.map((btn, idx) => this.createButton(btn, idx));\n    }\n    createButton(btn: SidebarMenuButton, idx: number) {\n        const img = this.props.buttonImg;\n        let pos = { x: 0, y: idx * img.height };\n        if (btn.isBottom) {\n            pos.y = this.props.menuHeight - img.height;\n        }\n        const box = { x: pos.x, y: pos.y, width: img.width, height: img.height };\n        const spriteRef = createRef<any>();\n        return jsx(\"fragment\", null, jsx(\"sprite\", {\n            image: img,\n            palette: this.props.buttonPal,\n            x: pos.x,\n            y: pos.y,\n            ref: spriteRef,\n        }), jsx(HtmlView, {\n            component: MenuButton,\n            props: {\n                buttonConfig: { label: btn.label, disabled: !!btn.disabled },\n                box: { x: box.x, y: box.y, width: box.width, height: box.height },\n                onMouseDown: (e: any) => {\n                    spriteRef.current.setFrame(1);\n                    const upHandler = () => {\n                        spriteRef.current.setFrame(0);\n                        document.removeEventListener(\"mouseup\", upHandler);\n                    };\n                    document.addEventListener(\"mouseup\", upHandler);\n                },\n                onClick: (e: any) => {\n                    btn.onClick && btn.onClick();\n                },\n            },\n        }));\n    }\n}\nexport default SidebarMenu;\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/SidebarPower.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { IndexedBitmap } from \"@/data/Bitmap\";\nimport { SpriteUtils } from \"@/engine/gfx/SpriteUtils\";\nimport { clamp } from \"@/util/math\";\nimport { TextureUtils } from \"@/engine/gfx/TextureUtils\";\nimport { HighlightAnimRunner } from \"@/engine/renderable/entity/HighlightAnimRunner\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\nimport { findReverse } from \"@/util/array\";\nimport { PaletteBasicMaterial } from \"@/engine/gfx/material/PaletteBasicMaterial\";\ntype SidebarPowerModel = {\n    powerDrained: number;\n    powerGenerated: number;\n};\ntype SidebarPowerProps = UiComponentProps & {\n    x?: number;\n    y?: number;\n    zIndex?: number;\n    height: number;\n    powerImg: any;\n    palette: any;\n    strings: any;\n    sidebarModel: SidebarPowerModel;\n};\ntype PipCount = {\n    red: number;\n    yellow: number;\n    green: number;\n};\nenum PipType {\n    None = 0,\n    Green = 1,\n    Yellow = 2,\n    Red = 3,\n    Highlight = 4\n}\nfunction pipCountEquals(a: PipCount, b: PipCount): boolean {\n    return a.green === b.green && a.yellow === b.yellow && a.red === b.red;\n}\nexport class SidebarPower extends UiComponent<SidebarPowerProps> {\n    visible: boolean = true;\n    declare pipHighlightAnimRunner: HighlightAnimRunner;\n    declare pips: IndexedBitmap[];\n    declare textureBitmap: IndexedBitmap;\n    declare texture: THREE.DataTexture;\n    declare mesh: THREE.Mesh;\n    declare meshEvtTarget: any;\n    declare pipCount?: PipCount;\n    declare targetPipCount: PipCount;\n    declare lastPipUpdate?: number;\n    declare lastPowerDrained?: number;\n    declare lastPowerGenerated?: number;\n    constructor(props: SidebarPowerProps) {\n        super(props);\n        this.pipHighlightAnimRunner = new HighlightAnimRunner(new BoxedVar(1), 1, 2, 15);\n    }\n    createUiObject(): UiObject {\n        const obj = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        obj.setPosition(this.props.x || 0, this.props.y || 0);\n        this.pips = this.createPips(this.props.powerImg);\n        const width = this.props.powerImg.width;\n        const height = this.props.height;\n        this.textureBitmap = new IndexedBitmap(width, height);\n        this.texture = this.createDataTexture(this.textureBitmap.data, width, height);\n        this.mesh = this.createMesh(width, height);\n        return obj;\n    }\n    createPips(powerImg: any): IndexedBitmap[] {\n        const arr: IndexedBitmap[] = [];\n        if (!powerImg || typeof powerImg.numImages !== \"number\")\n            return arr;\n        for (let i = 0; i < powerImg.numImages; i++) {\n            const img = powerImg.getImage(i);\n            if (!img)\n                continue;\n            arr.push(new IndexedBitmap(img.width, img.height, img.imageData));\n        }\n        return arr;\n    }\n    createDataTexture(data: Uint8Array, width: number, height: number): THREE.DataTexture {\n        const tex = new THREE.DataTexture(data, width, height, THREE.RedFormat);\n        tex.needsUpdate = true;\n        tex.minFilter = THREE.NearestFilter;\n        tex.magFilter = THREE.NearestFilter;\n        (tex as THREE.Texture & {\n            colorSpace: THREE.ColorSpace;\n        }).colorSpace = THREE.NoColorSpace;\n        return tex;\n    }\n    createMesh(width: number, height: number): THREE.Mesh {\n        const geometry = SpriteUtils.createRectGeometry(width, height);\n        SpriteUtils.addRectUvs(geometry, { x: 0, y: 0, width, height }, { width, height });\n        geometry.translate(width / 2, height / 2, 0);\n        const material = new PaletteBasicMaterial({\n            map: this.texture,\n            palette: TextureUtils.textureFromPalette(this.props.palette),\n            side: THREE.DoubleSide,\n            useRedIndex: true,\n        } as any);\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.frustumCulled = false;\n        return mesh;\n    }\n    defineChildren() {\n        return jsx(\"mesh\", {\n            zIndex: this.props.zIndex,\n            ref: (e: any) => (this.meshEvtTarget = e),\n            onClick: () => { },\n        }, this.mesh);\n    }\n    onFrame(now: number) {\n        const obj = this.getUiObject().get3DObject();\n        obj.visible = this.visible;\n        const { powerDrained, powerGenerated } = this.props.sidebarModel;\n        let changed = false;\n        if (this.lastPowerDrained !== powerDrained ||\n            this.lastPowerGenerated !== powerGenerated) {\n            this.lastPowerDrained = powerDrained;\n            this.lastPowerGenerated = powerGenerated;\n            this.meshEvtTarget.setTooltip(this.props.strings.get(\"TXT_POWER_DRAIN\", powerGenerated, powerDrained));\n            const c = Math.max(powerGenerated, powerDrained);\n            const a = c ? Math.min(1, powerDrained / c) : 1;\n            const l = c\n                ? Math.min(1, clamp(powerGenerated - powerDrained, 0, 100) / c)\n                : 0;\n            const hasPips = Array.isArray(this.pips) && this.pips.length > 0;\n            const pipHeight = hasPips ? this.pips[0].height + 1 : 1;\n            const n = c\n                ? this.computeHeightFromPowerLevel(Math.max(100, c))\n                : 1;\n            this.targetPipCount = {\n                green: Math.floor(((1 - a - l) * n) / pipHeight),\n                yellow: Math.floor((l * n) / pipHeight),\n                red: c ? Math.floor((a * n) / pipHeight) : 1,\n            };\n            this.pipHighlightAnimRunner.animation.stop();\n            changed = true;\n        }\n        const target = this.targetPipCount;\n        const pipCountUnchanged = this.pipCount && pipCountEquals(this.pipCount, target);\n        if (!this.lastPipUpdate ||\n            now - this.lastPipUpdate >= 50 ||\n            !pipCountUnchanged) {\n            this.lastPipUpdate = now;\n            if (this.pipCount) {\n                const dRed = Math.sign(target.red - this.pipCount.red);\n                const dYellow = Math.sign(target.yellow - this.pipCount.yellow);\n                const dGreen = Math.sign(target.green - this.pipCount.green);\n                if (dRed) {\n                    if (dRed > 0) {\n                        if (this.pipCount.yellow > dRed) {\n                            this.pipCount.yellow = Math.max(0, this.pipCount.yellow - dRed);\n                        }\n                        else {\n                            this.pipCount.green = Math.max(0, this.pipCount.green - dRed);\n                        }\n                    }\n                }\n                else {\n                    if (dYellow) {\n                        if (dYellow > 0) {\n                            this.pipCount.green = Math.max(0, this.pipCount.green - dYellow);\n                        }\n                    }\n                    else {\n                        this.pipCount.green += dGreen;\n                    }\n                    this.pipCount.yellow += dYellow;\n                }\n                this.pipCount.red += dRed;\n            }\n            else {\n                this.pipCount = { red: 1, yellow: 0, green: 0 };\n            }\n            this.updateTexture(this.pipCount, true);\n            if (pipCountEquals(this.pipCount, target)) {\n                this.pipHighlightAnimRunner.animate(10);\n            }\n        }\n        if (pipCountUnchanged) {\n            if (changed)\n                this.pipHighlightAnimRunner.animate(10);\n            if (this.pipHighlightAnimRunner.shouldUpdate()) {\n                const prev = !!this.pipHighlightAnimRunner.getValue();\n                this.pipHighlightAnimRunner.tick(now);\n                const curr = !!this.pipHighlightAnimRunner.getValue();\n                if (curr !== prev) {\n                    this.updateTexture(this.pipCount!, curr);\n                }\n            }\n        }\n    }\n    computeHeightFromPowerLevel(power: number): number {\n        return (clamp((Math.log10((power / 100 + 5) / 5e7) / (power / 100 + 3) + 2) / 2, 0, 1) * this.props.height);\n    }\n    updateTexture(pipCount: PipCount, highlight: boolean) {\n        if (!this.pips || this.pips.length === 0) {\n            return;\n        }\n        const pipHeight = this.pips[0].height;\n        const totalHeight = this.props.height;\n        const pipStep = pipHeight + 1;\n        let layers: [\n            number,\n            number,\n            number\n        ][][] = [\n            [[PipType.None, Math.floor(totalHeight / pipHeight), pipHeight]],\n            [\n                [PipType.Red, pipCount.red, pipStep],\n                [PipType.Yellow, pipCount.yellow, pipStep],\n                [PipType.Green, pipCount.green, pipStep],\n            ],\n        ];\n        if (highlight) {\n            const last = findReverse(layers[1], ([, count]) => count > 0);\n            if (last)\n                last[1]--;\n            layers[1].push([PipType.Highlight, 1, pipStep]);\n        }\n        for (const layer of layers) {\n            let y = totalHeight - pipHeight;\n            for (const [type, count, step] of layer) {\n                const pip = this.pips[type];\n                for (let i = 0; i < count; i++) {\n                    this.textureBitmap.drawIndexedImage(pip, 0, y);\n                    y -= step;\n                }\n            }\n        }\n        this.texture.needsUpdate = true;\n    }\n    hide() {\n        this.visible = false;\n    }\n    show() {\n        this.visible = true;\n    }\n    onDispose() {\n        this.mesh.geometry.dispose();\n        (this.mesh.material as any).dispose();\n        this.texture.dispose();\n    }\n}\nexport default SidebarPower;\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/SidebarRadar.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { SidebarRadarAnimationRunner } from \"@/gui/screen/game/component/hud/SidebarRadarAnimRunner\";\ntype SidebarRadarProps = UiComponentProps & {\n    x?: number;\n    y?: number;\n    zIndex?: number;\n    image: any;\n    palette: any;\n    sidebarModel?: {\n        radarEnabled?: boolean;\n    };\n};\nexport class SidebarRadar extends UiComponent<SidebarRadarProps> {\n    visible: boolean = true;\n    cover!: any;\n    minimapContainer!: any;\n    minimap?: any;\n    coverOpen?: boolean;\n    createUiObject() {\n        const obj = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        obj.setPosition(this.props.x || 0, this.props.y || 0);\n        return obj;\n    }\n    defineChildren() {\n        return jsx(\"fragment\", null, jsx(\"sprite\", {\n            image: this.props.image,\n            palette: this.props.palette,\n            zIndex: this.props.zIndex,\n            ref: (e: any) => (this.cover = e),\n            animationRunner: new SidebarRadarAnimationRunner(this.props.image),\n        }), jsx(\"container\", {\n            ref: (e: any) => (this.minimapContainer = e),\n            hidden: true,\n            x: 13,\n        }));\n    }\n    onFrame(now: number) {\n        const obj = this.getUiObject().get3DObject();\n        obj.visible = this.visible;\n        const sidebarModel = this.props.sidebarModel;\n        const radarEnabled = sidebarModel?.radarEnabled ?? true;\n        if (radarEnabled !== this.coverOpen) {\n            this.toggleCover(radarEnabled, this.coverOpen === undefined);\n            this.coverOpen = radarEnabled;\n        }\n        const runner = this.cover.getAnimationRunner();\n        if (runner.isStopped()) {\n            this.minimapContainer.setVisible(this.coverOpen);\n        }\n    }\n    toggleCover(open: boolean, instant: boolean = false) {\n        const runner = this.cover.getAnimationRunner();\n        if (open) {\n            runner.radarOn(instant);\n        }\n        else {\n            runner.radarOff(instant);\n        }\n        this.minimapContainer.setVisible(!!instant && open);\n    }\n    setMinimap(minimap: any) {\n        if (this.minimap) {\n            this.minimapContainer.remove(this.minimap);\n        }\n        this.minimap = minimap;\n        if (minimap) {\n            minimap.setFitSize(this.getMinimapAvailSpace());\n            this.minimapContainer.add(minimap);\n            minimap.setZIndex((this.props.zIndex || 0) + 1);\n        }\n    }\n    getMinimapAvailSpace() {\n        return {\n            width: this.props.image.width - 13 - 15,\n            height: this.props.image.height,\n        };\n    }\n    hide() {\n        this.visible = false;\n    }\n    show() {\n        this.visible = true;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/SidebarRadarAnimRunner.ts",
    "content": "import { IniSection } from \"@/data/IniSection\";\nimport { Animation, AnimationState } from \"@/engine/Animation\";\nimport { AnimProps } from \"@/engine/AnimProps\";\nimport { Engine } from \"@/engine/Engine\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\nexport enum AnimationType {\n    None = 0,\n    RadarOff = 1,\n    RadarOn = 2\n}\nexport class SidebarRadarAnimationRunner {\n    shpFile: any;\n    closed: boolean;\n    currentAnimationType: AnimationType;\n    animation?: Animation;\n    constructor(shpFile: any) {\n        this.shpFile = shpFile;\n        this.closed = true;\n        this.currentAnimationType = AnimationType.None;\n    }\n    radarOff(skipInit: boolean = false) {\n        this.currentAnimationType = AnimationType.RadarOff;\n        if (!skipInit) {\n            this.initAnimation();\n        }\n    }\n    radarOn(skipInit: boolean = false) {\n        this.currentAnimationType = AnimationType.RadarOn;\n        if (!skipInit) {\n            this.initAnimation();\n        }\n    }\n    initAnimation() {\n        const ini = new IniSection(\"\");\n        const props = new AnimProps(ini, this.shpFile);\n        const anim = new Animation(props, new BoxedVar(Engine.UI_ANIM_SPEED));\n        this.animation = anim;\n    }\n    tick(now: number) {\n        const anim = this.animation;\n        const type = this.currentAnimationType;\n        if (anim && type !== AnimationType.None) {\n            switch (anim.getState()) {\n                case AnimationState.STOPPED:\n                    break;\n                case AnimationState.NOT_STARTED:\n                    anim.start(now);\n                // falls through\n                case AnimationState.RUNNING:\n                default:\n                    anim.update(now);\n            }\n            if (anim.getState() === AnimationState.STOPPED) {\n                this.closed = type === AnimationType.RadarOff;\n                this.currentAnimationType = AnimationType.None;\n            }\n        }\n    }\n    shouldUpdate(): boolean {\n        return true;\n    }\n    isStopped(): boolean {\n        return this.currentAnimationType === AnimationType.None;\n    }\n    getCurrentFrame(): number {\n        if (!this.animation) {\n            return this.currentAnimationType === AnimationType.RadarOn\n                ? this.shpFile.numImages - 1\n                : 0;\n        }\n        let dir = this.currentAnimationType === AnimationType.RadarOff ? -1 : 1;\n        if (this.currentAnimationType === AnimationType.None && this.closed) {\n            dir *= -1;\n        }\n        let base = 0;\n        if (dir === -1) {\n            base = this.animation.props.end;\n        }\n        return base + dir * this.animation.getCurrentFrame();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/SidebarTabs.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\ntype TabImage = {\n    width: number;\n    height: number;\n};\ntype Tab = {\n    disabled: boolean;\n    flashing?: boolean;\n};\ntype SidebarTabsProps = UiComponentProps & {\n    aggregatedImageData: {\n        file: any;\n        imageIndexes: Map<TabImage, number>;\n    };\n    images: TabImage[];\n    palette: any;\n    tabSpacing: number;\n    onTabClick?: (tab: Tab) => void;\n    sidebarModel: {\n        tabs: Tab[];\n        activeTab: Tab;\n    };\n    strings: {\n        get: (key: string) => string;\n    };\n};\nexport class SidebarTabs extends UiComponent<SidebarTabsProps> {\n    tabObjects: any[] = [];\n    flashing: boolean = false;\n    lastFlashUpdate?: number;\n    constructor(props: SidebarTabsProps) {\n        super(props);\n        this.tabObjects = [];\n        this.flashing = false;\n    }\n    createUiObject() {\n        const obj = new UiObject(new THREE.Object3D());\n        obj.setPosition(this.props.x || 0, this.props.y || 0);\n        return obj;\n    }\n    defineChildren() {\n        const { aggregatedImageData, images, palette, tabSpacing, onTabClick, sidebarModel, strings, } = this.props;\n        const children = [];\n        for (let c = 0; c < 4; c++) {\n            const img = images[c];\n            const frameIndex = aggregatedImageData.imageIndexes.get(img);\n            if (frameIndex === undefined) {\n                throw new Error(`Tab ${c} image not found in aggregated file`);\n            }\n            children.push(jsx(\"sprite\", {\n                image: aggregatedImageData.file,\n                palette: palette,\n                x: (tabSpacing + img.width) * c,\n                tooltip: strings.get(\"Tip:Tab\" + (c + 1)),\n                onClick: (e: MouseEvent) => {\n                    if (e.button === 0) {\n                        const tab = sidebarModel.tabs[c];\n                        if (!tab.disabled) {\n                            onTabClick?.(tab);\n                        }\n                    }\n                },\n                onFrame: (now: number, sprite: any) => this.handleFrame(now, sprite, sidebarModel.tabs[c], frameIndex),\n            }));\n        }\n        return children;\n    }\n    handleFrame(now: number, sprite: {\n        setFrame?: (frame: number) => void;\n        get3DObject?: () => any;\n    }, tab: Tab, baseFrame: number) {\n        if (!this.lastFlashUpdate || now - this.lastFlashUpdate >= 250) {\n            this.lastFlashUpdate = now;\n            this.flashing = !this.flashing;\n        }\n        let state: number;\n        if (tab.disabled) {\n            state = 2;\n        }\n        else if (this.props.sidebarModel.activeTab === tab) {\n            state = 1;\n        }\n        else {\n            state = 0;\n        }\n        if (tab.flashing && this.flashing) {\n            state = 3;\n        }\n        if (sprite && typeof sprite.setFrame === 'function') {\n            sprite.setFrame(baseFrame + state);\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/SuperWeaponTimers.ts",
    "content": "import * as THREE from \"three\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { UiComponent, UiComponentProps } from \"@/gui/jsx/UiComponent\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { SpriteUtils } from \"@/engine/gfx/SpriteUtils\";\nimport { CanvasUtils } from \"@/engine/gfx/CanvasUtils\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nimport { formatTimeDuration } from \"@/util/format\";\ntype Player = {\n    defeated: boolean;\n    color: {\n        asHexString: () => string;\n    };\n    superWeaponsTrait?: {\n        getAll: () => Array<{\n            getTimerSeconds: () => number;\n            rules: {\n                showTimer: boolean;\n                uiName: string;\n            };\n        }>;\n    };\n    powerTrait?: {\n        getBlackoutDuration: () => number;\n    };\n};\ntype CountdownTimer = {\n    isRunning: () => boolean;\n    getSeconds: () => number;\n    text?: string;\n};\ntype StalemateDetectTrait = {\n    isStale: () => boolean;\n    getCountdownTicks: () => number;\n};\ntype SuperWeaponTimersProps = UiComponentProps & {\n    x?: number;\n    y?: number;\n    zIndex?: number;\n    width: number;\n    height: number;\n    players: Player[];\n    localPlayer?: Player;\n    countdownTimer: CountdownTimer;\n    stalemateDetectTrait?: StalemateDetectTrait;\n    strings: {\n        get: (key: string) => string;\n    };\n};\ntype TimerLine = {\n    text: string;\n    color: string;\n    flash: boolean;\n};\nexport class SuperWeaponTimers extends UiComponent<SuperWeaponTimersProps> {\n    declare ctx: CanvasRenderingContext2D;\n    declare texture: THREE.Texture;\n    declare mesh: THREE.Mesh;\n    lastUpdate?: number;\n    lastHasTimers?: boolean;\n    createUiObject() {\n        const obj = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        obj.setPosition(this.props.x || 0, this.props.y || 0);\n        const { width, height } = this.props;\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = width;\n        canvas.height = height;\n        this.ctx = canvas.getContext(\"2d\", { alpha: true })!;\n        this.texture = this.createTexture(canvas);\n        this.mesh = this.createMesh(width, height);\n        return obj;\n    }\n    createTexture(canvas: HTMLCanvasElement) {\n        const texture = new THREE.Texture(canvas);\n        texture.needsUpdate = true;\n        texture.flipY = false;\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        return texture;\n    }\n    createMesh(width: number, height: number) {\n        const geometry = SpriteUtils.createRectGeometry(width, height);\n        SpriteUtils.addRectUvs(geometry, { x: 0, y: 0, width, height }, { width, height });\n        geometry.translate(width / 2, height / 2, 0);\n        const material = new THREE.MeshBasicMaterial({\n            map: this.texture,\n            side: THREE.DoubleSide,\n            transparent: true,\n        });\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.frustumCulled = false;\n        return mesh;\n    }\n    defineChildren() {\n        return jsx(\"mesh\", { zIndex: this.props.zIndex }, this.mesh);\n    }\n    onFrame(now: number) {\n        if (!this.lastUpdate || now - this.lastUpdate >= 100) {\n            this.lastUpdate = now;\n            const lines: TimerLine[] = [];\n            if (this.props.stalemateDetectTrait?.isStale()) {\n                let seconds = Math.floor(this.props.stalemateDetectTrait.getCountdownTicks() /\n                    GameSpeed.BASE_TICKS_PER_SECOND);\n                const text = this.props.strings.get(\"TS:StalemateTimer\") +\n                    \"   \" +\n                    formatTimeDuration(seconds, true);\n                lines.push({ text, color: \"red\", flash: true });\n            }\n            const countdown = this.props.countdownTimer;\n            if (countdown.isRunning()) {\n                let seconds = countdown.getSeconds();\n                const text = (countdown.text !== undefined\n                    ? this.props.strings.get(countdown.text) + \"   \"\n                    : \"\") + formatTimeDuration(seconds, true);\n                lines.push({\n                    text,\n                    color: this.props.localPlayer?.color.asHexString() ?? \"white\",\n                    flash: false,\n                });\n            }\n            for (const player of this.props.players) {\n                if (!player.defeated) {\n                    const superWeapons = player.superWeaponsTrait?.getAll();\n                    const blackoutSeconds = (player.powerTrait?.getBlackoutDuration() ?? 0) /\n                        GameSpeed.BASE_TICKS_PER_SECOND;\n                    if ((superWeapons && superWeapons.length) || blackoutSeconds) {\n                        const color = player.color.asHexString();\n                        const timers: {\n                            seconds: number;\n                            label: string;\n                        }[] = [];\n                        if (superWeapons) {\n                            for (const sw of superWeapons) {\n                                if (sw.rules.showTimer) {\n                                    timers.push({\n                                        seconds: sw.getTimerSeconds(),\n                                        label: this.props.strings.get(sw.rules.uiName),\n                                    });\n                                }\n                            }\n                        }\n                        if (blackoutSeconds) {\n                            timers.push({\n                                seconds: blackoutSeconds,\n                                label: this.props.strings.get(\"MSG:BlackoutTimer\"),\n                            });\n                        }\n                        for (const { seconds, label } of timers) {\n                            const sec = Math.floor(seconds);\n                            const text = label + \"   \" + formatTimeDuration(sec, true);\n                            lines.push({ text, color, flash: sec === 0 });\n                        }\n                    }\n                }\n            }\n            const hasTimers = !!lines.length;\n            if (hasTimers !== this.lastHasTimers || hasTimers) {\n                this.lastHasTimers = hasTimers;\n                this.ctx.clearRect(0, 0, this.props.width, this.props.height);\n                let y = this.props.height - 20;\n                for (const { text, color, flash } of lines) {\n                    let drawColor = color;\n                    if (flash) {\n                        drawColor = Math.floor(now / 1000) % 2 ? color : \"orange\";\n                    }\n                    y -= this.drawLine(text, drawColor, y);\n                }\n                this.texture.needsUpdate = true;\n            }\n        }\n    }\n    drawLine(text: string, color: string, y: number): number {\n        return CanvasUtils.drawText(this.ctx, text, 0, y, {\n            color,\n            fontFamily: \"'Fira Sans Condensed', Arial, sans-serif\",\n            fontSize: 12,\n            fontWeight: \"500\",\n            paddingTop: 6,\n            height: 20,\n            backgroundColor: \"rgba(0, 0, 0, .75)\",\n            textAlign: \"right\",\n            paddingLeft: 4,\n            paddingRight: 4,\n        }).height;\n    }\n    onDispose() {\n        this.mesh.geometry.dispose();\n        (this.mesh.material as THREE.Material).dispose();\n        this.texture.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/commandBar/CommandBarButtonList.ts",
    "content": "import { CommandBarButtonType } from \"./CommandBarButtonType\";\nexport class CommandBarButtonList {\n    buttons: CommandBarButtonType[] = [];\n    fromIni(iniSection: {\n        getString: (key: string) => string | undefined;\n    }): this {\n        const buttonListStr = iniSection.getString(\"ButtonList\") ?? \"\";\n        const buttonNames = buttonListStr.split(\",\").map(s => s.trim()).filter(Boolean);\n        const validButtonNames = new Set(Object.keys(CommandBarButtonType).filter(key => isNaN(Number(key))));\n        const result: CommandBarButtonType[] = [];\n        for (const name of buttonNames) {\n            if (name === \"x\") {\n                result.push(CommandBarButtonType.Separator);\n            }\n            else if (validButtonNames.has(name)) {\n                const buttonType = CommandBarButtonType[name as keyof typeof CommandBarButtonType];\n                if (typeof buttonType === \"number\") {\n                    result.push(buttonType);\n                }\n            }\n            else {\n                console.warn(`Unknown command bar button type \"${name}\"`);\n            }\n        }\n        this.buttons = result;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/commandBar/CommandBarButtonType.ts",
    "content": "export enum CommandBarButtonType {\n    Separator = 0,\n    BugReport = 1,\n    Beacon = 2,\n    Cheer = 3,\n    Deploy = 4,\n    Guard = 5,\n    PlanningMode = 6,\n    Stop = 7,\n    Team01 = 8,\n    Team02 = 9,\n    Team03 = 10,\n    TypeSelect = 11,\n    ReplayRewind = 12,\n    ReplayPlay = 13,\n    ReplayPause = 14,\n    ReplaySpeed = 15\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/commandBar/CommandButtonConfig.ts",
    "content": "export {};\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/commandBar/commandButtonConfigs.ts",
    "content": "import { CommandBarButtonType } from \"./CommandBarButtonType\";\nexport interface CommandButtonConfig {\n    type: CommandBarButtonType;\n    icon: string;\n    tooltip: (e: {\n        get: (key: string) => string;\n    }) => string;\n}\nexport const commandButtonConfigs: CommandButtonConfig[] = [\n    {\n        type: CommandBarButtonType.BugReport,\n        icon: \"reportbug.shp\",\n        tooltip: (e) => e.get(\"ts:reportbug\"),\n    },\n    {\n        type: CommandBarButtonType.Team01,\n        icon: \"button00.shp\",\n        tooltip: (e) => e.get(\"tip:team01\"),\n    },\n    {\n        type: CommandBarButtonType.Team02,\n        icon: \"button01.shp\",\n        tooltip: (e) => e.get(\"tip:team02\"),\n    },\n    {\n        type: CommandBarButtonType.Team03,\n        icon: \"button02.shp\",\n        tooltip: (e) => e.get(\"tip:team03\"),\n    },\n    {\n        type: CommandBarButtonType.TypeSelect,\n        icon: \"button03.shp\",\n        tooltip: (e) => e.get(\"tip:typeselect\"),\n    },\n    {\n        type: CommandBarButtonType.Deploy,\n        icon: \"button04.shp\",\n        tooltip: (e) => e.get(\"tip:deploy\"),\n    },\n    {\n        type: CommandBarButtonType.Guard,\n        icon: \"button06.shp\",\n        tooltip: (e) => e.get(\"tip:guard\"),\n    },\n    {\n        type: CommandBarButtonType.Beacon,\n        icon: \"button07.shp\",\n        tooltip: (e) => e.get(\"tip:beacon\"),\n    },\n    {\n        type: CommandBarButtonType.Stop,\n        icon: \"button08.shp\",\n        tooltip: (e) => e.get(\"tip:stop\"),\n    },\n    {\n        type: CommandBarButtonType.PlanningMode,\n        icon: \"button09.shp\",\n        tooltip: (e) => e.get(\"tip:planningmode\"),\n    },\n    {\n        type: CommandBarButtonType.Cheer,\n        icon: \"button10.shp\",\n        tooltip: (e) => e.get(\"tip:cheer\"),\n    },\n    {\n        type: CommandBarButtonType.ReplayRewind,\n        icon: \"rewind.shp\",\n        tooltip: (e) => e.get(\"tip:replayrewind\"),\n    },\n    {\n        type: CommandBarButtonType.ReplayPlay,\n        icon: \"play.shp\",\n        tooltip: (e) => e.get(\"tip:play\"),\n    },\n    {\n        type: CommandBarButtonType.ReplayPause,\n        icon: \"pause.shp\",\n        tooltip: (e) => e.get(\"tip:pause\"),\n    },\n    {\n        type: CommandBarButtonType.ReplaySpeed,\n        icon: \"ffwd.shp\",\n        tooltip: (e) => e.get(\"tip:replayspeed\"),\n    },\n];\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/viewmodel/CombatantSidebarModel.ts",
    "content": "import { FactoryType } from \"@/game/rules/TechnoRules\";\nimport { ProductionQueue, QueueType, QueueStatus } from \"@/game/player/production/ProductionQueue\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { DockTrait } from \"@/game/gameobject/trait/DockTrait\";\nimport { SidebarModel, SidebarItemTargetType, SidebarItemStatus, SidebarCategory } from \"./SidebarModel\";\nimport { SuperWeapon, SuperWeaponStatus } from \"@/game/SuperWeapon\";\ntype SidebarTechnoItem = {\n    target: {\n        type: SidebarItemTargetType.Techno;\n        rules: any;\n    };\n    cameo: any;\n    disabled: boolean;\n    progress: number;\n    quantity: number;\n    status: SidebarItemStatus;\n};\ntype SidebarSpecialItem = {\n    target: {\n        type: SidebarItemTargetType.Special;\n        rules: any;\n    };\n    cameo: any;\n    disabled: boolean;\n    progress: number;\n    quantity: number;\n    status: SidebarItemStatus;\n};\nconst superWeaponStatusToSidebarStatus = new Map<SuperWeaponStatus, SidebarItemStatus>()\n    .set(SuperWeaponStatus.Charging, SidebarItemStatus.Started)\n    .set(SuperWeaponStatus.Paused, SidebarItemStatus.OnHold)\n    .set(SuperWeaponStatus.Ready, SidebarItemStatus.Ready);\nexport class CombatantSidebarModel extends SidebarModel {\n    player: any;\n    rules: any;\n    get credits(): number {\n        return Math.floor(this.player.credits);\n    }\n    get radarEnabled(): boolean {\n        return !!this.player.radarTrait && !this.player.radarTrait.isDisabled();\n    }\n    constructor(player: any, game: any) {\n        super(game);\n        this.player = player;\n        this.rules = game.rules;\n    }\n    computePurchaseCost(rules: any): number {\n        return this.game.sellTrait.computePurchaseValue(rules, this.player);\n    }\n    updateAvailableObjects(game: any) {\n        if (!this.player.production)\n            throw new Error(\"Player is not a combatant\");\n        const availableObjects = this.sortAvailableObjects(this.player.production.getAvailableObjects());\n        for (const tab of this.tabs) {\n            tab.items.length = 0;\n            tab.needsUpdate = true;\n        }\n        this.updateSuperWeaponItems();\n        for (const obj of availableObjects) {\n            const objRules = game.getObject(obj.name, obj.type);\n            const tab = this.tabs[this.getSidebarCategoryForQueueType(this.player.production.getQueueTypeForObject(obj))];\n            const queue = this.player.production.getQueueForObject(obj);\n            const factoryType = this.player.production.getFactoryTypeForQueueType(queue.type);\n            const item: SidebarTechnoItem = {\n                target: {\n                    type: SidebarItemTargetType.Techno,\n                    rules: obj,\n                },\n                cameo: this.player.production.hasVeteranType(factoryType) && obj.trainable\n                    ? objRules.altCameo\n                    : objRules.cameo,\n                disabled: false,\n                progress: 0,\n                quantity: 0,\n                status: SidebarItemStatus.Idle,\n            };\n            tab.items.push(item);\n            this.updateSidebarTechnoItem(item, queue, this.player.production);\n        }\n        for (const tab of this.tabs) {\n            this.updateTabFlashing(tab);\n        }\n        this.updateActiveTab();\n    }\n    updateActiveTab() {\n        if (this.activeTab.items.length !== 0)\n            return;\n        const found = this.tabs.find((tab) => tab.items.length > 0)?.id;\n        if (found !== undefined) {\n            this.selectTab(found);\n        }\n    }\n    updateFromQueue(queue: any) {\n        if (!this.player.production)\n            throw new Error(\"Player is not a combatant\");\n        const tab = this.tabs[this.getSidebarCategoryForQueueType(queue.type)];\n        tab.needsUpdate = true;\n        for (const item of tab.items) {\n            if (item.target.type === SidebarItemTargetType.Techno &&\n                this.player.production.getQueueForObject(item.target.rules) === queue) {\n                this.updateSidebarTechnoItem(item, queue, this.player.production);\n            }\n        }\n        this.updateTabFlashing(tab);\n    }\n    updateSuperWeapons() {\n        this.updateSuperWeaponItems();\n        this.updateActiveTab();\n    }\n    updateSuperWeaponItems() {\n        const superWeapons = this.player.superWeaponsTrait\n            ?.getAll()\n            .slice()\n            .sort((a: any, b: any) => 1000 * (a.rules.rechargeTime - b.rules.rechargeTime) +\n            a.name.charCodeAt(0) -\n            b.name.charCodeAt(0));\n        const tab = this.tabs[SidebarCategory.Armory];\n        tab.needsUpdate = true;\n        let firstTechnoIdx = tab.items.findIndex((item: any) => item.target.type === SidebarItemTargetType.Techno);\n        if (firstTechnoIdx !== -1) {\n            tab.items.splice(0, firstTechnoIdx);\n        }\n        else {\n            tab.items.length = 0;\n        }\n        const items = superWeapons?.map((sw: any) => {\n            const status = superWeaponStatusToSidebarStatus.get(sw.status);\n            if (status === undefined) {\n                throw new Error(`Unhandled super weapon status \"${sw.status}\"`);\n            }\n            const item: SidebarSpecialItem = {\n                target: {\n                    type: SidebarItemTargetType.Special,\n                    rules: sw.rules,\n                },\n                cameo: sw.rules.sidebarImage,\n                disabled: false,\n                progress: sw.getChargeProgress(),\n                quantity: 1,\n                status,\n            };\n            return item;\n        }) ?? [];\n        if (items.length) {\n            tab.items.unshift(...items);\n        }\n        this.updateTabFlashing(tab);\n    }\n    updateTabFlashing(tab: any) {\n        tab.flashing = tab.items.some((item: any) => item.status === SidebarItemStatus.Ready);\n    }\n    updateSidebarTechnoItem(item: SidebarTechnoItem, queue: any, production: any) {\n        if ((item.target.type as any) === SidebarItemTargetType.Special) {\n            throw new Error(\"Sidebar item must be of type Techno\");\n        }\n        const rules = item.target.rules;\n        const buildings = [...this.player.buildings];\n        let buildLimitReached = false;\n        if (Number.isFinite(rules.buildLimit)) {\n            let builtCount: number;\n            if (rules.buildLimit >= 0) {\n                builtCount = (rules.type === ObjectType.Building\n                    ? buildings\n                    : this.player.getOwnedObjectsByType(rules.type, true)).filter((o: any) => o.name === rules.name).length;\n            }\n            else {\n                builtCount = this.player.getLimitedUnitsBuilt(rules.name);\n            }\n            buildLimitReached = builtCount >= Math.abs(rules.buildLimit);\n        }\n        if (this.rules.general.padAircraft.includes(rules.name)) {\n            const totalPads = buildings\n                .filter((b: any) => b.factoryTrait?.type === FactoryType.AircraftType &&\n                b.helipadTrait)\n                .reduce((sum: number, b: any) => sum + (b.traits.find(DockTrait)?.numberOfDocks ?? 0), 0);\n            const ownedAircraft = [\n                ...this.player.getOwnedObjectsByType(ObjectType.Aircraft, true),\n            ].filter((o: any) => this.rules.general.padAircraft.includes(o.name)).length;\n            buildLimitReached = buildLimitReached || ownedAircraft >= totalPads;\n        }\n        const factoryType = production.getFactoryTypeForQueueType(queue.type);\n        const availableFactories = buildings.filter((b: any) => b.factoryTrait?.type === factoryType && !b.warpedOutTrait.isActive());\n        const found = queue.find(rules);\n        item.progress = found.length ? found[0].progress : 0;\n        item.quantity = found.reduce((sum: number, q: any) => sum + q.quantity, 0);\n        item.status = this.computeStatus(queue, found[0]);\n        item.disabled =\n            (queue.maxSize === 1 && found[0] !== queue.getFirst()) ||\n                buildLimitReached ||\n                (!availableFactories.length &&\n                    (!queue.currentSize || found[0] !== queue.getFirst()));\n    }\n    getTabForQueueType(type: QueueType) {\n        return this.tabs[this.getSidebarCategoryForQueueType(type)];\n    }\n    getSidebarCategoryForQueueType(type: QueueType): SidebarCategory {\n        switch (type) {\n            case QueueType.Structures:\n                return SidebarCategory.Structures;\n            case QueueType.Armory:\n                return SidebarCategory.Armory;\n            case QueueType.Infantry:\n                return SidebarCategory.Infantry;\n            case QueueType.Vehicles:\n            case QueueType.Ships:\n            case QueueType.Aircrafts:\n                return SidebarCategory.Vehicles;\n            default:\n                throw new Error(\"Unhandled queueType \" + QueueType[type]);\n        }\n    }\n    computeStatus(queue: any, first: any): SidebarItemStatus {\n        if (!first)\n            return SidebarItemStatus.Idle;\n        if (queue.getFirst() === first) {\n            if (queue.status === QueueStatus.Ready)\n                return SidebarItemStatus.Ready;\n            if (queue.status === QueueStatus.OnHold)\n                return SidebarItemStatus.OnHold;\n            return SidebarItemStatus.Started;\n        }\n        return SidebarItemStatus.InQueue;\n    }\n    sortAvailableObjects(objects: any[]): any[] {\n        return [...objects].sort((a, b) => {\n            const aVal = this.getObjectTypeSortValue(a);\n            const bVal = this.getObjectTypeSortValue(b);\n            if (aVal === bVal) {\n                if (a.aiBasePlanningSide === b.aiBasePlanningSide) {\n                    if (a.techLevel === b.techLevel) {\n                        return a.prerequisite.length < b.prerequisite.length ? -1 : 1;\n                    }\n                    return a.techLevel < b.techLevel ? -1 : 1;\n                }\n                return (a.aiBasePlanningSide ?? -1) < (b.aiBasePlanningSide ?? -1)\n                    ? -1\n                    : 1;\n            }\n            return aVal - bVal;\n        });\n    }\n    getObjectTypeSortValue(obj: any): number {\n        if (obj.type === ObjectType.Aircraft)\n            return 1;\n        if (obj.type === ObjectType.Vehicle) {\n            if (obj.naval)\n                return 2;\n            if (obj.consideredAircraft)\n                return 1;\n            return 0;\n        }\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/viewmodel/MessageList.ts",
    "content": "import { EventDispatcher } from \"@/util/event\";\nexport interface Message {\n    text: string;\n    color: string;\n    time: number;\n    animate: boolean;\n    durationSeconds?: number;\n}\nexport class MessageList {\n    messageDurationSeconds: number;\n    maxMessages: number;\n    localPlayer: any;\n    isComposing: boolean;\n    messages: Message[];\n    private _onNewMessage: EventDispatcher<[\n        MessageList,\n        Message\n    ]>;\n    constructor(messageDurationSeconds: number, maxMessages: number, localPlayer: any) {\n        this.messageDurationSeconds = messageDurationSeconds;\n        this.maxMessages = maxMessages;\n        this.localPlayer = localPlayer;\n        this.isComposing = false;\n        this.messages = [];\n        this._onNewMessage = new EventDispatcher();\n    }\n    get onNewMessage() {\n        return this._onNewMessage.asEvent();\n    }\n    addUiFeedbackMessage(text: string) {\n        const msg: Message = {\n            text,\n            color: this.localPlayer?.color.asHexString() ?? \"grey\",\n            time: Date.now(),\n            animate: false,\n        };\n        this.messages.push(msg);\n        this._onNewMessage.dispatch(this as any, msg);\n    }\n    addSystemMessage(text: string, colorOrPlayer: string | {\n        color: {\n            asHexString: () => string;\n        };\n    }, durationSeconds?: number) {\n        const color = typeof colorOrPlayer === \"string\"\n            ? colorOrPlayer\n            : colorOrPlayer.color.asHexString();\n        const msg: Message = {\n            text,\n            color,\n            time: Date.now(),\n            animate: true,\n            durationSeconds,\n        };\n        this.messages.push(msg);\n        this._onNewMessage.dispatch(this as any, msg);\n    }\n    addChatMessage(text: string, color: string) {\n        const msg: Message = {\n            text,\n            color,\n            time: Date.now(),\n            animate: true,\n        };\n        this.messages.push(msg);\n        this._onNewMessage.dispatch(this as any, msg);\n    }\n    prune() {\n        const now = Date.now();\n        this.messages = this.messages.filter((msg) => msg.time >=\n            now - 1000 * (msg.durationSeconds ?? this.messageDurationSeconds));\n        this.messages.splice(0, this.messages.length - this.maxMessages);\n    }\n    getAll(): Message[] {\n        return this.messages;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/viewmodel/SidebarModel.ts",
    "content": "import { SidebarTab } from \"./SidebarTab\";\nimport { GameSpeed } from \"@/game/GameSpeed\";\nexport enum SidebarItemTargetType {\n    Techno = 0,\n    Special = 1\n}\nexport enum SidebarItemStatus {\n    Idle = 0,\n    InQueue = 1,\n    Started = 2,\n    OnHold = 3,\n    Ready = 4\n}\nexport enum SidebarCategory {\n    Structures = 0,\n    Armory = 1,\n    Infantry = 2,\n    Vehicles = 3\n}\nexport class SidebarModel {\n    game: any;\n    replay: any;\n    powerDrained: number = 0;\n    powerGenerated: number = 0;\n    sellMode: boolean = false;\n    repairMode: boolean = false;\n    topTextLeftAlign: boolean = false;\n    tabs: SidebarTab[];\n    activeTabId: SidebarCategory;\n    constructor(game: any, replay?: any) {\n        this.game = game;\n        this.replay = replay;\n        this.powerDrained = 0;\n        this.powerGenerated = 0;\n        this.sellMode = false;\n        this.repairMode = false;\n        this.topTextLeftAlign = false;\n        this.tabs = [\n            new SidebarTab(SidebarCategory.Structures),\n            new SidebarTab(SidebarCategory.Armory),\n            new SidebarTab(SidebarCategory.Infantry),\n            new SidebarTab(SidebarCategory.Vehicles),\n        ];\n        this.activeTabId = SidebarCategory.Structures;\n    }\n    get activeTab(): SidebarTab {\n        return this.tabs[this.activeTabId];\n    }\n    get currentGameTime(): number {\n        return Math.floor(this.game.currentTime / 1000);\n    }\n    get replayTime(): number | undefined {\n        return this.replay\n            ? Math.floor(this.replay.endTick / GameSpeed.BASE_TICKS_PER_SECOND)\n            : undefined;\n    }\n    selectTab(tabId: SidebarCategory) {\n        if (!this.tabs[tabId].disabled) {\n            this.activeTabId = tabId;\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/component/hud/viewmodel/SidebarTab.ts",
    "content": "export class SidebarTab {\n    items: any[] = [];\n    needsUpdate: boolean = true;\n    flashing: boolean = false;\n    id: number;\n    constructor(id: number) {\n        this.id = id;\n    }\n    get disabled(): boolean {\n        return this.items.length === 0;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/gameMenu/ConInfoForm.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { CountryIcon } from '@/gui/component/CountryIcon';\nimport { OBS_COUNTRY_NAME } from '@/game/gameopts/constants';\nimport { RECIPIENT_ALL, RECIPIENT_TEAM } from '@/network/gservConfig';\nimport { Chat } from '@/gui/component/Chat';\ninterface Color {\n    asHexString(): string;\n}\ninterface Country {\n    name: string;\n}\ninterface Player {\n    name: string;\n    color: Color;\n    country?: Country;\n    isAi: boolean;\n}\ninterface ConInfo {\n    name: string;\n    status: string;\n    ping?: number;\n    lagAllowanceMillis?: number;\n}\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface ChatHistory {\n    lastComposeTarget?: {\n        value: {\n            type: any;\n            name: string;\n        };\n    };\n}\ninterface ConInfoFormProps {\n    strings: Strings;\n    conInfos?: ConInfo[];\n    players: Player[];\n    localPlayer: Player;\n    messages: any[];\n    chatHistory?: ChatHistory;\n    onSendMessage: (message: string) => void;\n}\nconst TURN_TIMEOUT_MILLIS = 60000;\nconst LAG_STATE_THRESH_MILLIS = 5000;\nconst CON_INFO_THRESH_MILLIS = 3000;\nconst PlayerConnectionStatus = {\n    Connected: 'Connected',\n    Disconnected: 'Disconnected',\n    Lagging: 'Lagging'\n};\nexport const ConInfoForm: React.FC<ConInfoFormProps> = ({ strings, conInfos, players, localPlayer, messages, chatHistory, onSendMessage, }) => {\n    const [timeRemaining, setTimeRemaining] = useState(() => Math.floor((TURN_TIMEOUT_MILLIS -\n        LAG_STATE_THRESH_MILLIS -\n        CON_INFO_THRESH_MILLIS) /\n        1000));\n    useEffect(() => {\n        const interval = setInterval(() => setTimeRemaining(Math.max(0, timeRemaining - 1)), 1000);\n        return () => clearInterval(interval);\n    }, [timeRemaining]);\n    return (<div className=\"con-info-form\">\n      <div className=\"con-info-form-content\">\n        <table>\n          <thead>\n            <tr>\n              <th></th>\n              <th className=\"player-name\">\n                {strings.get(\"GUI:Player\")}\n              </th>\n              <th className=\"player-ping\">\n                {strings.get(\"GUI:Ping\")}\n              </th>\n              <th className=\"player-time\">\n                {strings.get(\"GUI:Time\")}\n              </th>\n            </tr>\n          </thead>\n          <tbody>\n            {players\n            .filter((player) => !player.isAi)\n            .map((player) => {\n            const conInfo = conInfos?.find((info) => info.name === player.name);\n            return (<tr key={player.name} style={{\n                    color: player.color.asHexString(),\n                    opacity: conInfo &&\n                        conInfo.status !== PlayerConnectionStatus.Connected\n                        ? 0.5\n                        : 1,\n                }}>\n                    <td>\n                      <CountryIcon country={player.country\n                    ? player.country.name\n                    : OBS_COUNTRY_NAME}/>\n                    </td>\n                    <td className=\"player-name\">\n                      {player.name}\n                    </td>\n                    <td className=\"player-ping\">\n                      <meter value={conInfo?.ping ?? 1000} max={1000} low={150} high={500} optimum={0}/>\n                    </td>\n                    <td className=\"player-time\">\n                      {conInfo\n                    ? Math.floor((conInfo.lagAllowanceMillis ?? 0) / 1000)\n                    : undefined}\n                    </td>\n                  </tr>);\n        })}\n          </tbody>\n        </table>\n      </div>\n      <div className=\"con-info-form-footer\">\n        <div className=\"time-allowed\">\n          {strings.get(\"TXT_TIME_ALLOWED\", timeRemaining)}\n        </div>\n        <div className=\"chat\">\n          <Chat strings={strings} messages={messages} channels={[RECIPIENT_ALL, RECIPIENT_TEAM] as any} chatHistory={chatHistory} userColors={new Map(players.map((player) => [player.name, player.color.asHexString()]))} localUsername={localPlayer.name} onSendMessage={onSendMessage as any} onCancelMessage={undefined as any}/>\n        </div>\n      </div>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/game/gameMenu/ConnectionInfoScreen.ts",
    "content": "import { jsx } from '@/gui/jsx/jsx';\nimport { ScreenType } from '@/gui/screen/game/gameMenu/ScreenType';\nimport { HtmlView } from '@/gui/jsx/HtmlView';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { ConInfoForm } from '@/gui/screen/game/gameMenu/ConInfoForm';\nimport { GameMenuScreen } from '@/gui/screen/game/GameMenuScreen';\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface Player {\n    name: string;\n    color: {\n        asHexString(): string;\n    };\n    country?: {\n        name: string;\n    };\n    isAi: boolean;\n}\ninterface ChatMessage {\n    text?: string;\n    value?: string;\n    recipient?: any;\n}\ninterface ChatHistory {\n    onNewMessage: {\n        subscribe(handler: (message: ChatMessage) => void): void;\n        unsubscribe(handler: (message: ChatMessage) => void): void;\n    };\n    lastComposeTarget?: {\n        value: {\n            type: any;\n            name: string;\n        };\n    };\n}\ninterface GservConnection {\n    isOpen(): boolean;\n    onLoadInfo: {\n        subscribe(handler: (data: any) => void): void;\n        unsubscribe(handler: (data: any) => void): void;\n    };\n    requestLoadInfo(): void;\n}\ninterface ChatNetHandler {\n    submitMessage(message: string, recipient: any): void;\n}\ninterface ConnectionInfoParams {\n    players: Player[];\n    localPlayer: Player;\n    chatHistory: ChatHistory;\n    gservCon: GservConnection;\n    chatNetHandler: ChatNetHandler;\n    onQuit: () => void;\n}\ninterface SidebarButton {\n    label: string;\n    onClick: () => void;\n}\ninterface GameMenuController {\n    toggleContentAreaVisibility(visible: boolean): void;\n    setSidebarButtons(buttons: SidebarButton[]): void;\n    showSidebarButtons(): void;\n    hideSidebarButtons(): void;\n    setMainComponent(component: any): void;\n    pushScreen?(screenType: any, params: any): void;\n    popScreen?(): void;\n}\ninterface JsxRenderer {\n    render(element: any): any[];\n}\ninterface FormRef {\n    refresh(): void;\n    applyOptions(updater: (options: any) => void): void;\n}\nclass LoadInfoParser {\n    parse(data: any): any {\n        return data;\n    }\n}\nexport class ConnectionInfoScreen extends GameMenuScreen {\n    private strings: Strings;\n    private jsxRenderer: JsxRenderer;\n    private messages: ChatMessage[] = [];\n    private disposables = new CompositeDisposable();\n    private params?: ConnectionInfoParams;\n    private form?: FormRef;\n    declare controller?: GameMenuController;\n    constructor(strings: Strings, jsxRenderer: JsxRenderer) {\n        super();\n        this.strings = strings;\n        this.jsxRenderer = jsxRenderer;\n        this.messages = [];\n        this.disposables = new CompositeDisposable();\n    }\n    private handleChatMessage = (message: ChatMessage): void => {\n        this.messages.push(message);\n        this.form?.refresh();\n    };\n    private handleConInfoUpdate = (data: any): void => {\n        this.form?.applyOptions((options: any) => {\n            options.conInfos = new LoadInfoParser().parse(data);\n        });\n    };\n    onEnter(params: ConnectionInfoParams): void {\n        this.params = params;\n        this.controller?.toggleContentAreaVisibility(true);\n        this.initView(params);\n        if (params.gservCon.isOpen()) {\n            params.gservCon.onLoadInfo.subscribe(this.handleConInfoUpdate);\n            this.disposables.add(() => params.gservCon.onLoadInfo.unsubscribe(this.handleConInfoUpdate));\n            params.gservCon.requestLoadInfo();\n            const interval = setInterval(() => {\n                if (params.gservCon.isOpen()) {\n                    params.gservCon.requestLoadInfo();\n                }\n                else {\n                    this.disposables.dispose();\n                }\n            }, 1000);\n            this.disposables.add(() => clearInterval(interval));\n            params.chatHistory.onNewMessage.subscribe(this.handleChatMessage);\n            this.disposables.add(() => {\n                this.messages.length = 0;\n                params.chatHistory.onNewMessage.unsubscribe(this.handleChatMessage);\n            });\n        }\n        this.messages.push({\n            text: this.strings.get(\"GUI:ConnectingToPlayers\") +\n                \"...\\n\" +\n                this.strings.get(\"TXT_RECONNECT_HELP2\") +\n                \" \" +\n                this.strings.get(\"TXT_RECONNECT_HELP2B\"),\n        });\n    }\n    private initView(params: ConnectionInfoParams): void {\n        const strings = this.strings;\n        const buttons: SidebarButton[] = [\n            {\n                label: strings.get(\"GUI:AbortMission\"),\n                onClick: () => {\n                    this.controller?.pushScreen?.(ScreenType.QuitConfirm, {\n                        onQuit: params.onQuit,\n                        onCancel: () => {\n                            this.controller?.popScreen?.();\n                        },\n                    });\n                },\n            },\n        ];\n        this.controller?.setSidebarButtons(buttons);\n        this.controller?.showSidebarButtons();\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            width: \"100%\",\n            height: \"100%\",\n            component: ConInfoForm,\n            innerRef: (form: FormRef) => (this.form = form),\n            props: {\n                players: params.players,\n                localPlayer: params.localPlayer,\n                strings: this.strings,\n                messages: this.messages,\n                chatHistory: params.chatHistory,\n                onSendMessage: (message: ChatMessage) => {\n                    if (message.value && message.recipient) {\n                        params.chatNetHandler.submitMessage(message.value, message.recipient);\n                    }\n                },\n            },\n        }));\n        this.controller?.setMainComponent(component);\n        this.disposables.add(() => (this.form = undefined));\n    }\n    async onLeave(): Promise<void> {\n        this.params = undefined;\n        this.controller?.hideSidebarButtons();\n        this.controller?.toggleContentAreaVisibility(false);\n        this.disposables.dispose();\n    }\n    async onStack(): Promise<void> {\n        this.controller?.hideSidebarButtons();\n    }\n    onUnstack(): void {\n        if (this.params) {\n            this.initView(this.params);\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/gameMenu/DiploForm.tsx",
    "content": "import React from 'react';\nimport { AllianceStatus } from '@/game/Alliances';\nimport { OBS_COUNTRY_NAME, aiUiNames } from '@/game/gameopts/constants';\nimport { CountryIcon } from '@/gui/component/CountryIcon';\nimport { Chat } from '@/gui/component/Chat';\nimport { RECIPIENT_ALL, RECIPIENT_TEAM } from '@/network/gservConfig';\nimport { PingIndicator } from '@/gui/component/PingIndicator';\ninterface Color {\n    asHexString(): string;\n}\ninterface Country {\n    name: string;\n}\ninterface Player {\n    name: string;\n    color: Color;\n    country?: Country;\n    isAi: boolean;\n    isObserver?: boolean;\n    defeated: boolean;\n    aiDifficulty?: string;\n    getUnitsKilled(): number;\n    isCombatant(): boolean;\n}\ninterface Alliance {\n    status: AllianceStatus;\n    players: {\n        first: Player;\n        second: Player;\n    };\n}\ninterface PlayerInfo {\n    player: Player;\n    alliance?: Alliance;\n    allianceToggleable: boolean;\n    muted: boolean;\n}\ninterface ConInfo {\n    name: string;\n    status: string;\n    ping?: number;\n}\ninterface GameMode {\n    label: string;\n}\ninterface GameModes {\n    getById(id: string): GameMode;\n}\ninterface GameOptions {\n    gameMode: string;\n    shortGame: boolean;\n    cratesAppear: boolean;\n    superWeapons: boolean;\n    destroyableBridges: boolean;\n    multiEngineer: boolean;\n    noDogEngiKills: boolean;\n}\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface ChatHistory {\n    lastComposeTarget?: {\n        value: {\n            type: any;\n            name: string;\n        };\n    };\n}\ninterface DiploFormProps {\n    strings: Strings;\n    playerInfos: PlayerInfo[];\n    localPlayer?: Player;\n    taunts?: boolean;\n    singlePlayer: boolean;\n    alliancesAllowed: boolean;\n    gameModes: GameModes;\n    gameOpts: GameOptions;\n    mapName: string;\n    messages?: any[];\n    chatHistory?: ChatHistory;\n    conInfos?: ConInfo[];\n    onToggleTaunts: (enabled: boolean) => void;\n    onToggleAlliance: (player: Player, enabled: boolean) => void;\n    onToggleChat: (player: Player, enabled: boolean) => void;\n    onSendMessage: (message: string) => void;\n    onCancelMessage: () => void;\n}\nconst PlayerConnectionStatus = {\n    Connected: 'Connected',\n    Disconnected: 'Disconnected',\n    Lagging: 'Lagging'\n};\nexport const DiploForm: React.FC<DiploFormProps> = ({ strings, playerInfos, localPlayer, taunts, singlePlayer, alliancesAllowed, gameModes, gameOpts, mapName, messages, chatHistory, conInfos, onToggleTaunts, onToggleAlliance, onToggleChat, onSendMessage, onCancelMessage, }) => {\n    const gameTypeLabel = strings.get(gameModes.getById(gameOpts.gameMode).label);\n    const formatBoolean = (value: boolean): string => value ? strings.get(\"TXT_ON\") : strings.get(\"TXT_OFF\");\n    const localPlayerPing = conInfos?.find((info) => info.name === localPlayer?.name)?.ping;\n    return (<div className=\"diplo-form\">\n      <div className=\"players\">\n        <table>\n          <thead>\n            <tr>\n              <th className=\"player-country\"></th>\n              <th className=\"player-ping\"></th>\n              <th className=\"player-name\">{strings.get(\"GUI:Player\")}</th>\n              <th>{strings.get(\"GUI:Allies\")}</th>\n              {!singlePlayer && <th>{strings.get(\"GUI:Chat\")}</th>}\n              <th>{strings.get(\"GUI:Kills\")}</th>\n            </tr>\n          </thead>\n          <tbody>\n            {localPlayer && (<tr style={{\n                color: localPlayer.defeated\n                    ? \"grey\"\n                    : localPlayer.color.asHexString(),\n            }}>\n                <td className=\"player-country\">\n                  <CountryIcon country={localPlayer.country\n                ? localPlayer.country.name\n                : OBS_COUNTRY_NAME}/>\n                </td>\n                <td className=\"player-ping\">\n                  {localPlayerPing !== undefined && (<PingIndicator ping={localPlayerPing} strings={strings}/>)}\n                </td>\n                <td className=\"player-name\">{localPlayer.name}</td>\n                <td></td>\n                {!singlePlayer && <td></td>}\n                <td>\n                  {!localPlayer.isObserver || localPlayer.defeated\n                ? localPlayer.getUnitsKilled()\n                : undefined}\n                </td>\n              </tr>)}\n            {playerInfos.map((playerInfo, index) => {\n            const conInfo = conInfos?.find((info) => info.name === playerInfo.player.name);\n            const ping = conInfo?.status === PlayerConnectionStatus.Connected\n                ? conInfo.ping\n                : undefined;\n            return (<tr key={index} style={{\n                    color: playerInfo.player.defeated\n                        ? \"grey\"\n                        : playerInfo.player.color.asHexString(),\n                }}>\n                  <td className=\"player-country\">\n                    <CountryIcon country={playerInfo.player.country\n                    ? playerInfo.player.country.name\n                    : OBS_COUNTRY_NAME}/>\n                  </td>\n                  <td className=\"player-ping\">\n                    {ping !== undefined && (<PingIndicator ping={ping} strings={strings}/>)}\n                  </td>\n                  <td className=\"player-name\">\n                    {playerInfo.player.isAi\n                    ? strings.get(aiUiNames.get(playerInfo.player.aiDifficulty as any) || '')\n                    : playerInfo.player.name}\n                  </td>\n                  <td>\n                    {(!localPlayer?.isObserver || localPlayer.defeated) && (<input type=\"checkbox\" name=\"alliance\" className={playerInfo.alliance?.status === AllianceStatus.Requested\n                        ? playerInfo.alliance.players.first === localPlayer\n                            ? \"semi-checked-left\"\n                            : \"semi-checked-right\"\n                        : undefined} disabled={!alliancesAllowed ||\n                        !playerInfo.allianceToggleable ||\n                        !playerInfo.player.isCombatant()} checked={playerInfo.alliance?.status === AllianceStatus.Formed} onChange={() => onToggleAlliance(playerInfo.player, !(playerInfo.alliance?.status === AllianceStatus.Formed ||\n                        (playerInfo.alliance?.status === AllianceStatus.Requested &&\n                            playerInfo.alliance.players.first === localPlayer)))}/>)}\n                  </td>\n                  {!singlePlayer && (<td>\n                      {!playerInfo.player.isAi && (<input type=\"checkbox\" name=\"mute\" checked={!playerInfo.muted} onChange={(e) => onToggleChat(playerInfo.player, e.target.checked)}/>)}\n                    </td>)}\n                  <td>\n                    {!playerInfo.player.isObserver || playerInfo.player.defeated\n                    ? playerInfo.player.getUnitsKilled()\n                    : undefined}\n                  </td>\n                </tr>);\n        })}\n          </tbody>\n        </table>\n      </div>\n      <div className=\"diplo-form-footer\">\n        <div className=\"game-settings\">\n          <div>{strings.get(\"TXT_MAP\", mapName)}</div>\n          <div>\n            {[\n            `${strings.get(\"GUI:GameType\")}: ${gameTypeLabel}`,\n            `${strings.get(\"GUI:ShortGame\")}: ${formatBoolean(gameOpts.shortGame)}`,\n            `${strings.get(\"GUI:CratesAppear\")}: ${formatBoolean(gameOpts.cratesAppear)}`,\n            `${strings.get(\"GUI:SuperWeaponsAllowed\")}: ${formatBoolean(gameOpts.superWeapons)}`,\n            `${strings.get(\"GUI:DestroyableBridges\")}: ${formatBoolean(gameOpts.destroyableBridges)}`,\n            `${strings.get(\"GUI:MultiEngineer\")}: ${formatBoolean(gameOpts.multiEngineer)}`,\n            `${strings.get(\"GUI:NoDogEngiKills\")}: ${formatBoolean(gameOpts.noDogEngiKills)}`,\n        ].join(\", \")}\n          </div>\n        </div>\n        {!singlePlayer && (<div data-r-tooltip={strings.get(\"STT:TauntsOn\")}>\n            <label>\n              <input type=\"checkbox\" name=\"taunts\" checked={!!taunts} disabled={taunts === undefined} onChange={(e) => onToggleTaunts(e.target.checked)}/>\n              {\" \"}\n              <span>{strings.get(\"GUI:TauntsOn\")}</span>\n            </label>\n          </div>)}\n        {!singlePlayer && messages && chatHistory && localPlayer && (<div className=\"chat\">\n            <Chat localUsername={localPlayer.name} messages={messages} chatHistory={chatHistory} channels={[RECIPIENT_ALL, RECIPIENT_TEAM]} strings={strings} userColors={new Map([localPlayer, ...playerInfos.map((info) => info.player)].map((player) => [\n                player.name,\n                player.color.asHexString(),\n            ]))} onSendMessage={onSendMessage} onCancelMessage={onCancelMessage}/>\n          </div>)}\n      </div>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/game/gameMenu/DiploScreen.ts",
    "content": "import { jsx } from '@/gui/jsx/jsx';\nimport { HtmlView } from '@/gui/jsx/HtmlView';\nimport { DiploForm } from '@/gui/screen/game/gameMenu/DiploForm';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { GameMenuScreen } from '@/gui/screen/game/GameMenuScreen';\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface Player {\n    name: string;\n    color: {\n        asHexString(): string;\n    };\n    country?: {\n        name: string;\n    };\n    isAi: boolean;\n    isObserver?: boolean;\n    defeated: boolean;\n    aiDifficulty?: string;\n    getUnitsKilled(): number;\n    isCombatant(): boolean;\n}\ninterface Alliance {\n    status: any;\n    players: {\n        first: Player;\n        second: Player;\n    };\n}\ninterface Alliances {\n    filterByPlayer(player: Player): Alliance[];\n    canRequestAlliance(player: Player): boolean;\n    canFormAlliance(player1: Player, player2: Player): boolean;\n}\ninterface Game {\n    gameOpts: {\n        mapTitle: string;\n        gameMode: string;\n        shortGame: boolean;\n        cratesAppear: boolean;\n        superWeapons: boolean;\n        destroyableBridges: boolean;\n        multiEngineer: boolean;\n        noDogEngiKills: boolean;\n    };\n    rules: {\n        mpDialogSettings: {\n            alliesAllowed: boolean;\n            allyChangeAllowed: boolean;\n        };\n    };\n    alliances: Alliances;\n    getNonNeutralPlayers(): Player[];\n}\ninterface ChatHistory {\n    getAll(): any[];\n    onNewMessage: {\n        subscribe(handler: () => void): void;\n        unsubscribe(handler: () => void): void;\n    };\n}\ninterface GservConnection {\n    isOpen(): boolean;\n    onLoadInfo: {\n        subscribe(handler: (data: any) => void): void;\n        unsubscribe(handler: (data: any) => void): void;\n    };\n    requestLoadInfo(): void;\n}\ninterface DiploScreenParams {\n    localPlayer: Player;\n    isSinglePlayer: boolean;\n    game: Game;\n    chatHistory?: ChatHistory;\n    gservCon?: GservConnection;\n    onCancel: () => void;\n    onToggleAlliance: (player: Player, enabled: boolean) => void;\n    onSendMessage: (message: string) => void;\n}\ninterface SidebarButton {\n    label: string;\n    isBottom?: boolean;\n    onClick: () => void;\n}\ninterface GameMenuController {\n    toggleContentAreaVisibility(visible: boolean): void;\n    setSidebarButtons(buttons: SidebarButton[]): void;\n    showSidebarButtons(): void;\n    hideSidebarButtons(): void;\n    setMainComponent(component: any): void;\n}\ninterface JsxRenderer {\n    render(element: any): any[];\n}\ninterface Renderer {\n    onFrame: {\n        subscribe(handler: (time: number) => void): void;\n        unsubscribe(handler: (time: number) => void): void;\n    };\n}\ninterface GameModes {\n    getById(id: string): {\n        label: string;\n    };\n}\ninterface TauntsRef {\n    value?: boolean;\n}\ninterface FormRef {\n    applyOptions(updater: (options: any) => void): void;\n}\ninterface PlayerInfo {\n    player: Player;\n    muted: boolean;\n    allianceToggleable: boolean;\n    alliance?: Alliance;\n}\nclass LoadInfoParser {\n    parse(data: any): any {\n        return data;\n    }\n}\nexport class DiploScreen extends GameMenuScreen {\n    private strings: Strings;\n    private jsxRenderer: JsxRenderer;\n    private renderer: Renderer;\n    private gameModes: GameModes;\n    private taunts: TauntsRef;\n    private mutedPlayers: Set<string>;\n    private disposables = new CompositeDisposable();\n    private params?: DiploScreenParams;\n    private form?: FormRef;\n    private lastUpdate?: number;\n    declare controller?: GameMenuController;\n    constructor(strings: Strings, jsxRenderer: JsxRenderer, renderer: Renderer, gameModes: GameModes, taunts: TauntsRef, mutedPlayers: Set<string>) {\n        super();\n        this.strings = strings;\n        this.jsxRenderer = jsxRenderer;\n        this.renderer = renderer;\n        this.gameModes = gameModes;\n        this.taunts = taunts;\n        this.mutedPlayers = mutedPlayers;\n        this.disposables = new CompositeDisposable();\n    }\n    private onFrame = (time: number): void => {\n        if (!this.lastUpdate || time - this.lastUpdate > 500) {\n            this.lastUpdate = time;\n            this.updateForm();\n        }\n    };\n    private handleConInfoUpdate = (data: any): void => {\n        this.form?.applyOptions((options: any) => {\n            options.conInfos = new LoadInfoParser().parse(data);\n        });\n    };\n    private updateForm = (): void => {\n        this.form?.applyOptions((options: any) => {\n            if (this.params) {\n                options.playerInfos = this.buildPlayerInfos(this.params.game, this.params.localPlayer);\n                options.taunts = this.taunts.value;\n                options.messages = this.params.chatHistory?.getAll();\n            }\n        });\n    };\n    onEnter(params: DiploScreenParams): void {\n        this.controller?.toggleContentAreaVisibility(true);\n        this.initView(params);\n        this.params = params;\n        this.renderer.onFrame.subscribe(this.onFrame);\n        this.disposables.add(() => this.renderer.onFrame.unsubscribe(this.onFrame));\n        const chatHistory = params.chatHistory;\n        if (chatHistory) {\n            chatHistory.onNewMessage.subscribe(this.updateForm);\n            this.disposables.add(() => chatHistory.onNewMessage.unsubscribe(this.updateForm));\n        }\n        const gservCon = params.gservCon;\n        if (gservCon?.isOpen()) {\n            gservCon.onLoadInfo.subscribe(this.handleConInfoUpdate);\n            this.disposables.add(() => gservCon.onLoadInfo.unsubscribe(this.handleConInfoUpdate));\n            gservCon.requestLoadInfo();\n            const interval = setInterval(() => {\n                if (gservCon.isOpen()) {\n                    gservCon.requestLoadInfo();\n                }\n                else {\n                    this.disposables.dispose();\n                }\n            }, 10000);\n            this.disposables.add(() => clearInterval(interval));\n        }\n    }\n    private initView(params: DiploScreenParams): void {\n        const strings = this.strings;\n        const buttons: SidebarButton[] = [\n            {\n                label: strings.get(\"GUI:ResumeMission\"),\n                isBottom: true,\n                onClick: params.onCancel,\n            },\n        ];\n        this.controller?.setSidebarButtons(buttons);\n        this.controller?.showSidebarButtons();\n        const { localPlayer, isSinglePlayer, game } = params;\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            width: \"100%\",\n            height: \"100%\",\n            component: DiploForm,\n            innerRef: (form: FormRef) => (this.form = form),\n            props: {\n                playerInfos: this.buildPlayerInfos(game, localPlayer),\n                localPlayer: localPlayer,\n                gameOpts: game.gameOpts,\n                gameModes: this.gameModes,\n                taunts: isSinglePlayer ? undefined : this.taunts.value,\n                singlePlayer: isSinglePlayer,\n                alliancesAllowed: !isSinglePlayer &&\n                    game.rules.mpDialogSettings.alliesAllowed &&\n                    game.rules.mpDialogSettings.allyChangeAllowed,\n                mapName: game.gameOpts.mapTitle,\n                messages: params.chatHistory?.getAll(),\n                chatHistory: params.chatHistory,\n                onToggleTaunts: (enabled: boolean) => (this.taunts.value = enabled),\n                onToggleAlliance: params.onToggleAlliance,\n                onToggleChat: (player: Player, enabled: boolean) => {\n                    if (enabled) {\n                        this.mutedPlayers.delete(player.name);\n                    }\n                    else {\n                        this.mutedPlayers.add(player.name);\n                    }\n                },\n                onSendMessage: params.onSendMessage,\n                onCancelMessage: (shouldCancel: boolean) => shouldCancel && params.onCancel(),\n                strings: this.strings,\n            },\n        }));\n        this.controller?.setMainComponent(component);\n        this.disposables.add(() => (this.form = undefined));\n    }\n    private buildPlayerInfos(game: Game, localPlayer: Player): PlayerInfo[] {\n        const alliances = localPlayer ? game.alliances.filterByPlayer(localPlayer) : undefined;\n        return game\n            .getNonNeutralPlayers()\n            .filter((player) => player !== localPlayer)\n            .map((player) => ({\n            player: player,\n            muted: this.mutedPlayers.has(player.name),\n            allianceToggleable: !!localPlayer &&\n                game.alliances.canRequestAlliance(player) &&\n                game.alliances.canFormAlliance(localPlayer, player),\n            alliance: alliances?.find((alliance) => alliance.players.first === player || alliance.players.second === player),\n        }));\n    }\n    async onLeave(): Promise<void> {\n        this.params = undefined;\n        this.controller?.hideSidebarButtons();\n        this.controller?.toggleContentAreaVisibility(false);\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/gameMenu/GameMenuController.ts",
    "content": "import { Controller } from '@/gui/screen/Controller';\ninterface SidebarButton {\n    label: string;\n    isBottom?: boolean;\n    onClick: () => void;\n}\ninterface Hud {\n    showSidebarMenu(buttons: SidebarButton[]): void;\n    hideSidebarMenu(): void;\n    setMenuContentComponent(component?: any): void;\n    toggleMenuContentVisibility(visible: boolean): void;\n}\nexport class GameMenuController extends Controller {\n    private hud: Hud;\n    private contentAreaVisible = false;\n    private sidebarButtons?: SidebarButton[];\n    private mainContentComponent?: any;\n    constructor(hud: Hud) {\n        super();\n        this.hud = hud;\n        this.contentAreaVisible = false;\n    }\n    async goToScreenBlocking(screenType: any, params?: any): Promise<any> {\n        return super.goToScreenBlocking(screenType, params);\n    }\n    goToScreen(screenType: any, params?: any): void {\n        super.goToScreen(screenType, params);\n    }\n    async pushScreen(screenType: any, params?: any): Promise<void> {\n        this.setMainComponent();\n        await super.pushScreen(screenType, params);\n    }\n    async popScreen(result?: any): Promise<any> {\n        this.setMainComponent();\n        return await super.popScreen(result);\n    }\n    async close(): Promise<void> {\n        while (this.getCurrentScreen() || this.screenStack.length) {\n            await this.popScreen();\n        }\n    }\n    setHud(hud: Hud): void {\n        this.hud = hud;\n    }\n    setSidebarButtons(buttons: SidebarButton[]): void {\n        this.sidebarButtons = buttons;\n    }\n    showSidebarButtons(): void {\n        if (this.sidebarButtons === undefined) {\n            throw new Error(\"Sidebar buttons should be set first\");\n        }\n        this.hud.showSidebarMenu(this.sidebarButtons);\n    }\n    hideSidebarButtons(): void {\n        this.sidebarButtons = undefined;\n        this.hud.hideSidebarMenu();\n    }\n    setMainComponent(component?: any): void {\n        this.mainContentComponent = component;\n        this.hud.setMenuContentComponent(this.mainContentComponent);\n    }\n    toggleContentAreaVisibility(visible: boolean): void {\n        this.contentAreaVisible = visible;\n        this.hud.toggleMenuContentVisibility(visible);\n    }\n    rerenderCurrentScreen(): void {\n        if (this.sidebarButtons) {\n            this.hud.showSidebarMenu(this.sidebarButtons);\n        }\n        this.hud.setMenuContentComponent(this.mainContentComponent);\n        this.hud.toggleMenuContentVisibility(this.contentAreaVisible);\n    }\n    destroy(): void {\n        super.destroy();\n        this.setMainComponent(undefined);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/gameMenu/GameMenuHomeScreen.ts",
    "content": "import { ScreenType } from '@/gui/screen/game/gameMenu/ScreenType';\nimport { FullScreen } from '@/gui/FullScreen';\nimport { getHumanReadableKey } from '@/gui/screen/options/component/getHumanReadableKey';\nimport { GameMenuScreen } from '@/gui/screen/game/GameMenuScreen';\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface GameMenuHomeParams {\n    onCancel: () => void;\n    onQuit?: () => void;\n    onObserve?: () => void;\n    observeAllowed?: boolean;\n}\ninterface SidebarButton {\n    label: string;\n    isBottom?: boolean;\n    tooltip?: string;\n    disabled?: boolean;\n    onClick: () => void;\n}\ninterface GameMenuController {\n    toggleContentAreaVisibility(visible: boolean): void;\n    setSidebarButtons(buttons: SidebarButton[]): void;\n    showSidebarButtons(): void;\n    hideSidebarButtons(): void;\n    pushScreen?(screenType: ScreenType, params?: any): Promise<void>;\n}\nexport class GameMenuHomeScreen extends GameMenuScreen {\n    private strings: Strings;\n    private fullScreen: FullScreen;\n    private params?: GameMenuHomeParams;\n    declare controller?: GameMenuController;\n    constructor(strings: Strings, fullScreen: FullScreen) {\n        super();\n        this.strings = strings;\n        this.fullScreen = fullScreen;\n    }\n    onEnter(params: GameMenuHomeParams): void {\n        this.params = params;\n        this.controller?.toggleContentAreaVisibility(true);\n        this.initView(params);\n    }\n    private initView(params: GameMenuHomeParams): void {\n        const strings = this.strings;\n        const buttons: SidebarButton[] = [\n            {\n                label: strings.get(\"GUI:Options\"),\n                onClick: () => {\n                    this.controller?.pushScreen?.(ScreenType.Options);\n                },\n            },\n            {\n                label: strings.get(\"GUI:Fullscreen\", getHumanReadableKey(FullScreen.hotKey)),\n                tooltip: strings.get(\"STT:Fullscreen\"),\n                disabled: !this.fullScreen.isAvailable(),\n                onClick: () => this.fullScreen.toggle(),\n            },\n            {\n                label: strings.get(\"GUI:AbortMission\"),\n                onClick: () => {\n                    this.controller?.pushScreen?.(ScreenType.QuitConfirm, this.params);\n                },\n            },\n            {\n                label: strings.get(\"GUI:ResumeMission\"),\n                isBottom: true,\n                onClick: params.onCancel,\n            },\n        ];\n        this.controller?.setSidebarButtons(buttons);\n        this.controller?.showSidebarButtons();\n    }\n    async onLeave(): Promise<void> {\n        this.controller?.hideSidebarButtons();\n        this.controller?.toggleContentAreaVisibility(false);\n    }\n    async onStack(): Promise<void> {\n        this.controller?.hideSidebarButtons();\n    }\n    onUnstack(): void {\n        if (this.params) {\n            this.initView(this.params);\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/gameMenu/QuitConfirmScreen.ts",
    "content": "import { GameMenuScreen } from '@/gui/screen/game/GameMenuScreen';\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface QuitConfirmParams {\n    onQuit: () => void;\n    onCancel: () => void;\n    onObserve?: () => void;\n    observeAllowed?: boolean;\n}\ninterface SidebarButton {\n    label: string;\n    isBottom?: boolean;\n    onClick: () => void;\n}\ninterface GameMenuController {\n    setSidebarButtons(buttons: SidebarButton[]): void;\n    showSidebarButtons(): void;\n    hideSidebarButtons(): void;\n}\nexport class QuitConfirmScreen extends GameMenuScreen {\n    private strings: Strings;\n    declare controller?: GameMenuController;\n    constructor(strings: Strings) {\n        super();\n        this.strings = strings;\n    }\n    onEnter(params: QuitConfirmParams): void {\n        this.initView(params);\n    }\n    private initView(params: QuitConfirmParams): void {\n        const strings = this.strings;\n        const buttons: SidebarButton[] = [\n            { label: strings.get(\"GUI:Quit\"), onClick: params.onQuit },\n            ...(params.observeAllowed\n                ? [{ label: strings.get(\"GUI:Observe\"), onClick: params.onObserve! }]\n                : []),\n            {\n                label: strings.get(\"GUI:ResumeMission\"),\n                isBottom: true,\n                onClick: params.onCancel,\n            },\n        ];\n        this.controller?.setSidebarButtons(buttons);\n        this.controller?.showSidebarButtons();\n    }\n    async onLeave(): Promise<void> {\n        this.controller?.hideSidebarButtons();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/gameMenu/ScreenParamsMap.ts",
    "content": "import { ScreenType } from './ScreenType';\nexport {};\n"
  },
  {
    "path": "src/gui/screen/game/gameMenu/ScreenType.ts",
    "content": "export enum ScreenType {\n    Home = 0,\n    Diplo = 1,\n    ConnectionInfo = 2,\n    QuitConfirm = 3,\n    Options = 4,\n    OptionsSound = 5,\n    OptionsKeyboard = 6\n}\n"
  },
  {
    "path": "src/gui/screen/game/loadingScreen/LanLoadingScreenApi.ts",
    "content": "import { jsx } from '@/gui/jsx/jsx';\nimport { OBS_COUNTRY_ID, NO_TEAM_ID } from '@/game/gameopts/constants';\nimport { PlayerConnectionStatus } from '@/network/gamestate/PlayerConnectionStatus';\nimport { LanMatchSession } from '@/network/lan/LanMatchSession';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { LoadingScreenWrapper } from './LoadingScreenWrapper';\nimport { LoadingScreenApi } from './LoadingScreenApi';\n\ninterface Player {\n    name: string;\n    countryId: number;\n    colorId: number;\n    teamId: number;\n}\n\ninterface Country {\n    name: string;\n    side: any;\n    uiName: string;\n}\n\ninterface Rules {\n    getMultiplayerColors(): Map<number, any>;\n    getMultiplayerCountries(): Country[];\n    colors: Map<string, any>;\n}\n\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\n\ninterface UiScene {\n    menuViewport: any;\n    add(object: any): void;\n    remove(object: any): void;\n}\n\ninterface JsxRenderer {\n    render(element: any): any[];\n}\n\ninterface GameResConfig {\n    isCdn(): boolean;\n    getCdnBaseUrl(): string;\n}\n\ninterface ExtendedPlayerInfo {\n    name: string;\n    status: any;\n    loadPercent: number;\n    country: Country;\n    color: string;\n    team: number;\n}\n\nexport class LanLoadingScreenApi implements LoadingScreenApi {\n    private lastLoadPercent = 0;\n    private disposables = new CompositeDisposable();\n    private players?: Player[];\n    private localPlayerName?: string;\n    private mapName?: string;\n    private loadingScreen?: any;\n\n    private handleLanMatchUpdate = () => {\n        if (!this.players || !this.localPlayerName || !this.mapName) {\n            return;\n        }\n        if (this.loadingScreen) {\n            this.loadingScreen.applyOptions((options: any) => {\n                options.playerInfos = this.createExtendedLoadingInfos();\n            });\n            return;\n        }\n        this.createLoadingScreen();\n    };\n\n    constructor(\n        private readonly lanMatchSession: LanMatchSession,\n        private readonly rules: Rules,\n        private readonly strings: Strings,\n        private readonly uiScene: UiScene,\n        private readonly jsxRenderer: JsxRenderer,\n        private readonly gameResConfig: GameResConfig\n    ) { }\n\n    async start(players: Player[], mapName: string, localPlayerName: string): Promise<void> {\n        this.players = players;\n        this.localPlayerName = localPlayerName;\n        this.mapName = mapName;\n        this.lanMatchSession.onSnapshotChange.subscribe(this.handleLanMatchUpdate);\n        this.disposables.add(() => this.lanMatchSession.onSnapshotChange.unsubscribe(this.handleLanMatchUpdate));\n        this.handleLanMatchUpdate();\n    }\n\n    onLoadProgress(percent: number): void {\n        const roundedPercent = Math.floor(percent);\n        if (roundedPercent <= this.lastLoadPercent) {\n            return;\n        }\n        this.lastLoadPercent = roundedPercent;\n        this.lanMatchSession.reportLoadProgress(roundedPercent);\n        this.handleLanMatchUpdate();\n    }\n\n    private createExtendedLoadingInfos(): ExtendedPlayerInfo[] {\n        const colors = [...this.rules.getMultiplayerColors().values()];\n        const countries = this.rules.getMultiplayerCountries();\n        const lanSnapshot = this.lanMatchSession.getSnapshot();\n        const descriptor = this.lanMatchSession.getLaunchDescriptor();\n        const assignmentByName = new Map(descriptor.humanAssignments.map((assignment) => [assignment.name, assignment.peerId] as [string, string]));\n        const transportByPeerId = new Map(lanSnapshot.transportMembers.map((member) => [member.id, member]));\n        const hasTeams = this.players?.every((player) => player.countryId === OBS_COUNTRY_ID || player.teamId !== NO_TEAM_ID);\n        const extendedInfos = (this.players ?? []).map((player) => {\n            const peerId = assignmentByName.get(player.name);\n            const transportMember = peerId ? transportByPeerId.get(peerId) : undefined;\n            const status = !transportMember\n                ? PlayerConnectionStatus.Disconnected\n                : transportMember.isSelf || transportMember.status === 'connected'\n                    ? PlayerConnectionStatus.Connected\n                    : PlayerConnectionStatus.Lagging;\n            return {\n                name: player.name,\n                status,\n                loadPercent: peerId ? lanSnapshot.loadPercentByPeerId[peerId] ?? 0 : 0,\n                country: countries[player.countryId],\n                color: player.countryId === OBS_COUNTRY_ID\n                    ? '#fff'\n                    : colors[player.colorId].asHexString(),\n                team: player.teamId,\n            };\n        });\n\n        if (hasTeams) {\n            return extendedInfos.sort((a, b) => {\n                if (Boolean(a.country) === Boolean(b.country)) {\n                    return a.team - b.team;\n                }\n                return Number(b.country !== undefined) - Number(a.country !== undefined);\n            });\n        }\n        return extendedInfos;\n    }\n\n    private createLoadingScreen(): void {\n        const [uiObject] = this.jsxRenderer.render(jsx(LoadingScreenWrapper, {\n            ref: (ref: any) => (this.loadingScreen = ref),\n            strings: this.strings,\n            rules: this.rules,\n            viewport: this.uiScene.menuViewport,\n            playerName: this.localPlayerName,\n            mapName: this.mapName!,\n            playerInfos: this.createExtendedLoadingInfos(),\n            gameResConfig: this.gameResConfig,\n        }));\n        this.uiScene.add(uiObject);\n        this.disposables.add(uiObject, () => this.uiScene.remove(uiObject), () => (this.loadingScreen = undefined));\n    }\n\n    dispose(): void {\n        this.disposables.dispose();\n    }\n\n    updateViewport(): void {\n        this.loadingScreen?.updateViewport(this.uiScene.menuViewport);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/loadingScreen/LoadingScreen.tsx",
    "content": "import React from 'react';\nimport { PlayerConnectionStatus } from '@/network/gamestate/PlayerConnectionStatus';\nimport { CountryIcon } from '@/gui/component/CountryIcon';\nimport { NO_TEAM_ID, OBS_COUNTRY_NAME } from '@/game/gameopts/constants';\nimport { formatTeamId } from '@/gui/component/TeamSelect';\ninterface Country {\n    name: string;\n    uiName?: string;\n    side?: any;\n}\ninterface PlayerInfo {\n    name: string;\n    status: PlayerConnectionStatus;\n    loadPercent: number;\n    country?: Country;\n    color: string;\n    team: number;\n}\ninterface Viewport {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface LoadingScreenProps {\n    playerInfos: PlayerInfo[];\n    countryName: string;\n    color: string;\n    bgImageSrc?: string;\n    viewport: Viewport;\n    strings: {\n        get(key: string, ...args: any[]): string;\n    };\n    countryUiNames: Map<string, string>;\n    mapName: string;\n}\nconst countrySpecialUnits = new Map<string, string>()\n    .set(\"Americans\", \"Name:Para\")\n    .set(\"French\", \"Name:GTGCAN\")\n    .set(\"Germans\", \"Name:TNKD\")\n    .set(\"British\", \"Name:SNIPE\")\n    .set(\"Russians\", \"Name:TTNK\")\n    .set(\"Confederation\", \"Name:TERROR\")\n    .set(\"Africans\", \"Name:DTRUCK\")\n    .set(\"Arabs\", \"Name:DESO\")\n    .set(\"Alliance\", \"Name:BEAGLE\");\nconst countryBriefings = new Map<string, string>()\n    .set(\"Americans\", \"LoadBrief:USA\")\n    .set(\"French\", \"LoadBrief:French\")\n    .set(\"Germans\", \"LoadBrief:Germans\")\n    .set(\"British\", \"LoadBrief:British\")\n    .set(\"Russians\", \"LoadBrief:Russia\")\n    .set(\"Confederation\", \"LoadBrief:Cuba\")\n    .set(\"Africans\", \"LoadBrief:Lybia\")\n    .set(\"Arabs\", \"LoadBrief:Iraq\")\n    .set(\"Alliance\", \"LoadBrief:Korea\");\nexport class LoadingScreen extends React.Component<LoadingScreenProps> {\n    render(): React.ReactElement {\n        const playerInfos = this.props.playerInfos;\n        const countryName = this.props.countryName;\n        const color = this.props.color;\n        const showTeams = playerInfos.length > 1 &&\n            playerInfos.every(player => !player.country || player.team !== NO_TEAM_ID);\n        const briefingKey = countryBriefings.get(countryName);\n        const specialUnitKey = countrySpecialUnits.get(countryName);\n        const strings = this.props.strings;\n        return (<div className=\"loading-screen\" style={this.getStyle(this.props.bgImageSrc)}>\n        {specialUnitKey && (<div className=\"special-unit-name\">\n            {strings.get(specialUnitKey)}\n          </div>)}\n        {briefingKey && (<div className=\"briefing-text\" style={{ color }}>\n            {strings.get(briefingKey)}\n          </div>)}\n        <div className=\"loading-text\" style={{ color }}>\n          {strings.get(\"GUI:LoadingEx\")}\n        </div>\n        <div className=\"player-status-container\">\n          {playerInfos ? playerInfos.map(player => this.renderStatus(player, showTeams)) : null}\n        </div>\n        <div style={{ color }} className=\"country-name\">\n          {this.props.strings.get(this.props.countryUiNames.get(countryName) ?? countryName)}\n        </div>\n        <div style={{ color }} className=\"map-name\">\n          {this.props.mapName}\n        </div>\n      </div>);\n    }\n    private renderStatus(player: PlayerInfo, showTeams: boolean): React.ReactElement {\n        const opacity = player.status === PlayerConnectionStatus.Connected ? 1 : 0.5;\n        return (<div key={player.name} className=\"player-status\" style={{ opacity, color: player.color }}>\n        {showTeams && (<span className=\"player-team\">\n            {player.country !== undefined &&\n                    this.props.strings.get(\"GUI:TeamNo\", formatTeamId(player.team))}\n          </span>)}\n        <progress value={player.loadPercent.toString()} max={100}/>\n        <CountryIcon country={player.country ? player.country.name : OBS_COUNTRY_NAME}/>\n        <span className=\"player-name\">\n          {player.name}\n        </span>\n      </div>);\n    }\n    private getStyle(bgImageSrc?: string): React.CSSProperties {\n        const viewport = this.props.viewport;\n        return {\n            backgroundImage: bgImageSrc ? `url(${bgImageSrc})` : undefined,\n            backgroundSize: \"cover\",\n            width: viewport.width + \"px\",\n            height: viewport.height + \"px\",\n            position: \"absolute\",\n            left: viewport.x,\n            top: viewport.y,\n        };\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/loadingScreen/LoadingScreenApi.ts",
    "content": "export interface LoadingScreenApi {\n    start(...args: any[]): Promise<void>;\n    onLoadProgress(percent: number): void;\n    dispose(): void;\n    updateViewport(): void;\n}\n"
  },
  {
    "path": "src/gui/screen/game/loadingScreen/LoadingScreenApiFactory.ts",
    "content": "import { LoadInfoParser } from '@/network/gameopt/LoadInfoParser';\nimport { LanMatchSession } from '@/network/lan/LanMatchSession';\nimport { LanLoadingScreenApi } from './LanLoadingScreenApi';\nimport { MpLoadingScreenApi } from './MpLoadingScreenApi';\nimport { ReplayLoadingScreenApi } from './ReplayLoadingScreenApi';\nimport { SpLoadingScreenApi } from './SpLoadingScreenApi';\nimport { LoadingScreenApi } from './LoadingScreenApi';\nexport enum LoadingScreenType {\n    SinglePlayer = 0,\n    MultiPlayer = 1,\n    Replay = 2,\n    Lan = 3\n}\ninterface Rules {\n    getMultiplayerCountries(): any[];\n    getMultiplayerColors(): Map<any, any>;\n    colors: Map<string, any>;\n}\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface UiScene {\n    menuViewport: any;\n    add(object: any): void;\n    remove(object: any): void;\n}\ninterface JsxRenderer {\n    render(element: any): any[];\n}\ninterface GameResConfig {\n    isCdn(): boolean;\n    getCdnBaseUrl(): string;\n}\ninterface GservCon {\n    isOpen(): boolean;\n    onLoadInfo: {\n        subscribe(handler: (info: any) => void): void;\n        unsubscribe(handler: (info: any) => void): void;\n    };\n    requestLoadInfo(): void;\n    sendLoadedPercent(percent: number): void;\n}\nexport class LoadingScreenApiFactory {\n    constructor(private rules: Rules, private strings: Strings, private uiScene: UiScene, private jsxRenderer: JsxRenderer, private gameResConfig: GameResConfig, private gservCon: GservCon) { }\n    create(type: LoadingScreenType, lanMatchSession?: LanMatchSession): LoadingScreenApi {\n        const { rules, strings, uiScene, jsxRenderer, gameResConfig, gservCon, } = this;\n        switch (type) {\n            case LoadingScreenType.SinglePlayer:\n                return new SpLoadingScreenApi(rules, strings, uiScene, jsxRenderer, gameResConfig);\n            case LoadingScreenType.MultiPlayer:\n                const loadInfoParser = new LoadInfoParser();\n                return new MpLoadingScreenApi(gservCon, loadInfoParser, rules, strings, uiScene, jsxRenderer, gameResConfig);\n            case LoadingScreenType.Replay:\n                return new ReplayLoadingScreenApi(rules, strings, uiScene, jsxRenderer, gameResConfig);\n            case LoadingScreenType.Lan:\n                if (!lanMatchSession) {\n                    throw new Error('Missing LAN match session for LAN loading screen.');\n                }\n                return new LanLoadingScreenApi(lanMatchSession, rules, strings, uiScene, jsxRenderer, gameResConfig);\n            default:\n                throw new Error(`Unsupported loading screen type \"${type}\"`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/loadingScreen/LoadingScreenWrapper.tsx",
    "content": "import { jsx } from '@/gui/jsx/jsx';\nimport { HtmlContainer } from '@/gui/HtmlContainer';\nimport { UiComponent } from '@/gui/jsx/UiComponent';\nimport { UiObject } from '@/gui/UiObject';\nimport { HtmlView } from '@/gui/jsx/HtmlView';\nimport { LoadingScreen } from './LoadingScreen';\nimport { OBS_COUNTRY_NAME, OBS_COUNTRY_UI_NAME } from '@/game/gameopts/constants';\nimport * as THREE from 'three';\nimport { SideType } from '@/game/SideType';\nimport { Engine } from '@/engine/Engine';\ninterface Country {\n    name: string;\n    side: SideType;\n    uiName: string;\n}\ninterface PlayerInfo {\n    name: string;\n    country?: Country;\n    color?: string;\n}\ninterface Rules {\n    getMultiplayerCountries(): Country[];\n    colors: Map<string, any>;\n}\ninterface Viewport {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface GameResConfig {\n    isCdn(): boolean;\n    getCdnBaseUrl(): string;\n}\ninterface LoadingScreenWrapperProps {\n    playerInfos: PlayerInfo[];\n    strings: {\n        get(key: string, ...args: any[]): string;\n    };\n    rules: Rules;\n    viewport: Viewport;\n    playerName?: string;\n    mapName: string;\n    gameResConfig: GameResConfig;\n}\nconst countryBackgrounds = new Map<string, string>()\n    .set(\"Americans\", \"ls800ustates.png\")\n    .set(\"French\", \"ls800france.png\")\n    .set(\"Germans\", \"ls800germany.png\")\n    .set(\"British\", \"ls800ukingdom.png\")\n    .set(\"Russians\", \"ls800russia.png\")\n    .set(\"Confederation\", \"ls800cuba.png\")\n    .set(\"Africans\", \"ls800libya.png\")\n    .set(\"Arabs\", \"ls800iraq.png\")\n    .set(\"Alliance\", \"ls800korea.png\")\n    .set(OBS_COUNTRY_NAME, \"ls800obs.png\");\nexport class LoadingScreenWrapper extends UiComponent<LoadingScreenWrapperProps> {\n    private declare countryName: string;\n    private declare color: string;\n    private declare bgHtmlImg?: string;\n    private declare bgSpriteImg?: string;\n    private declare bgSpritePal?: string;\n    private declare htmlEl?: any;\n    private declare sprite?: any;\n    createUiObject({ playerName, gameResConfig }: {\n        playerName?: string;\n        gameResConfig: GameResConfig;\n    }): UiObject {\n        const uiObject = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        const player = playerName\n            ? this.props.playerInfos.find(p => p.name === playerName)\n            : undefined;\n        const countryName = player?.country ? player.country.name : OBS_COUNTRY_NAME;\n        this.countryName = countryName;\n        let color = player?.color ?? \"#fff\";\n        if (player?.country) {\n            const loadColorKey = player.country.side === SideType.GDI ? \"AlliedLoad\" : \"SovietLoad\";\n            color = this.props.rules.colors.get(loadColorKey)?.asHexString() ?? \"#fff\";\n        }\n        this.color = color;\n        const backgroundImage = countryBackgrounds.get(countryName);\n        if (backgroundImage) {\n            if (gameResConfig.isCdn()) {\n                this.bgHtmlImg = gameResConfig.getCdnBaseUrl() + \"ls/\" + backgroundImage;\n            }\n            else {\n                this.bgSpriteImg = backgroundImage.replace(\"png\", \"shp\");\n                this.bgSpritePal = player?.country ? \"mpls.pal\" : \"mplsobs.pal\";\n            }\n        }\n        else {\n            console.warn(\"Missing loading image for country \" + countryName);\n        }\n        try {\n            console.log('[LoadingScreenWrapper] createUiObject:', {\n                isCdn: gameResConfig.isCdn(),\n                countryName,\n                bgSpriteImg: this.bgSpriteImg,\n                bgSpritePal: this.bgSpritePal,\n                bgHtmlImg: this.bgHtmlImg,\n            });\n            if (!gameResConfig.isCdn() && Engine.vfs) {\n                const imgName = this.bgSpriteImg!;\n                const palName = this.bgSpritePal!;\n                const imgExists = Engine.vfs.fileExists(imgName);\n                const palExists = Engine.vfs.fileExists(palName);\n                console.log('[LoadingScreenWrapper] VFS existence:', { imgName, imgExists, palName, palExists });\n                try {\n                    (Engine.vfs as any).debugListFileOwners?.(imgName);\n                    (Engine.vfs as any).debugListFileOwners?.(palName);\n                    console.log('[LoadingScreenWrapper] VFS archives:', Engine.vfs.listArchives());\n                }\n                catch { }\n            }\n        }\n        catch { }\n        return uiObject;\n    }\n    defineChildren(): any {\n        const countries = this.props.rules.getMultiplayerCountries();\n        const viewport = this.props.viewport;\n        try {\n            console.log('[LoadingScreenWrapper] defineChildren: willRenderSprite=', !this.props.gameResConfig.isCdn(), {\n                bgSpriteImg: this.bgSpriteImg,\n                bgSpritePal: this.bgSpritePal,\n                viewport,\n            });\n        }\n        catch { }\n        return jsx(\"fragment\", null, this.props.gameResConfig.isCdn()\n            ? []\n            : jsx(\"sprite\", {\n                image: this.bgSpriteImg,\n                palette: this.bgSpritePal,\n                x: viewport.x,\n                y: viewport.y,\n                ref: (sprite: any) => (this.sprite = sprite),\n            }), jsx(HtmlView, {\n            innerRef: (el: any) => (this.htmlEl = el),\n            component: LoadingScreen,\n            props: {\n                viewport: this.props.viewport,\n                countryUiNames: new Map([\n                    [OBS_COUNTRY_NAME, OBS_COUNTRY_UI_NAME],\n                    ...countries.map(country => [country.name, country.uiName] as [\n                        string,\n                        string\n                    ])\n                ]),\n                strings: this.props.strings,\n                countryName: this.countryName,\n                mapName: this.props.mapName,\n                color: this.color,\n                playerInfos: this.props.playerInfos,\n                bgImageSrc: this.bgHtmlImg,\n            },\n        }));\n    }\n    updateViewport(viewport: Viewport): void {\n        this.htmlEl?.applyOptions((options: any) => (options.viewport = viewport));\n        this.sprite?.setPosition(viewport.x, viewport.y);\n    }\n    applyOptions(optionsUpdater: (options: any) => void): void {\n        this.htmlEl?.applyOptions(optionsUpdater);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/loadingScreen/MpLoadingScreenApi.ts",
    "content": "import { jsx } from '@/gui/jsx/jsx';\nimport { OBS_COUNTRY_ID, NO_TEAM_ID } from '@/game/gameopts/constants';\nimport { PlayerConnectionStatus } from '@/network/gamestate/PlayerConnectionStatus';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { LoadingScreenWrapper } from './LoadingScreenWrapper';\nimport { LoadingScreenApi } from './LoadingScreenApi';\ninterface LoadInfo {\n    name: string;\n    status: any;\n    loadPercent: number;\n}\ninterface LoadInfoParser {\n    parse(data: any): LoadInfo[];\n}\ninterface Player {\n    name: string;\n    countryId: number;\n    colorId: number;\n    teamId: number;\n}\ninterface Country {\n    name: string;\n    side: any;\n    uiName: string;\n}\ninterface Rules {\n    getMultiplayerColors(): Map<number, any>;\n    getMultiplayerCountries(): Country[];\n    colors: Map<string, any>;\n}\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface UiScene {\n    menuViewport: any;\n    add(object: any): void;\n    remove(object: any): void;\n}\ninterface JsxRenderer {\n    render(element: any): any[];\n}\ninterface GameResConfig {\n    isCdn(): boolean;\n    getCdnBaseUrl(): string;\n}\ninterface GservCon {\n    isOpen(): boolean;\n    onLoadInfo: {\n        subscribe(handler: (info: any) => void): void;\n        unsubscribe(handler: (info: any) => void): void;\n    };\n    requestLoadInfo(): void;\n    sendLoadedPercent(percent: number): void;\n}\ninterface ExtendedPlayerInfo {\n    name: string;\n    status: any;\n    loadPercent: number;\n    country: Country;\n    color: string;\n    team: number;\n}\nexport class MpLoadingScreenApi implements LoadingScreenApi {\n    private lastLoadPercent = 0;\n    private disposables = new CompositeDisposable();\n    private players?: Player[];\n    private localPlayerName?: string;\n    private mapName?: string;\n    private loadingScreen?: any;\n    private handleLoadInfoUpdate = (loadInfoData: any) => {\n        const loadInfos = this.loadInfoParser.parse(loadInfoData);\n        if (this.loadingScreen) {\n            this.loadingScreen.applyOptions((options: any) => {\n                options.playerInfos = this.createExtendedLoadingInfos(loadInfos);\n            });\n        }\n        else {\n            this.createLoadingScreen(loadInfos);\n        }\n    };\n    constructor(private gservCon: GservCon | undefined, private loadInfoParser: LoadInfoParser, private rules: Rules, private strings: Strings, private uiScene: UiScene, private jsxRenderer: JsxRenderer, private gameResConfig: GameResConfig) { }\n    async start(players: Player[], mapName: string, localPlayerName: string): Promise<void> {\n        this.players = players;\n        this.localPlayerName = localPlayerName;\n        this.mapName = mapName;\n        if (!this.gservCon?.isOpen()) {\n            this.handleLoadInfoUpdate(this.createFallbackLoadInfos(0));\n            return;\n        }\n        if (this.gservCon.isOpen()) {\n            this.mapName = mapName;\n            this.gservCon.onLoadInfo.subscribe(this.handleLoadInfoUpdate);\n            this.disposables.add(() => this.gservCon.onLoadInfo.unsubscribe(this.handleLoadInfoUpdate));\n            this.gservCon.requestLoadInfo();\n            const intervalId = setInterval(() => {\n                if (this.gservCon?.isOpen()) {\n                    this.gservCon.requestLoadInfo();\n                }\n                else {\n                    this.disposables.dispose();\n                }\n            }, 10000);\n            this.disposables.add(() => clearInterval(intervalId));\n        }\n    }\n    onLoadProgress(percent: number): void {\n        const roundedPercent = Math.floor(percent);\n        if (roundedPercent > this.lastLoadPercent) {\n            this.lastLoadPercent = roundedPercent;\n            if (this.gservCon?.isOpen()) {\n                this.gservCon.sendLoadedPercent(roundedPercent);\n            }\n            else if (this.players?.length) {\n                this.handleLoadInfoUpdate(this.createFallbackLoadInfos(roundedPercent));\n            }\n        }\n    }\n    private createFallbackLoadInfos(loadPercent: number): LoadInfo[] {\n        return (this.players ?? []).map((player) => ({\n            name: player.name,\n            status: PlayerConnectionStatus.Connected,\n            loadPercent: player.name === this.localPlayerName ? loadPercent : 0,\n        }));\n    }\n    private createExtendedLoadingInfos(loadInfos: LoadInfo[]): ExtendedPlayerInfo[] {\n        const colors = [...this.rules.getMultiplayerColors().values()];\n        const countries = this.rules.getMultiplayerCountries();\n        const hasTeams = this.players?.every(player => player.countryId === OBS_COUNTRY_ID ||\n            player.teamId !== NO_TEAM_ID);\n        const extendedInfos = loadInfos\n            .map(loadInfo => {\n            const player = this.players!.find(p => p.name === loadInfo.name)!;\n            return {\n                name: loadInfo.name,\n                status: loadInfo.status,\n                loadPercent: loadInfo.loadPercent,\n                country: countries[player.countryId],\n                color: player.countryId === OBS_COUNTRY_ID\n                    ? \"#fff\"\n                    : colors[player.colorId].asHexString(),\n                team: player.teamId,\n            };\n        });\n        if (hasTeams) {\n            return extendedInfos.sort((a, b) => {\n                if (Boolean(a.country) === Boolean(b.country)) {\n                    return a.team - b.team;\n                }\n                return Number(b.country !== undefined) - Number(a.country !== undefined);\n            });\n        }\n        return extendedInfos;\n    }\n    private createLoadingScreen(loadInfos: LoadInfo[]): void {\n        const [uiObject] = this.jsxRenderer.render(jsx(LoadingScreenWrapper, {\n            ref: (ref: any) => (this.loadingScreen = ref),\n            strings: this.strings,\n            rules: this.rules,\n            viewport: this.uiScene.menuViewport,\n            playerName: this.localPlayerName,\n            mapName: this.mapName!,\n            playerInfos: this.createExtendedLoadingInfos(loadInfos),\n            gameResConfig: this.gameResConfig,\n        }));\n        this.uiScene.add(uiObject);\n        this.disposables.add(uiObject, () => this.uiScene.remove(uiObject), () => (this.loadingScreen = undefined));\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n    updateViewport(): void {\n        this.loadingScreen?.updateViewport(this.uiScene.menuViewport);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/loadingScreen/ReplayLoadingScreenApi.ts",
    "content": "import { jsx } from '@/gui/jsx/jsx';\nimport { OBS_COUNTRY_ID, NO_TEAM_ID } from '@/game/gameopts/constants';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { PlayerConnectionStatus } from '@/network/gamestate/PlayerConnectionStatus';\nimport { LoadingScreenWrapper } from './LoadingScreenWrapper';\nimport { LoadingScreenApi } from './LoadingScreenApi';\ninterface Player {\n    name: string;\n    countryId: number;\n    colorId: number;\n    teamId: number;\n}\ninterface Country {\n    name: string;\n    side: any;\n    uiName: string;\n}\ninterface Rules {\n    getMultiplayerColors(): Map<number, any>;\n    getMultiplayerCountries(): Country[];\n    colors: Map<string, any>;\n}\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface UiScene {\n    menuViewport: any;\n    add(object: any): void;\n    remove(object: any): void;\n}\ninterface JsxRenderer {\n    render(element: any): any[];\n}\ninterface GameResConfig {\n    isCdn(): boolean;\n    getCdnBaseUrl(): string;\n}\ninterface ExtendedPlayerInfo {\n    name: string;\n    status: PlayerConnectionStatus;\n    loadPercent: number;\n    country: Country;\n    color: string;\n    team: number;\n}\nexport class ReplayLoadingScreenApi implements LoadingScreenApi {\n    private lastLoadPercent = 0;\n    private disposables = new CompositeDisposable();\n    private players?: Player[];\n    private mapName?: string;\n    private loadingScreen?: any;\n    private lastRenderTime?: number;\n    private handleLoadInfoUpdate = (loadPercent: number) => {\n        if (this.loadingScreen) {\n            const now = performance.now();\n            if (!this.lastRenderTime || now - this.lastRenderTime > 1000 / 15) {\n                this.lastRenderTime = now;\n                this.loadingScreen.applyOptions((options: any) => {\n                    options.playerInfos = this.createExtendedLoadingInfos(loadPercent);\n                });\n            }\n        }\n        else {\n            this.createLoadingScreen(loadPercent);\n        }\n    };\n    constructor(private rules: Rules, private strings: Strings, private uiScene: UiScene, private jsxRenderer: JsxRenderer, private gameResConfig: GameResConfig) { }\n    async start(players: Player[], mapName: string): Promise<void> {\n        this.players = players;\n        this.mapName = mapName;\n        this.handleLoadInfoUpdate(0);\n    }\n    onLoadProgress(percent: number): void {\n        const roundedPercent = Math.floor(percent);\n        if (roundedPercent > this.lastLoadPercent) {\n            this.lastLoadPercent = roundedPercent;\n            this.handleLoadInfoUpdate(roundedPercent);\n        }\n    }\n    private createExtendedLoadingInfos(loadPercent: number): ExtendedPlayerInfo[] {\n        const colors = [...this.rules.getMultiplayerColors().values()];\n        const countries = this.rules.getMultiplayerCountries();\n        const hasTeams = this.players?.every(player => player.countryId === OBS_COUNTRY_ID ||\n            player.teamId !== NO_TEAM_ID);\n        const extendedInfos = this.players!\n            .filter(player => player.countryId !== OBS_COUNTRY_ID)\n            .map(player => ({\n            name: player.name,\n            status: PlayerConnectionStatus.Connected,\n            loadPercent,\n            country: countries[player.countryId],\n            color: colors[player.colorId].asHexString(),\n            team: player.teamId,\n        }));\n        if (hasTeams) {\n            return extendedInfos.sort((a, b) => {\n                if (Boolean(a.country) === Boolean(b.country)) {\n                    return a.team - b.team;\n                }\n                return Number(b.country !== undefined) - Number(a.country !== undefined);\n            });\n        }\n        return extendedInfos;\n    }\n    private createLoadingScreen(loadPercent: number): void {\n        const [uiObject] = this.jsxRenderer.render(jsx(LoadingScreenWrapper, {\n            ref: (ref: any) => (this.loadingScreen = ref),\n            strings: this.strings,\n            rules: this.rules,\n            viewport: this.uiScene.menuViewport,\n            playerName: undefined,\n            mapName: this.mapName!,\n            playerInfos: this.createExtendedLoadingInfos(loadPercent),\n            gameResConfig: this.gameResConfig,\n        }));\n        this.uiScene.add(uiObject);\n        this.disposables.add(uiObject, () => this.uiScene.remove(uiObject), () => (this.loadingScreen = undefined));\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n    updateViewport(): void {\n        this.loadingScreen?.updateViewport(this.uiScene.menuViewport);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/loadingScreen/SpLoadingScreenApi.ts",
    "content": "import { jsx } from '@/gui/jsx/jsx';\nimport { OBS_COUNTRY_ID } from '@/game/gameopts/constants';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { PlayerConnectionStatus } from '@/network/gamestate/PlayerConnectionStatus';\nimport { LoadingScreenWrapper } from './LoadingScreenWrapper';\nimport { LoadingScreenApi } from './LoadingScreenApi';\ninterface Player {\n    name: string;\n    countryId: number;\n    colorId: number;\n    teamId: number;\n}\ninterface Country {\n    name: string;\n    side: any;\n    uiName: string;\n}\ninterface Rules {\n    getMultiplayerColors(): Map<number, any>;\n    getMultiplayerCountries(): Country[];\n    colors: Map<string, any>;\n}\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface UiScene {\n    menuViewport: any;\n    add(object: any): void;\n    remove(object: any): void;\n}\ninterface JsxRenderer {\n    render(element: any): any[];\n}\ninterface GameResConfig {\n    isCdn(): boolean;\n    getCdnBaseUrl(): string;\n}\ninterface ExtendedPlayerInfo {\n    name: string;\n    status: PlayerConnectionStatus;\n    loadPercent: number;\n    country: Country;\n    color: string;\n    team: number;\n}\nexport class SpLoadingScreenApi implements LoadingScreenApi {\n    private lastLoadPercent = 0;\n    private disposables = new CompositeDisposable();\n    private players?: Player[];\n    private localPlayerName?: string;\n    private mapName?: string;\n    private loadingScreen?: any;\n    private lastRenderTime?: number;\n    private handleLoadInfoUpdate = (loadPercent: number) => {\n        if (this.loadingScreen) {\n            const now = performance.now();\n            if (!this.lastRenderTime || now - this.lastRenderTime > 1000 / 15) {\n                this.lastRenderTime = now;\n                this.loadingScreen.applyOptions((options: any) => {\n                    options.playerInfos = this.createExtendedLoadingInfos(loadPercent);\n                });\n            }\n        }\n        else {\n            this.createLoadingScreen();\n        }\n    };\n    constructor(private rules: Rules, private strings: Strings, private uiScene: UiScene, private jsxRenderer: JsxRenderer, private gameResConfig: GameResConfig) { }\n    async start(players: Player[], mapName: string, localPlayerName: string): Promise<void> {\n        this.players = players;\n        this.localPlayerName = localPlayerName;\n        this.mapName = mapName;\n        this.handleLoadInfoUpdate(0);\n    }\n    onLoadProgress(percent: number): void {\n        const roundedPercent = Math.floor(percent);\n        if (roundedPercent > this.lastLoadPercent) {\n            this.lastLoadPercent = roundedPercent;\n            this.handleLoadInfoUpdate(roundedPercent);\n        }\n    }\n    private createExtendedLoadingInfos(loadPercent: number): ExtendedPlayerInfo[] {\n        const colors = [...this.rules.getMultiplayerColors().values()];\n        const countries = this.rules.getMultiplayerCountries();\n        const localPlayer = this.players!.find(player => player.name === this.localPlayerName)!;\n        return [\n            {\n                name: this.localPlayerName!,\n                status: PlayerConnectionStatus.Connected,\n                loadPercent,\n                country: countries[localPlayer.countryId],\n                color: localPlayer.countryId === OBS_COUNTRY_ID\n                    ? \"#fff\"\n                    : colors[localPlayer.colorId].asHexString(),\n                team: localPlayer.teamId,\n            },\n        ];\n    }\n    private createLoadingScreen(): void {\n        const [uiObject] = this.jsxRenderer.render(jsx(LoadingScreenWrapper, {\n            ref: (ref: any) => (this.loadingScreen = ref),\n            strings: this.strings,\n            rules: this.rules,\n            viewport: this.uiScene.menuViewport,\n            playerName: this.localPlayerName,\n            mapName: this.mapName!,\n            playerInfos: this.createExtendedLoadingInfos(0),\n            gameResConfig: this.gameResConfig,\n        }));\n        this.uiScene.add(uiObject);\n        this.disposables.add(uiObject, () => this.uiScene.remove(uiObject));\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n    updateViewport(): void {\n        this.loadingScreen?.updateViewport(this.uiScene.menuViewport);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/ArrowScrollHandler.ts",
    "content": "import * as THREE from 'three';\nexport class ArrowScrollHandler {\n    private isPaused = false;\n    private readonly scrollDir = new THREE.Vector2();\n    private readonly pressedKeys = new Set<string>();\n    constructor(private readonly mapScrollHandler: any) { }\n    handleKeyDown(event: KeyboardEvent): void {\n        if (this.isPaused || !['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {\n            return;\n        }\n        event.preventDefault();\n        event.stopPropagation();\n        if (event.repeat) {\n            return;\n        }\n        this.pressedKeys.add(event.key);\n        this.updateScrollDir();\n        this.mapScrollHandler.requestForceScroll(this.scrollDir);\n    }\n    handleKeyUp(event: KeyboardEvent): void {\n        if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {\n            return;\n        }\n        event.preventDefault();\n        event.stopPropagation();\n        this.pressedKeys.delete(event.key);\n        this.updateScrollDir();\n        if (!this.scrollDir.length()) {\n            this.mapScrollHandler.cancelForceScroll();\n        }\n    }\n    cancel(): void {\n        this.pressedKeys.clear();\n        this.updateScrollDir();\n        if (!this.scrollDir.length()) {\n            this.mapScrollHandler.cancelForceScroll();\n        }\n    }\n    pause(): void {\n        this.isPaused = true;\n    }\n    unpause(): void {\n        this.isPaused = false;\n    }\n    private updateScrollDir(): void {\n        this.scrollDir.set(0, 0);\n        for (const key of this.pressedKeys) {\n            switch (key) {\n                case 'ArrowUp':\n                    this.scrollDir.y -= 1;\n                    break;\n                case 'ArrowDown':\n                    this.scrollDir.y += 1;\n                    break;\n                case 'ArrowLeft':\n                    this.scrollDir.x -= 1;\n                    break;\n                case 'ArrowRight':\n                    this.scrollDir.x += 1;\n                    break;\n                default:\n                    throw new Error(`Unhandled arrow key \"${key}\"`);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/BeaconMode.ts",
    "content": "import { PointerType } from '@/engine/type/PointerType';\nimport { EventDispatcher } from '@/util/event';\nexport class BeaconMode {\n    private readonly _onExecute = new EventDispatcher<BeaconMode, any>();\n    private currentTile?: any;\n    private lastTile?: any;\n    private lastUpdate?: number;\n    get onExecute() {\n        return this._onExecute.asEvent();\n    }\n    static factory(pointer: any, renderer: any): BeaconMode {\n        return new BeaconMode(pointer, renderer);\n    }\n    constructor(private readonly pointer: any, private readonly renderer: any) { }\n    private readonly onFrame = (time: number): void => {\n        if (this.lastTile === this.currentTile && this.lastUpdate !== undefined && time - this.lastUpdate < 1000 / 15) {\n            return;\n        }\n        this.lastTile = this.currentTile;\n        this.lastUpdate = time;\n        this.pointer.setPointerType(this.currentTile ? PointerType.Beacon : PointerType.Default);\n    };\n    enter(): void {\n        this.currentTile = undefined;\n        this.lastTile = undefined;\n        this.lastUpdate = undefined;\n        this.renderer.onFrame.subscribe(this.onFrame);\n    }\n    hover(hover: any, minimap: boolean): void {\n        if (!minimap) {\n            this.currentTile = hover?.tile;\n        }\n    }\n    execute(hover: any, minimap: boolean): false | void {\n        if (minimap) {\n            return false;\n        }\n        const tile = hover?.tile;\n        if (!tile) {\n            return false;\n        }\n        this._onExecute.dispatch(this, tile);\n        this.end();\n        return;\n    }\n    cancel(): void {\n        this.end();\n    }\n    private end(): void {\n        this.renderer.onFrame.unsubscribe(this.onFrame);\n    }\n    dispose(): void {\n        this.end();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/CameraPanHandler.ts",
    "content": "import * as THREE from 'three';\nimport { pointEquals } from '@/util/geometry';\nimport { PointerType } from '@/engine/type/PointerType';\nimport { clamp } from '@/util/math';\nexport class CameraPanHandler {\n    private startPos?: {\n        x: number;\n        y: number;\n    };\n    private initialPan?: {\n        x: number;\n        y: number;\n    };\n    private readonly panVector = new THREE.Vector2();\n    private isPanning = false;\n    private paused = false;\n    private stickyMode = false;\n    private lastUpdate?: number;\n    constructor(private readonly cameraPan: any, private readonly pointer: any, private readonly panRate: any, private readonly freeCamera: any, private readonly worldScene: any) { }\n    private readonly onFrame = (time: number): void => {\n        if (this.paused ||\n            !this.isPanning ||\n            (this.lastUpdate !== undefined && time - this.lastUpdate < 1000 / 60)) {\n            return;\n        }\n        this.lastUpdate = time;\n        if (!this.panVector.x && !this.panVector.y) {\n            this.pointer.setPointerType(PointerType.Pan);\n            return;\n        }\n        const currentPan = this.stickyMode ? this.initialPan! : this.cameraPan.getPan();\n        const panLimits = this.cameraPan.getPanLimits();\n        let nextPan = {\n            x: clamp(currentPan.x + this.panVector.x, panLimits.x, panLimits.x + panLimits.width),\n            y: clamp(currentPan.y + this.panVector.y, panLimits.y, panLimits.y + panLimits.height),\n        };\n        if (this.freeCamera.value) {\n            nextPan = {\n                x: currentPan.x + this.panVector.x,\n                y: currentPan.y + this.panVector.y,\n            };\n        }\n        const panChanged = !pointEquals(nextPan, currentPan);\n        const blockedX = this.panVector.x && nextPan.x === currentPan.x;\n        const blockedY = this.panVector.y && nextPan.y === currentPan.y;\n        let subFrame = 0;\n        if (blockedX || blockedY) {\n            const blocked = new THREE.Vector2(blockedX ? Math.sign(this.panVector.x) : 0, blockedY ? Math.sign(this.panVector.y) : 0);\n            subFrame = 1 + ((THREE.MathUtils.radToDeg(blocked.angle()) + 90) % 360) / 45;\n        }\n        this.pointer.setPointerType(PointerType.Pan, subFrame);\n        if (panChanged) {\n            this.cameraPan.setPan(nextPan);\n        }\n        this.isPanning = panChanged;\n    };\n    start(pointer: {\n        x: number;\n        y: number;\n    }): void {\n        this.startPos = pointer;\n        this.initialPan = undefined;\n        this.isPanning = false;\n        this.panVector.set(0, 0);\n        this.worldScene.onBeforeCameraUpdate.subscribe(this.onFrame);\n    }\n    update(pointer: {\n        x: number;\n        y: number;\n    }, sticky: boolean): void {\n        if (!this.startPos) {\n            return;\n        }\n        if (sticky) {\n            this.initialPan ||= this.cameraPan.getPan();\n            this.panVector.x = this.startPos.x - pointer.x;\n            this.panVector.y = this.startPos.y - pointer.y;\n        }\n        else {\n            const rate = (this.panRate.value / 5) * 100;\n            this.panVector.x = Math.floor((rate * clamp(pointer.x - this.startPos.x, -600, 600)) / 600);\n            this.panVector.y = Math.floor((rate * clamp(pointer.y - this.startPos.y, -600, 600)) / 600);\n        }\n        this.isPanning = true;\n        this.stickyMode = sticky;\n    }\n    finish(): void {\n        this.worldScene.onBeforeCameraUpdate.unsubscribe(this.onFrame);\n        this.pointer.setPointerType(PointerType.Default);\n        this.initialPan = undefined;\n        this.startPos = undefined;\n        this.isPanning = false;\n        this.panVector.set(0, 0);\n    }\n    setPaused(paused: boolean): void {\n        this.paused = paused;\n    }\n    dispose(): void {\n        this.finish();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/CustomScrollHandler.ts",
    "content": "export class CustomScrollHandler {\n    private isPaused = false;\n    constructor(private readonly mapScrollHandler: any) { }\n    requestScroll(direction: any): void {\n        if (!this.isPaused) {\n            this.mapScrollHandler.requestForceScroll(direction);\n        }\n    }\n    cancel(): void {\n        this.mapScrollHandler.cancelForceScroll();\n    }\n    pause(): void {\n        this.isPaused = true;\n    }\n    unpause(): void {\n        this.isPaused = false;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/DefaultActionHandler.ts",
    "content": "import { PointerType } from '@/engine/type/PointerType';\nimport { Coords } from '@/game/Coords';\nimport { isNotNullOrUndefined } from '@/util/typeGuard';\nimport { EventDispatcher } from '@/util/event';\nimport { MoveOrder } from '@/game/order/MoveOrder';\nimport { orderPriorities } from '@/game/order/orderPriorities';\nimport { OrderFactory } from '@/game/order/OrderFactory';\nimport { AttackOrder } from '@/game/order/AttackOrder';\nimport { Target } from '@/game/Target';\nimport { AttackMoveOrder } from '@/game/order/AttackMoveOrder';\nimport { OrderFeedbackType } from '@/game/order/OrderFeedbackType';\nimport { GuardAreaOrder } from '@/game/order/GuardAreaOrder';\nclass SelectAction {\n    private force = false;\n    private allowTypeSelect = false;\n    constructor(private readonly game: any, private readonly unitSelectionHandler: any, private readonly currentPlayer: any, private readonly toggleSelect: boolean = false) { }\n    setForce(force: boolean): this {\n        this.force = force;\n        return this;\n    }\n    setTypeSelect(allowTypeSelect: boolean): this {\n        this.allowTypeSelect = allowTypeSelect;\n        return this;\n    }\n    getPointerType(): PointerType {\n        return PointerType.Select;\n    }\n    isAllowed(): boolean {\n        return true;\n    }\n    isValidTarget(target: any): boolean {\n        if (!target?.isTechno?.()) {\n            return false;\n        }\n        if (this.currentPlayer &&\n            (target.isInfantry?.() || target.isVehicle?.()) &&\n            target.disguiseTrait?.hasTerrainDisguise?.() &&\n            !this.game.alliances.haveSharedIntel(this.currentPlayer, target.owner)) {\n            return false;\n        }\n        const selected = this.unitSelectionHandler.getSelectedUnits();\n        const targetAlreadySelected = selected.includes(target);\n        const canCollapseMultipleSelection = !this.toggleSelect &&\n            targetAlreadySelected &&\n            selected.length > 1 &&\n            selected.every((unit: any) => unit.owner === target.owner);\n        if (!this.toggleSelect &&\n            selected.some((unit: any) => unit.isUnit?.()) &&\n            this.currentPlayer &&\n            !this.currentPlayer.isObserver &&\n            target.isTechno?.() &&\n            !this.game.areFriendly(target, selected[0]) &&\n            selected[0].owner === this.currentPlayer) {\n            return false;\n        }\n        return (target.rules.selectable &&\n            (this.toggleSelect ||\n                this.force ||\n                (this.allowTypeSelect && selected.length === 1 && selected[0] === target) ||\n                !targetAlreadySelected ||\n                canCollapseMultipleSelection));\n    }\n    execute(target: any): void {\n        if (this.allowTypeSelect) {\n            const selected = this.unitSelectionHandler.getSelectedUnits();\n            if (selected.length === 1 && selected[0] === target) {\n                this.unitSelectionHandler.selectByType();\n                return;\n            }\n        }\n        if (this.toggleSelect) {\n            this.unitSelectionHandler.toggleSelection(target);\n        }\n        else {\n            this.unitSelectionHandler.selectSingleUnit(target);\n        }\n    }\n}\nexport enum ActionFilter {\n    All = 0,\n    SelectOnly = 1,\n    NoSelect = 2\n}\nexport class DefaultActionHandler {\n    private readonly _onOrder = new EventDispatcher<any, any>();\n    private selectAction!: SelectAction;\n    private selectToggleAction?: SelectAction;\n    private forceMoveAction?: any;\n    private forceAttackAction?: any;\n    private attackMoveAction?: any;\n    private guardAreaAction?: any;\n    private defaultActions: any[] = [];\n    private specialActions: any[] = [];\n    private currentTarget?: any;\n    private currentSelected?: any[];\n    private mostSignificantAction?: any;\n    get onOrder() {\n        return this._onOrder.asEvent();\n    }\n    static factory(renderableManager: any, unitSelection: any, unitSelectionHandler: any, currentPlayer: any, map: any, game: any, audioVisualRules: any): DefaultActionHandler {\n        const handler = new DefaultActionHandler(renderableManager, currentPlayer, audioVisualRules, map.tileOccupation);\n        const selectAction = new SelectAction(game, unitSelectionHandler, currentPlayer);\n        handler.selectAction = selectAction;\n        if (currentPlayer && !currentPlayer.isObserver) {\n            handler.defaultActions = [\n                ...orderPriorities.map((orderType) => new OrderFactory(game, map).create(orderType, unitSelection)),\n                selectAction,\n                new MoveOrder(game, map, unitSelection),\n            ];\n            handler.selectToggleAction = new SelectAction(game, unitSelectionHandler, currentPlayer, true);\n            handler.forceMoveAction = new MoveOrder(game, map, unitSelection, true);\n            handler.forceAttackAction = new AttackOrder(game, { forceAttack: true });\n            handler.attackMoveAction = new AttackMoveOrder(game, map);\n            handler.guardAreaAction = new GuardAreaOrder(game, true);\n            handler.specialActions = [\n                handler.selectToggleAction,\n                handler.forceMoveAction,\n                handler.forceAttackAction,\n                handler.attackMoveAction,\n                handler.guardAreaAction,\n            ];\n        }\n        else {\n            handler.defaultActions = [selectAction];\n            handler.specialActions = [];\n        }\n        return handler;\n    }\n    constructor(private readonly renderableManager: any, private readonly currentPlayer: any, private readonly audioVisualRules: any, private readonly tileOccupation: any) { }\n    private createOrderTarget(hover: any): Target {\n        return new Target(hover?.gameObject, hover?.tile, this.tileOccupation);\n    }\n    private getDefaultAction(sourceObject: any, selected: any[], hover: any, target: any, filter: ActionFilter, force: boolean, allowTypeSelect: boolean, keyboardEvent: any, minimap: boolean): any {\n        const hoveredObject = hover.gameObject;\n        const selectAction = this.selectAction.setForce(force).setTypeSelect(false);\n        if (!sourceObject || sourceObject.owner !== this.currentPlayer || sourceObject.rules.spawned) {\n            return !minimap && filter !== ActionFilter.NoSelect && selectAction.isValidTarget(hoveredObject)\n                ? selectAction\n                : undefined;\n        }\n        if (filter !== ActionFilter.NoSelect &&\n            !minimap &&\n            keyboardEvent?.shiftKey &&\n            !keyboardEvent?.ctrlKey &&\n            this.selectToggleAction?.isValidTarget(hoveredObject)) {\n            return this.selectToggleAction;\n        }\n        if (filter === ActionFilter.SelectOnly) {\n            return !minimap && selectAction.setTypeSelect(allowTypeSelect).isValidTarget(hoveredObject)\n                ? selectAction\n                : undefined;\n        }\n        const allWarpedOut = selected.every((unit) => unit.warpedOutTrait?.isActive?.());\n        if (keyboardEvent?.ctrlKey && !allWarpedOut) {\n            if (keyboardEvent.shiftKey) {\n                if (this.attackMoveAction?.set(sourceObject, target).isValid()) {\n                    return this.attackMoveAction;\n                }\n            }\n            else if (keyboardEvent.altKey) {\n                if (this.guardAreaAction?.set(sourceObject, target).isValid()) {\n                    return this.guardAreaAction;\n                }\n            }\n            else if (this.forceAttackAction?.set(sourceObject, target).isValid()) {\n                return this.forceAttackAction;\n            }\n        }\n        if (keyboardEvent?.altKey && !allWarpedOut && this.forceMoveAction?.set(sourceObject, target).isValid()) {\n            return this.forceMoveAction;\n        }\n        for (const action of this.defaultActions) {\n            if (action instanceof SelectAction) {\n                if (filter !== ActionFilter.NoSelect && !minimap && action.setForce(force).setTypeSelect(false).isValidTarget(hoveredObject)) {\n                    return action;\n                }\n            }\n            else if (!allWarpedOut &&\n                (!minimap || action.minimapAllowed) &&\n                !(action.singleSelectionRequired && selected.length > 1) &&\n                action.set(sourceObject, target).isValid()) {\n                return action;\n            }\n        }\n        if (minimap && !allWarpedOut && this.forceMoveAction?.set(sourceObject, target).isValid()) {\n            return this.forceMoveAction;\n        }\n        return undefined;\n    }\n    private updateMostSignificantAction(selected: any[], hover: any, target: any, filter: ActionFilter, force: boolean, allowTypeSelect: boolean, keyboardEvent: any, minimap: boolean): any {\n        if (!selected.length) {\n            return this.getDefaultAction(undefined, selected, hover, target, filter, force, allowTypeSelect, keyboardEvent, minimap);\n        }\n        const actions = selected\n            .map((unit) => {\n            const action = this.getDefaultAction(unit, selected, hover, target, filter, force, allowTypeSelect, keyboardEvent, minimap);\n            if (action) {\n                return { unit, action };\n            }\n            return undefined;\n        })\n            .filter(isNotNullOrUndefined);\n        const specialActions = [...this.specialActions.values()];\n        if (!actions.length) {\n            return undefined;\n        }\n        return actions.reduce((best: any, entry: any) => {\n            if (!best) {\n                return entry.action instanceof SelectAction ? entry.action : entry.action.set(entry.unit, target);\n            }\n            const bestIndex = this.defaultActions.indexOf(best);\n            const currentIndex = this.defaultActions.indexOf(entry.action);\n            const currentBeatsBest = specialActions.includes(entry.action) ||\n                currentIndex < bestIndex ||\n                (!(best instanceof SelectAction) &&\n                    best.sourceObject?.rules?.leadershipRating < entry.unit.rules.leadershipRating &&\n                    currentIndex === bestIndex);\n            return currentBeatsBest\n                ? entry.action instanceof SelectAction\n                    ? entry.action\n                    : entry.action.set(entry.unit, target)\n                : best;\n        }, undefined);\n    }\n    getPointerType(minimap: boolean): PointerType {\n        if (this.mostSignificantAction instanceof SelectAction) {\n            return this.mostSignificantAction.getPointerType();\n        }\n        if (!this.currentSelected || !this.mostSignificantAction) {\n            return minimap ? PointerType.Mini : PointerType.Default;\n        }\n        if (!this.mostSignificantAction.isAllowed()) {\n            const sourceObject = this.mostSignificantAction.sourceObject;\n            for (const unit of this.currentSelected) {\n                this.mostSignificantAction.set(unit, this.currentTarget);\n                if (this.mostSignificantAction.isValid() && this.mostSignificantAction.isAllowed()) {\n                    return this.mostSignificantAction.getPointerType(minimap, this.currentSelected);\n                }\n            }\n            this.mostSignificantAction.set(sourceObject, this.currentTarget);\n        }\n        return this.mostSignificantAction.getPointerType(minimap, this.currentSelected);\n    }\n    update(hover: any, selected: any[], rightClickMove: boolean, keyboardEvent: any, minimap: boolean = false): void {\n        const target = (this.currentTarget = this.createOrderTarget(hover));\n        this.currentSelected = selected;\n        this.mostSignificantAction = this.updateMostSignificantAction(selected, hover, target, ActionFilter.All, rightClickMove, false, keyboardEvent, minimap);\n    }\n    execute(hover: any, selected: any[], filter: ActionFilter, force: boolean, allowTypeSelect: boolean, keyboardEvent: any, minimap: boolean = false): boolean {\n        const target = (this.currentTarget = this.createOrderTarget(hover));\n        this.currentSelected = selected;\n        this.mostSignificantAction = this.updateMostSignificantAction(selected, hover, target, filter, force, allowTypeSelect, keyboardEvent, minimap);\n        if (!this.mostSignificantAction) {\n            return false;\n        }\n        const allowed = this.mostSignificantAction.isAllowed();\n        if (allowed) {\n            if (this.mostSignificantAction instanceof MoveOrder ||\n                (this.mostSignificantAction instanceof AttackMoveOrder && !target.obj?.isTechno?.()) ||\n                this.mostSignificantAction instanceof GuardAreaOrder) {\n                this.renderableManager.createTransientAnim(this.audioVisualRules.moveFlash, (renderable: any) => {\n                    renderable.setPosition(Coords.tile3dToWorld(target.tile.rx + 0.5, target.tile.ry + 0.5, target.tile.z + (target.getBridge()?.tileElevation ?? 0)));\n                });\n            }\n            else if (!(this.mostSignificantAction instanceof SelectAction) && !selected.includes(hover.gameObject)) {\n                hover.entity?.highlight?.();\n            }\n        }\n        if (this.mostSignificantAction instanceof SelectAction) {\n            this.mostSignificantAction.execute(hover.gameObject);\n        }\n        else {\n            this._onOrder.dispatch(this, {\n                orderType: this.mostSignificantAction.orderType,\n                terminal: this.mostSignificantAction.terminal,\n                feedbackType: allowed ? this.mostSignificantAction.feedbackType : OrderFeedbackType.None,\n                feedbackUnit: allowed ? this.mostSignificantAction.sourceObject : undefined,\n                target,\n            });\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/InteractionMode.ts",
    "content": "export abstract class InteractionMode {\n    protected active = false;\n    abstract enter(): void;\n    abstract exit(): void;\n    abstract handleClick(x: number, y: number, target?: any): void;\n    isActive(): boolean {\n        return this.active;\n    }\n    dispose(): void {\n        if (this.active) {\n            this.exit();\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/MapHoverHandler.ts",
    "content": "import * as THREE from 'three';\nimport { EventDispatcher } from '@/util/event';\nimport { Coords } from '@/game/Coords';\nexport class MapHoverHandler {\n    private readonly _onHoverChange = new EventDispatcher<any, any>();\n    private isActive = false;\n    private needsUpdate = false;\n    private lastUpdate?: number;\n    private lastPointerPos?: {\n        x: number;\n        y: number;\n    };\n    private currentHoverEntity?: any;\n    private currentHoverTile?: any;\n    constructor(private readonly entityIntersectHelper: any, private readonly mapTileIntersectHelper: any, private readonly map: any, private shroud: any, private readonly renderer: any) { }\n    get onHoverChange() {\n        return this._onHoverChange.asEvent();\n    }\n    getCurrentHover(): any {\n        if (!this.currentHoverTile) {\n            return undefined;\n        }\n        if (this.currentHoverEntity?.gameObject?.isDestroyed || this.currentHoverEntity?.gameObject?.isCrashing) {\n            return {\n                entity: undefined,\n                gameObject: undefined,\n                tile: this.currentHoverTile,\n            };\n        }\n        return {\n            entity: this.currentHoverEntity,\n            gameObject: this.currentHoverEntity?.gameObject,\n            tile: this.currentHoverTile,\n        };\n    }\n    setShroud(shroud: any): void {\n        this.shroud = shroud;\n    }\n    update(pointer: {\n        x: number;\n        y: number;\n    }, immediate: boolean = false): void {\n        this.lastPointerPos = pointer;\n        if (immediate) {\n            this.doUpdate();\n            return;\n        }\n        if (!this.isActive) {\n            this.isActive = true;\n            this.needsUpdate = true;\n            this.renderer.onFrame.subscribe(this.onFrame);\n        }\n        else {\n            this.needsUpdate = true;\n        }\n    }\n    private readonly onFrame = (time: number): void => {\n        if (!this.isActive ||\n            (!this.needsUpdate && this.lastUpdate !== undefined && time - this.lastUpdate < 1000 / 15)) {\n            return;\n        }\n        this.needsUpdate = false;\n        this.lastUpdate = time;\n        this.doUpdate();\n    };\n    private doUpdate(): void {\n        if (!this.lastPointerPos) {\n            return;\n        }\n        const previousEntity = this.currentHoverEntity;\n        const previousTile = this.currentHoverTile;\n        const intersection = this.entityIntersectHelper.getEntityAtScreenPoint(this.lastPointerPos);\n        if (intersection) {\n            this.currentHoverEntity = intersection.renderable;\n            let tile: any;\n            const gameObject = intersection.renderable.gameObject;\n            const foundation = gameObject.getFoundation?.();\n            if (gameObject.isBuilding?.() && foundation && (foundation.width > 1 || foundation.height > 1)) {\n                tile = this.mapTileIntersectHelper.getTileAtScreenPoint(this.lastPointerPos);\n            }\n            else if (gameObject.isTechno?.() && !gameObject.art?.isVoxel) {\n                tile = gameObject.tile;\n            }\n            else {\n                const mapCoords = new THREE.Vector2(intersection.point.x, intersection.point.z)\n                    .multiplyScalar(1 / Coords.LEPTONS_PER_TILE)\n                    .floor();\n                tile = this.map.tiles.getByMapCoords(mapCoords.x, mapCoords.y);\n                if (!tile) {\n                    console.warn(`[MapHoverHandler] No tile exists at rx,ry=${JSON.stringify(mapCoords)}. Falling back to object tile.`);\n                }\n                tile = tile ?? gameObject.tile;\n            }\n            const bridge = this.map.tileOccupation.getBridgeOnTile(tile);\n            if (this.currentHoverEntity.gameObject.isOverlay?.() && this.currentHoverEntity.gameObject.isBridge?.() && !bridge) {\n                this.currentHoverEntity = undefined;\n            }\n            this.currentHoverTile = tile;\n        }\n        else {\n            this.currentHoverEntity = undefined;\n            this.currentHoverTile = this.mapTileIntersectHelper.getTileAtScreenPoint(this.lastPointerPos);\n        }\n        if (this.shroud &&\n            this.currentHoverTile &&\n            this.shroud.isShrouded(this.currentHoverTile, this.currentHoverEntity?.gameObject?.tileElevation) &&\n            !(this.currentHoverEntity?.gameObject?.isOverlay?.() && this.currentHoverEntity?.gameObject?.isBridge?.())) {\n            this.currentHoverEntity = undefined;\n        }\n        if (this.currentHoverEntity === previousEntity && this.currentHoverTile === previousTile) {\n            return;\n        }\n        previousEntity?.selectionModel?.setHover(false);\n        this.currentHoverEntity?.selectionModel?.setHover(true);\n        if (this.currentHoverTile) {\n            this._onHoverChange.dispatch(this, {\n                entity: this.currentHoverEntity,\n                gameObject: this.currentHoverEntity?.gameObject,\n                tile: this.currentHoverTile,\n            });\n        }\n    }\n    finish(): void {\n        this.currentHoverEntity?.selectionModel?.setHover(false);\n        this.currentHoverEntity = undefined;\n        this.currentHoverTile = undefined;\n        if (this.isActive) {\n            this.renderer.onFrame.unsubscribe(this.onFrame);\n            this.isActive = false;\n            this.needsUpdate = false;\n        }\n    }\n    dispose(): void {\n        this.finish();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/MapScrollHandler.ts",
    "content": "import * as THREE from 'three';\nimport { pointEquals } from '@/util/geometry';\nimport { clamp } from '@/util/math';\nimport { PointerType } from '@/engine/type/PointerType';\nexport class MapScrollHandler {\n    private isActive = false;\n    private paused = false;\n    private forceScrollCancelRequested = false;\n    private panDirection?: THREE.Vector2;\n    private pointerFrameNo = 0;\n    private forceScrollDirection?: THREE.Vector2;\n    private lastUpdate?: number;\n    constructor(private readonly canvas: HTMLCanvasElement, private readonly cameraPan: any, private readonly pointer: any, private readonly scrollRate: any, private readonly worldScene: any) { }\n    private readonly onFrame = (time: number): void => {\n        if (this.paused ||\n            !this.isActive ||\n            (this.lastUpdate !== undefined && time - this.lastUpdate < 1000 / 60)) {\n            return;\n        }\n        this.lastUpdate = time;\n        const currentPan = this.cameraPan.getPan();\n        const panLimits = this.cameraPan.getPanLimits();\n        let nextPan: {\n            x: number;\n            y: number;\n        } | undefined;\n        let keepActive = false;\n        if (this.panDirection?.x || this.panDirection?.y) {\n            const rate = (this.scrollRate.value / 5) * 10;\n            nextPan = {\n                x: clamp(currentPan.x + this.panDirection.x * rate, panLimits.x, panLimits.x + panLimits.width),\n                y: clamp(currentPan.y + this.panDirection.y * rate, panLimits.y, panLimits.y + panLimits.height),\n            };\n            const moved = !pointEquals(nextPan, currentPan);\n            this.pointer.setPointerType(moved ? PointerType.Scroll : PointerType.NoScroll, this.pointerFrameNo);\n            if (moved) {\n                keepActive = true;\n            }\n        }\n        if (this.forceScrollDirection) {\n            nextPan = {\n                x: clamp(currentPan.x + 30 * this.forceScrollDirection.x, panLimits.x, panLimits.x + panLimits.width),\n                y: clamp(currentPan.y + 30 * this.forceScrollDirection.y, panLimits.y, panLimits.y + panLimits.height),\n            };\n            if (!pointEquals(nextPan, currentPan)) {\n                keepActive = true;\n            }\n        }\n        this.isActive = keepActive;\n        if (nextPan) {\n            this.cameraPan.setPan(nextPan);\n        }\n        if (!this.isActive) {\n            this.worldScene.onBeforeCameraUpdate.unsubscribe(this.onFrame);\n        }\n        if (this.forceScrollCancelRequested) {\n            this.forceScrollCancelRequested = false;\n            this.forceScrollDirection = undefined;\n        }\n    };\n    isScrolling(): boolean {\n        return !!this.panDirection && (!!this.panDirection.x || !!this.panDirection.y);\n    }\n    requestForceScroll(direction: THREE.Vector2): void {\n        this.forceScrollDirection = direction.clone?.() ?? new THREE.Vector2(direction.x, direction.y);\n        this.forceScrollCancelRequested = false;\n        if (!this.isActive) {\n            this.isActive = true;\n            this.worldScene.onBeforeCameraUpdate.subscribe(this.onFrame);\n        }\n    }\n    cancelForceScroll(): void {\n        this.forceScrollCancelRequested = true;\n    }\n    update(pointer: {\n        x: number;\n        y: number;\n    }): void {\n        const height = this.canvas.height;\n        const width = this.canvas.width;\n        let directionX = pointer.x < 3 ? -1 : pointer.x > width - 4 ? 1 : 0;\n        let directionY = pointer.y < 3 ? -1 : pointer.y > height - 4 ? 1 : 0;\n        if (directionX) {\n            if (pointer.y < Math.min(300, height / 3)) {\n                directionY = -1;\n            }\n            else if (pointer.y > Math.max(height - 300, (2 * height) / 3)) {\n                directionY = 1;\n            }\n        }\n        else if (directionY) {\n            if (pointer.x < Math.min(300, width / 3)) {\n                directionX = -1;\n            }\n            else if (pointer.x > Math.max(width - 300, (2 * width) / 3)) {\n                directionX = 1;\n            }\n        }\n        this.panDirection = new THREE.Vector2(directionX, directionY);\n        this.pointerFrameNo = ((THREE.MathUtils.radToDeg(this.panDirection.angle()) + 90) % 360) / 45;\n        if (!this.isActive) {\n            this.isActive = true;\n            this.worldScene.onBeforeCameraUpdate.subscribe(this.onFrame);\n        }\n    }\n    cancel(): void {\n        this.cancelForceScroll();\n        if (this.isActive) {\n            this.worldScene.onBeforeCameraUpdate.unsubscribe(this.onFrame);\n            this.isActive = false;\n        }\n    }\n    setPaused(paused: boolean): void {\n        this.paused = paused;\n    }\n    dispose(): void {\n        this.cancel();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/MinimapHandler.ts",
    "content": "import { IsoCoords } from '@/engine/IsoCoords';\n\nexport class MinimapHandler {\n    constructor(public readonly minimap: any, private readonly map: any, private shroud: any, private readonly worldScene: any, private readonly mapPanningHelper: any) { }\n    setShroud(shroud: any): void {\n        this.shroud = shroud;\n    }\n    panToTile(tile: any): void {\n        this.worldScene.cameraPan.setPan(this.mapPanningHelper.computeCameraPanFromTile(tile.rx, tile.ry));\n    }\n    isTileWithinViewport(tile: any, padding: number = 2): boolean {\n        if (!tile) {\n            return false;\n        }\n        const pan = this.worldScene.cameraPan.getPan();\n        const viewport = this.worldScene.viewport;\n        const zoom = this.worldScene.cameraZoom?.getZoom?.() ?? 1;\n        const origin = this.mapPanningHelper.getScreenPanOrigin();\n        const visibleRect = {\n            x: origin.x + pan.x - viewport.width / (2 * zoom),\n            y: origin.y + pan.y - viewport.height / (2 * zoom),\n            width: viewport.width / zoom,\n            height: viewport.height / zoom,\n        };\n        const topLeft = IsoCoords.screenToScreenTile(visibleRect.x, visibleRect.y);\n        const bottomRight = IsoCoords.screenToScreenTile(\n            visibleRect.x + visibleRect.width,\n            visibleRect.y + visibleRect.height,\n        );\n        return tile.dx >= topLeft.x - padding &&\n            tile.dx <= bottomRight.x + padding &&\n            tile.dy >= topLeft.y - padding &&\n            tile.dy <= bottomRight.y + padding;\n    }\n    getHover(tile: any): any {\n        return {\n            entity: undefined,\n            gameObject: this.shroud?.isShrouded(tile)\n                ? undefined\n                : this.map\n                    .getObjectsOnTile(tile)\n                    .sort((a: any, b: any) => Number(b.isTechno?.()) - Number(a.isTechno?.()))\n                    .shift(),\n            tile,\n        };\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/PendingPlacementHandler.ts",
    "content": "import { EventType } from '@/game/event/EventType';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { PlacementGrid } from '@/gui/screen/game/worldInteraction/placementMode/PlacementGrid';\nexport class PendingPlacementHandler {\n    private readonly placements: any[] = [];\n    private readonly gridModels = new Map<any, any>();\n    private readonly grids = new Map<any, PlacementGrid>();\n    private readonly disposables = new CompositeDisposable();\n    static factory(game: any, player: any, renderer: any, worldScene: any): PendingPlacementHandler {\n        const constructionWorker = game.getConstructionWorker(player);\n        return new PendingPlacementHandler(game, constructionWorker, renderer, worldScene);\n    }\n    constructor(private readonly game: any, private readonly constructionWorker: any, private readonly renderer: any, private readonly worldScene: any) { }\n    private readonly onFrame = (): void => {\n        for (const placement of this.placements) {\n            const gridModel = this.gridModels.get(placement);\n            if (gridModel) {\n                gridModel.tiles = this.constructionWorker.getPlacementPreview(placement.rules.name, placement.tile, {\n                    normalizedTile: true,\n                });\n            }\n        }\n    };\n    pushPlacementInfo(placement: any): void {\n        this.placements.push(placement);\n        this.addGrid(placement);\n    }\n    init(): void {\n        this.renderer.onFrame.subscribe(this.onFrame);\n        this.disposables.add(() => this.renderer.onFrame.unsubscribe(this.onFrame));\n        this.disposables.add(this.game.events.subscribe(EventType.BuildingPlace, (event: any) => {\n            this.removePendingPlacement(event.target.tile);\n        }), this.game.events.subscribe(EventType.BuildingFailedPlace, (event: any) => {\n            this.removePendingPlacement(event.tile);\n        }));\n    }\n    private removePendingPlacement(tile: any): void {\n        const index = this.placements.findIndex((placement) => placement.tile === tile);\n        const placement = this.placements[index];\n        if (index !== -1) {\n            this.placements.splice(index, 1);\n            this.removeGrid(placement);\n        }\n    }\n    private addGrid(placement: any): void {\n        const gridModel = {\n            tiles: this.constructionWorker.getPlacementPreview(placement.rules.name, placement.tile, {\n                normalizedTile: true,\n            }),\n            visible: true,\n            rangeIndicator: undefined,\n            rangeIndicatorColor: undefined,\n            showBusy: true,\n        };\n        const grid = new PlacementGrid(gridModel, this.worldScene.camera, this.game.map.tiles);\n        this.worldScene.add(grid);\n        this.gridModels.set(placement, gridModel);\n        this.grids.set(placement, grid);\n    }\n    private removeGrid(placement: any): void {\n        const grid = this.grids.get(placement);\n        if (!grid) {\n            return;\n        }\n        this.worldScene.remove(grid);\n        grid.dispose();\n        this.grids.delete(placement);\n        this.gridModels.delete(placement);\n    }\n    dispose(): void {\n        for (const placement of [...this.placements]) {\n            this.removeGrid(placement);\n        }\n        this.placements.length = 0;\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/PlacementMode.ts",
    "content": "import { PlacementGrid } from '@/gui/screen/game/worldInteraction/placementMode/PlacementGrid';\nimport { circleIntersect } from '@/util/geometry';\nimport { EventDispatcher } from '@/util/event';\nimport { ObjectType } from '@/engine/type/ObjectType';\nexport class PlacementMode {\n    private defenseMode = false;\n    private readonly buildingRanges = new Map<any, {\n        center: {\n            x: number;\n            y: number;\n        };\n        radius: number;\n    }>();\n    private readonly _onBuildingPlaceRequest = new EventDispatcher<PlacementMode, {\n        rules: any;\n        tile: any;\n    }>();\n    private placementGridModel: any;\n    private currentBuilding?: any;\n    private currentTile?: any;\n    private lastTile?: any;\n    private lastUpdate?: number;\n    private currentRangeCircleRadius?: number;\n    get onBuildingPlaceRequest() {\n        return this._onBuildingPlaceRequest.asEvent();\n    }\n    static factory(game: any, player: any, renderer: any, worldScene: any, eva: any): PlacementMode {\n        const constructionWorker = game.getConstructionWorker(player);\n        const placementGridModel = {\n            tiles: [],\n            visible: false,\n            rangeIndicator: undefined,\n            rangeIndicatorColor: undefined,\n        };\n        const placementGrid = new PlacementGrid(placementGridModel, worldScene.camera, game.map.tiles);\n        const placementMode = new PlacementMode(game, player, constructionWorker, renderer, eva, placementGrid, worldScene);\n        placementMode.placementGridModel = placementGridModel;\n        return placementMode;\n    }\n    constructor(private readonly game: any, private readonly player: any, private readonly constructionWorker: any, private readonly renderer: any, private readonly eva: any, private readonly placementGrid: PlacementGrid, private readonly worldScene: any) { }\n    private readonly onFrame = (time: number): void => {\n        if (this.lastTile === this.currentTile && this.lastUpdate !== undefined && time - this.lastUpdate < 1000 / 15) {\n            return;\n        }\n        this.lastTile = this.currentTile;\n        this.lastUpdate = time;\n        if (this.currentBuilding) {\n            this.updateGridModel(this.currentBuilding.name);\n        }\n    };\n    init(): void {\n        this.worldScene.add(this.placementGrid);\n    }\n    dispose(): void {\n        this.worldScene.remove(this.placementGrid);\n        this.placementGrid.dispose();\n        this.endConstructionMode();\n    }\n    enter(): void {\n        this.currentTile = undefined;\n        this.lastTile = undefined;\n        this.lastUpdate = undefined;\n        this.renderer.onFrame.subscribe(this.onFrame);\n    }\n    setBuilding(buildingRules: any): void {\n        this.currentBuilding = buildingRules;\n        if (buildingRules.primary || buildingRules.hasRadialIndicator) {\n            this.defenseMode = true;\n            this.prepareBuildingRanges(buildingRules);\n        }\n        else {\n            this.defenseMode = false;\n        }\n    }\n    getBuilding(): any {\n        return this.currentBuilding;\n    }\n    hover(hover: any, minimap: boolean): void {\n        if (!minimap && hover?.tile !== this.currentTile) {\n            this.currentTile = hover?.tile;\n        }\n    }\n    private updateGridModel(buildingName: string): void {\n        const tile = this.currentTile;\n        if (!tile) {\n            this.placementGridModel.visible = false;\n            return;\n        }\n        const preview = this.constructionWorker.getPlacementPreview(buildingName, tile);\n        this.placementGridModel.tiles = preview;\n        this.placementGridModel.visible = true;\n        if (this.defenseMode) {\n            this.showBuildingRangeOverlays(tile, buildingName);\n            this.placementGridModel.rangeIndicator = this.getBuildingRangeCircle(tile, buildingName);\n            this.placementGridModel.rangeIndicatorColor = this.player.color.asHex();\n        }\n        else {\n            this.placementGridModel.rangeIndicator = undefined;\n        }\n    }\n    execute(hover: any, minimap: boolean): false | void {\n        if (!this.currentBuilding || minimap) {\n            return false;\n        }\n        const tile = hover?.tile;\n        if (!tile) {\n            return false;\n        }\n        if (this.player.production.isAvailableForProduction(this.currentBuilding)) {\n            if (!this.constructionWorker.canPlaceAt(this.currentBuilding.name, tile)) {\n                this.eva.play('EVA_CannotDeployHere');\n                return false;\n            }\n            this._onBuildingPlaceRequest.dispatch(this, {\n                rules: this.currentBuilding,\n                tile: this.constructionWorker.normalizePlacementTile(this.currentBuilding.name, tile),\n            });\n            this.endConstructionMode();\n            return;\n        }\n        this.endConstructionMode();\n        return;\n    }\n    cancel(): void {\n        this.endConstructionMode();\n    }\n    private endConstructionMode(): void {\n        this.defenseMode = false;\n        this.placementGridModel.visible = false;\n        this.placementGridModel.rangeIndicator = undefined;\n        this.hideBuildingRangeOverlays();\n        this.buildingRanges.clear();\n        this.currentBuilding = undefined;\n        this.renderer.onFrame.unsubscribe(this.onFrame);\n    }\n    private hideBuildingRangeOverlays(): void {\n        this.buildingRanges.forEach((_, building) => {\n            building.showWeaponRange = false;\n        });\n    }\n    private showBuildingRangeOverlays(tile: any, buildingName: string): void {\n        const circle = this.getBuildingRangeCircle(tile, buildingName);\n        this.buildingRanges.forEach((range, building) => {\n            building.showWeaponRange = circleIntersect(circle, range);\n        });\n    }\n    private getBuildingRangeCircle(tile: any, buildingName: string): {\n        center: {\n            x: number;\n            y: number;\n        };\n        radius: number;\n    } {\n        const foundation = this.game.art.getObject(buildingName, ObjectType.Building).foundation;\n        return {\n            center: {\n                x: tile.rx + (foundation.width % 2 !== 0 ? 0.5 : 0),\n                y: tile.ry + (foundation.height % 2 !== 0 ? 0.5 : 0),\n            },\n            radius: this.currentRangeCircleRadius,\n        };\n    }\n    private prepareBuildingRanges(buildingRules: any): void {\n        const matchingBuildings = [...this.player.buildings].filter((building: any) => building.name === buildingRules.name);\n        if (buildingRules.psychicDetectionRadius) {\n            this.currentRangeCircleRadius = buildingRules.psychicDetectionRadius;\n        }\n        else if (buildingRules.gapGenerator) {\n            this.currentRangeCircleRadius = buildingRules.gapRadiusInCells;\n        }\n        else if (buildingRules.primary) {\n            this.currentRangeCircleRadius = this.game.rules.getWeapon(buildingRules.primary).range;\n        }\n        this.buildingRanges.clear();\n        matchingBuildings.forEach((building: any) => {\n            const tile = building.tile;\n            const foundation = this.game.art.getObject(building.name, ObjectType.Building).foundation;\n            const center = {\n                x: tile.rx + foundation.width / 2,\n                y: tile.ry + foundation.height / 2,\n            };\n            const radius = building.psychicDetectorTrait?.radiusTiles ??\n                building.gapGeneratorTrait?.radiusTiles ??\n                building.primaryWeapon?.range;\n            if (radius) {\n                this.buildingRanges.set(building, { center, radius });\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/PlanningMode.ts",
    "content": "import { OrderType } from '@/game/order/OrderType';\nimport { SoundKey } from '@/engine/sound/SoundKey';\nimport { ChannelType } from '@/engine/sound/ChannelType';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { isNotNullOrUndefined } from '@/util/typeGuard';\nimport { WaypointLines } from '@/engine/renderable/entity/WaypointLines';\nimport { ORDER_UNIT_LIMIT } from '@/game/action/OrderUnitsAction';\nexport class PlanningMode {\n    private active = false;\n    private paths: any[] = [];\n    private selectedPaths: any[] = [];\n    private selectedUnits = new Set<any>();\n    private lastUpdate?: number;\n    private waypointLines?: WaypointLines;\n    constructor(private readonly player: any, private readonly messageList: any, private readonly sound: any, private readonly strings: any, private readonly worldScene: any, private readonly unitSelection: any, private readonly unitSelectionHandler: any, private readonly renderer: any, private readonly targetLines: any, private readonly maxWaypointPathLength: number) { }\n    private readonly onFrame = (time: number): void => {\n        if (this.lastUpdate === undefined || time - this.lastUpdate > 1000 / 15) {\n            this.lastUpdate = time;\n            this.updatePaths();\n        }\n    };\n    isActive(): boolean {\n        return this.active;\n    }\n    enter(): void {\n        if (this.active) {\n            return;\n        }\n        this.active = true;\n        if (this.targetLines.get3DObject()) {\n            this.targetLines.get3DObject().visible = false;\n        }\n        this.renderer.onFrame.subscribe(this.onFrame);\n        const waypointPaths = new Set([\n            ...this.player.getOwnedObjectsByType(ObjectType.Infantry),\n            ...this.player.getOwnedObjectsByType(ObjectType.Vehicle),\n        ]\n            .map((unit: any) => unit.unitOrderTrait.waypointPath)\n            .filter(isNotNullOrUndefined));\n        this.paths = [...waypointPaths].map((path: any) => {\n            const clonedPath = {\n                original: path,\n                units: new Set(path.units),\n                waypoints: [] as any[],\n            };\n            path.waypoints.forEach((waypoint: any) => {\n                const clonedWaypoint = {\n                    orderType: waypoint.orderType,\n                    target: waypoint.target,\n                    next: undefined,\n                    draft: false,\n                    terminal: waypoint.terminal,\n                    original: waypoint,\n                };\n                if (clonedPath.waypoints.length) {\n                    clonedPath.waypoints[clonedPath.waypoints.length - 1].next = clonedWaypoint;\n                }\n                clonedPath.waypoints.push(clonedWaypoint);\n            });\n            return clonedPath;\n        });\n        this.waypointLines = new WaypointLines(this.unitSelection, this.player, this.selectedPaths, this.paths, this.worldScene.camera);\n        this.worldScene.add(this.waypointLines);\n    }\n    pushOrder(orderType: OrderType, target: any, terminal: boolean): void {\n        if (orderType === OrderType.Deploy) {\n            this.handleInvalidCommand(this.strings.get('MSG:PlanningModeNoDeploy'));\n            return;\n        }\n        if (this.selectedPaths.length > 1) {\n            this.handleInvalidCommand(this.strings.get('MSG:PlanningModeHeteroSel'));\n            return;\n        }\n        if (this.selectedUnits.size > ORDER_UNIT_LIMIT) {\n            this.handleInvalidCommand(this.strings.get('MSG:PlannerMaximum'));\n            return;\n        }\n        for (const unit of this.selectedUnits) {\n            if (unit.isBuilding?.()) {\n                this.handleInvalidCommand(this.strings.get('MSG:PlanningModeNoBuildings'));\n                return;\n            }\n            if (unit.isAircraft?.()) {\n                this.handleInvalidCommand(this.strings.get('MSG:PlanningModeNoAircraft'));\n                return;\n            }\n        }\n        let path = this.selectedPaths[0];\n        if (!path && this.selectedUnits.size) {\n            path = { original: undefined, units: new Set(this.selectedUnits), waypoints: [] };\n            this.paths.push(path);\n            this.selectedPaths.push(path);\n        }\n        if (!path) {\n            return;\n        }\n        if (path.waypoints.length === this.maxWaypointPathLength) {\n            this.handleInvalidCommand(this.strings.get('MSG:NodeMaximum'));\n            return;\n        }\n        if (path.waypoints.find((waypoint: any) => waypoint.target.equals(target))) {\n            this.handleInvalidCommand(this.strings.get('MSG:PlanningModeInvalidNodeX'));\n            return;\n        }\n        if (path.waypoints.length && path.waypoints.slice(path.waypoints[0].draft ? 0 : 1).find((waypoint: any) => waypoint.terminal)) {\n            this.handleInvalidCommand(this.strings.get('MSG:PostTerminatingCommand'));\n            return;\n        }\n        const waypoint = {\n            orderType,\n            target,\n            terminal,\n            next: undefined,\n            draft: true,\n            original: undefined,\n        };\n        if (path.waypoints.length) {\n            path.waypoints[path.waypoints.length - 1].next = waypoint;\n        }\n        path.waypoints.push(waypoint);\n        if (terminal) {\n            this.handleInvalidCommand(this.strings.get('MSG:PostTerminatingCommand'));\n            this.unitSelectionHandler.deselectAll();\n            return;\n        }\n        this.sound.play(SoundKey.AddPlanningModeCommandSound, ChannelType.Ui);\n    }\n    exit(): any[] {\n        const paths = this.paths;\n        if (this.active) {\n            if (this.targetLines.get3DObject()) {\n                this.targetLines.get3DObject().visible = true;\n            }\n            this.renderer.onFrame.unsubscribe(this.onFrame);\n            this.active = false;\n            this.paths = [];\n            this.selectedPaths = [];\n            this.selectedUnits.clear();\n            if (this.waypointLines) {\n                this.worldScene.remove(this.waypointLines);\n                this.waypointLines.dispose();\n                this.waypointLines = undefined;\n            }\n        }\n        for (const path of paths) {\n            path.waypoints = path.waypoints.filter((waypoint: any) => waypoint.draft);\n        }\n        return paths.filter((path) => path.waypoints.length);\n    }\n    private updatePaths(): void {\n        for (const path of [...this.paths]) {\n            if (path.original) {\n                if (!(path.original.units.length === path.units.size || path.waypoints.find((waypoint: any) => waypoint.draft))) {\n                    path.units = new Set(path.original.units);\n                }\n                if (path.original.units.length === 0) {\n                    path.waypoints = path.waypoints.filter((waypoint: any) => waypoint.draft);\n                }\n                else {\n                    path.waypoints = path.waypoints.filter((waypoint: any) => waypoint.draft || path.original.waypoints.includes(waypoint.original));\n                }\n                if (!path.waypoints.length) {\n                    this.paths.splice(this.paths.indexOf(path), 1);\n                    const selectedIndex = this.selectedPaths.indexOf(path);\n                    if (selectedIndex !== -1) {\n                        this.selectedPaths.splice(selectedIndex, 1);\n                    }\n                }\n            }\n        }\n    }\n    updateSelection(selection: any[]): any[] | undefined {\n        this.updatePaths();\n        const nextSelection = [...selection];\n        const selectedPaths = new Set<any>();\n        for (const unit of selection) {\n            for (const path of this.paths) {\n                if (path.units.has(unit)) {\n                    selectedPaths.add(path);\n                    nextSelection.push(...path.units);\n                }\n            }\n        }\n        this.selectedPaths.length = 0;\n        this.selectedPaths.push(...selectedPaths);\n        this.selectedUnits = new Set(nextSelection);\n        if (this.selectedUnits.size !== selection.length) {\n            return [...this.selectedUnits];\n        }\n    }\n    private handleInvalidCommand(message: string): void {\n        this.sound.play(SoundKey.ScoldSound, ChannelType.Ui);\n        this.messageList.addUiFeedbackMessage(message);\n    }\n    dispose(): void {\n        this.exit();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/RepairMode.ts",
    "content": "import { PointerType } from '@/engine/type/PointerType';\nimport { EventDispatcher } from '@/util/event';\nexport class RepairMode {\n    private readonly _onExecute = new EventDispatcher<RepairMode, any>();\n    private currentTile?: any;\n    private lastTile?: any;\n    private lastUpdate?: number;\n    get onExecute() {\n        return this._onExecute.asEvent();\n    }\n    static factory(game: any, player: any, sidebarModel: any, pointer: any, renderer: any): RepairMode {\n        return new RepairMode(game, player, sidebarModel, pointer, renderer);\n    }\n    constructor(private readonly game: any, private readonly player: any, private readonly sidebarModel: any, private readonly pointer: any, private readonly renderer: any) { }\n    private readonly onFrame = (time: number): void => {\n        if (this.lastTile === this.currentTile && this.lastUpdate !== undefined && time - this.lastUpdate < 1000 / 15) {\n            return;\n        }\n        this.lastTile = this.currentTile;\n        this.lastUpdate = time;\n        const tile = this.currentTile;\n        const hasRepairableBuilding = !!(tile && this.findRepairableBuilding(tile));\n        this.pointer.setPointerType(tile ? (hasRepairableBuilding ? PointerType.SideRepair : PointerType.NoRepair) : PointerType.Default);\n    };\n    enter(): void {\n        this.sidebarModel.repairMode = true;\n        this.currentTile = undefined;\n        this.lastTile = undefined;\n        this.lastUpdate = undefined;\n        this.renderer.onFrame.subscribe(this.onFrame);\n    }\n    hover(hover: any, minimap: boolean): void {\n        if (!minimap) {\n            this.currentTile = hover?.tile;\n        }\n    }\n    private findRepairableBuilding(tile: any): any {\n        return this.game.map\n            .getObjectsOnTile(tile)\n            .find((gameObject: any) => gameObject.isBuilding?.() &&\n            gameObject.owner === this.player &&\n            gameObject.healthTrait.health < 100 &&\n            gameObject.rules.repairable &&\n            gameObject.rules.clickRepairable);\n    }\n    execute(hover: any, minimap: boolean): boolean {\n        if (minimap) {\n            return false;\n        }\n        const tile = hover?.tile;\n        if (!tile) {\n            return false;\n        }\n        const building = this.findRepairableBuilding(tile);\n        if (building) {\n            this._onExecute.dispatch(this, building);\n        }\n        return false;\n    }\n    cancel(): void {\n        this.end();\n    }\n    private end(): void {\n        this.sidebarModel.repairMode = false;\n        this.renderer.onFrame.unsubscribe(this.onFrame);\n    }\n    dispose(): void {\n        this.end();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/SellMode.ts",
    "content": "import { PointerType } from '@/engine/type/PointerType';\nimport { EventDispatcher } from '@/util/event';\nimport { BuildStatus } from '@/game/gameobject/Building';\nimport { DockableTrait } from '@/game/gameobject/trait/DockableTrait';\nexport class SellMode {\n    private readonly _onExecute = new EventDispatcher<SellMode, any>();\n    private currentHover?: any;\n    private lastHover?: any;\n    private lastUpdate?: number;\n    get onExecute() {\n        return this._onExecute.asEvent();\n    }\n    static factory(game: any, player: any, sidebarModel: any, pointer: any, renderer: any): SellMode {\n        return new SellMode(game, player, sidebarModel, pointer, renderer);\n    }\n    constructor(private readonly game: any, private readonly player: any, private readonly sidebarModel: any, private readonly pointer: any, private readonly renderer: any) { }\n    private readonly onFrame = (time: number): void => {\n        if (this.lastHover?.tile === this.currentHover?.tile &&\n            this.lastHover?.gameObject === this.currentHover?.gameObject &&\n            this.lastUpdate !== undefined &&\n            time - this.lastUpdate < 1000 / 15) {\n            return;\n        }\n        this.lastHover = this.currentHover;\n        this.lastUpdate = time;\n        let pointerType = PointerType.Default;\n        if (this.currentHover?.tile) {\n            const gameObject = this.currentHover.gameObject;\n            pointerType =\n                gameObject && this.isRefundableObject(gameObject)\n                    ? gameObject.isBuilding()\n                        ? PointerType.Sell\n                        : PointerType.SellMini\n                    : PointerType.NoSell;\n        }\n        this.pointer.setPointerType(pointerType);\n    };\n    enter(): void {\n        this.sidebarModel.sellMode = true;\n        this.currentHover = undefined;\n        this.lastHover = undefined;\n        this.lastUpdate = undefined;\n        this.renderer.onFrame.subscribe(this.onFrame);\n    }\n    hover(hover: any, minimap: boolean): void {\n        if (!minimap) {\n            this.currentHover = hover;\n        }\n    }\n    isRefundableObject(gameObject: any): boolean {\n        return !!(gameObject.isTechno?.() &&\n            gameObject.owner === this.player &&\n            !gameObject.rules.unsellable &&\n            this.game.sellTrait.computeRefundValue(gameObject) > 0 &&\n            (gameObject.isBuilding?.()\n                ? gameObject.buildStatus === BuildStatus.Ready && !gameObject.warpedOutTrait.isActive()\n                : gameObject.traits.find(DockableTrait)?.dock?.rules.unitSell));\n    }\n    execute(hover: any, minimap: boolean): boolean {\n        if (minimap) {\n            return false;\n        }\n        const gameObject = hover?.gameObject;\n        if (gameObject && this.isRefundableObject(gameObject)) {\n            this._onExecute.dispatch(this, gameObject);\n        }\n        return false;\n    }\n    cancel(): void {\n        this.end();\n    }\n    private end(): void {\n        this.sidebarModel.sellMode = false;\n        this.renderer.onFrame.unsubscribe(this.onFrame);\n    }\n    dispose(): void {\n        this.end();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/SpecialActionMode.ts",
    "content": "import { PointerType } from '@/engine/type/PointerType';\nimport { EventDispatcher } from '@/util/event';\nimport { SuperWeaponType } from '@/game/type/SuperWeaponType';\nconst pointerTypeBySuperWeapon = new Map<SuperWeaponType, PointerType>()\n    .set(SuperWeaponType.MultiMissile, PointerType.Nuke)\n    .set(SuperWeaponType.LightningStorm, PointerType.Storm)\n    .set(SuperWeaponType.IronCurtain, PointerType.Iron)\n    .set(SuperWeaponType.ChronoSphere, PointerType.Chrono)\n    .set(SuperWeaponType.ChronoWarp, PointerType.Chrono)\n    .set(SuperWeaponType.AmerParaDrop, PointerType.Para)\n    .set(SuperWeaponType.ParaDrop, PointerType.Para);\nexport class SpecialActionMode {\n    private readonly _onExecute = new EventDispatcher<SpecialActionMode, {\n        tile: any;\n        tile2?: any;\n    }>();\n    private isPostClick = false;\n    private preTile?: any;\n    private pointerSwType?: SuperWeaponType;\n    get onExecute() {\n        return this._onExecute.asEvent();\n    }\n    get superWeaponType() {\n        return this.superWeaponRules.type;\n    }\n    static factory(allSuperWeaponRules: any, superWeaponRules: any, superWeaponFxHandler: any, pointer: any, eva: any): SpecialActionMode {\n        return new SpecialActionMode(allSuperWeaponRules, superWeaponRules, superWeaponFxHandler, pointer, eva);\n    }\n    constructor(private readonly allSuperWeaponRules: any, private readonly superWeaponRules: any, private readonly superWeaponFxHandler: any, private readonly pointer: any, private readonly eva: any) {\n        this.pointerSwType = this.superWeaponRules.type;\n    }\n    enter(): void {\n        this.eva.play('EVA_SelectTarget');\n    }\n    hover(hover: any): void {\n        const tile = hover?.tile;\n        const pointerType = this.pointerSwType !== undefined ? pointerTypeBySuperWeapon.get(this.pointerSwType) : undefined;\n        this.pointer.setPointerType(tile && pointerType !== undefined ? pointerType : PointerType.Default);\n    }\n    execute(hover: any): false | void {\n        const tile = hover?.tile;\n        if (!tile) {\n            return false;\n        }\n        if (this.superWeaponRules.type === SuperWeaponType.ChronoSphere &&\n            !this.isPostClick) {\n            this.superWeaponFxHandler.createChronoSphereAnim(tile);\n        }\n        if (this.superWeaponRules.preClick && !this.isPostClick) {\n            this.isPostClick = true;\n            this.preTile = tile;\n            const dependentType = [...this.allSuperWeaponRules.values()].find((rules: any) => rules.postClick && rules.preDependent === this.superWeaponRules.type)?.type;\n            if (dependentType === undefined) {\n                throw new Error(`No super weapon section found with PostClick=yes and PreDependent=\"${SuperWeaponType[this.superWeaponRules.type]}\"`);\n            }\n            this.pointerSwType = dependentType;\n            return false;\n        }\n        this._onExecute.dispatch(this, this.isPostClick ? { tile: this.preTile, tile2: tile } : { tile });\n    }\n    cancel(): void {\n        this.end();\n    }\n    private end(): void {\n        if (this.superWeaponRules.type === SuperWeaponType.ChronoSphere && this.isPostClick) {\n            this.superWeaponFxHandler.disposeChronoSphereAnim();\n        }\n    }\n    dispose(): void {\n        this.end();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/Tooltip.ts",
    "content": "import { UiObject } from '@/gui/UiObject';\nimport { SpriteUtils } from '@/engine/gfx/SpriteUtils';\nimport { CanvasUtils } from '@/engine/gfx/CanvasUtils';\nimport * as THREE from 'three';\nexport class Tooltip extends UiObject {\n    private texture?: THREE.Texture;\n    private mesh?: THREE.Mesh;\n    constructor(private readonly text: string, private readonly color: string, private readonly pointer: any, private readonly viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }) {\n        super(new THREE.Object3D());\n    }\n    override create3DObject(): void {\n        if (!this.mesh) {\n            const root = this.get3DObject();\n            if (!root) {\n                throw new Error('Tooltip root object was not created');\n            }\n            const texture = (this.texture = this.createTexture(this.text, this.color));\n            const size = {\n                width: (texture.image as any).width,\n                height: (texture.image as any).height,\n            };\n            const mesh = (this.mesh = this.createMesh(texture, size.width, size.height));\n            const position = this.computePosition(this.pointer, this.viewport, size);\n            mesh.position.x = position.x;\n            mesh.position.y = position.y;\n            root.add(mesh);\n            mesh.updateMatrix();\n        }\n        super.create3DObject();\n    }\n    private createMesh(texture: THREE.Texture, width: number, height: number): THREE.Mesh {\n        const geometry = SpriteUtils.createRectGeometry(width, height);\n        SpriteUtils.addRectUvs(geometry, { x: 0, y: 0, width, height }, { width, height });\n        geometry.translate(width / 2, height / 2, 0);\n        const material = new THREE.MeshBasicMaterial({\n            map: texture,\n            side: THREE.DoubleSide,\n        });\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.matrixAutoUpdate = false;\n        mesh.frustumCulled = false;\n        return mesh;\n    }\n    private createTexture(text: string, color: string): THREE.Texture {\n        const canvas = document.createElement('canvas');\n        canvas.width = 0;\n        canvas.height = 0;\n        const alphaContext = canvas.getContext('2d', { willReadFrequently: true, alpha: true });\n        if (!alphaContext) {\n            throw new Error('Failed to create tooltip alpha canvas context');\n        }\n        let y = 0;\n        for (const line of text.split('\\n')) {\n            const rect = CanvasUtils.drawText(alphaContext, line, 0, y, {\n                color,\n                fontFamily: \"'Fira Sans Condensed', Arial, sans-serif\",\n                fontSize: 12,\n                fontWeight: '500',\n                paddingTop: 5,\n                paddingBottom: 5,\n                paddingLeft: 2,\n                paddingRight: 4,\n                autoEnlargeCanvas: true,\n            });\n            y += rect.height;\n        }\n        const width = canvas.width;\n        const height = canvas.height;\n        const imageData = alphaContext.getImageData(0, 0, width, height);\n        canvas.width = width + 1;\n        canvas.height = height + 1;\n        const context = canvas.getContext('2d', { willReadFrequently: true, alpha: true });\n        if (!context) {\n            throw new Error('Failed to create tooltip canvas context');\n        }\n        context.putImageData(imageData, 1, 1);\n        context.globalCompositeOperation = 'destination-over';\n        context.fillStyle = 'black';\n        context.fillRect(0, 0, canvas.width, canvas.height);\n        context.globalCompositeOperation = 'source-over';\n        context.strokeStyle = color;\n        context.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);\n        const texture = new THREE.Texture(canvas);\n        texture.minFilter = THREE.NearestFilter;\n        texture.magFilter = THREE.NearestFilter;\n        texture.needsUpdate = true;\n        texture.flipY = false;\n        return texture;\n    }\n    private computePosition(pointer: any, viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }, size: {\n        width: number;\n        height: number;\n    }): {\n        x: number;\n        y: number;\n    } {\n        const position = { ...pointer.getPosition() };\n        if (position.x + 20 + size.width > viewport.x + viewport.width) {\n            position.x -= 20 + size.width;\n        }\n        else {\n            position.x += 20;\n        }\n        if (position.y + 20 + size.height > viewport.y + viewport.height) {\n            position.y -= 20 + size.height;\n        }\n        else {\n            position.y += 20;\n        }\n        return position;\n    }\n    override destroy(): void {\n        super.destroy();\n        this.texture?.dispose();\n        if (this.mesh) {\n            (this.mesh.material as THREE.Material).dispose();\n            this.mesh.geometry.dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/TooltipHandler.ts",
    "content": "import { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { Tooltip } from './Tooltip';\nimport { resolveHoverTooltipText } from '@/gui/screen/game/TooltipTextResolver';\nclass HoverTarget {\n    entity?: any;\n    uiObject?: any;\n    equals(other: HoverTarget): boolean {\n        return (this.entity ?? this.uiObject) === (other.entity ?? other.uiObject);\n    }\n    copy(other: HoverTarget): void {\n        this.entity = other.entity;\n        this.uiObject = other.uiObject;\n    }\n}\nexport class TooltipHandler {\n    static readonly ZINDEX = 100;\n    private readonly disposables = new CompositeDisposable();\n    private readonly currentHover = new HoverTarget();\n    private readonly lastHover = new HoverTarget();\n    private tooltip?: Tooltip;\n    private hoverStartTime?: number;\n    private lastUpdateTime?: number;\n    private isTouch = false;\n    private needsHoverTimeReset = false;\n    private paused = false;\n    constructor(private readonly mapHoverHandler: any, private readonly tooltipTextColor: string, private readonly pointer: any, private readonly uiScene: any, private readonly renderer: any, private readonly strings: any, private readonly debugText: any) { }\n    private readonly handleUiMouseMove = (event: any): void => {\n        const intersectionObject = event.intersection?.object;\n        let tooltipTarget = intersectionObject;\n        while (tooltipTarget && tooltipTarget.userData?.tooltip === undefined) {\n            tooltipTarget = tooltipTarget.parent;\n        }\n        this.currentHover.uiObject = tooltipTarget ?? intersectionObject;\n        if (this.hoverStartTime !== undefined) {\n            this.needsHoverTimeReset = true;\n        }\n        this.isTouch = !!event.isTouch;\n    };\n    private readonly handleMouseDown = (): void => {\n        this.paused = true;\n        this.reset();\n    };\n    private readonly handleMouseUp = (event: any): void => {\n        this.paused = false;\n        this.isTouch = !!event.isTouch;\n    };\n    private readonly handleMouseWheel = (): void => {\n        this.reset();\n    };\n    private readonly onFrame = (): void => {\n        const now = performance.now();\n        if (this.lastUpdateTime !== undefined && now - this.lastUpdateTime < 1000 / 15) {\n            return;\n        }\n        this.lastUpdateTime = now;\n        this.currentHover.entity = this.mapHoverHandler.getCurrentHover()?.entity;\n        if (this.paused) {\n            return;\n        }\n        if (this.currentHover.equals(this.lastHover)) {\n            if (this.needsHoverTimeReset) {\n                this.needsHoverTimeReset = false;\n                this.hoverStartTime = now;\n            }\n            const hoverDelay = this.currentHover.entity ? 800 : 400;\n            if (this.hoverStartTime !== undefined &&\n                now - this.hoverStartTime > hoverDelay) {\n                const tooltipText = this.getTooltipText(this.currentHover);\n                if (tooltipText && !this.tooltip && !this.isTouch) {\n                    const tooltip = new Tooltip(tooltipText, this.tooltipTextColor, this.pointer, this.uiScene.viewport);\n                    tooltip.setZIndex(TooltipHandler.ZINDEX);\n                    this.tooltip = tooltip;\n                    this.uiScene.add(tooltip);\n                }\n            }\n            return;\n        }\n        this.lastHover.copy(this.currentHover);\n        this.hoverStartTime = undefined;\n        this.destroyTooltip();\n        if (this.getTooltipText(this.currentHover) !== undefined) {\n            this.hoverStartTime = now;\n        }\n    };\n    init(): void {\n        this.disposables.add(this.pointer.pointerEvents.addEventListener(this.uiScene.get3DObject(), 'mousemove', this.handleUiMouseMove));\n        this.disposables.add(this.pointer.pointerEvents.addEventListener('canvas', 'mousedown', this.handleMouseDown), this.pointer.pointerEvents.addEventListener('canvas', 'wheel', this.handleMouseWheel), this.pointer.pointerEvents.addEventListener('canvas', 'mouseup', this.handleMouseUp));\n        this.renderer.onFrame.subscribe(this.onFrame);\n        this.disposables.add(() => this.renderer.onFrame.unsubscribe(this.onFrame));\n    }\n    reset(): void {\n        this.destroyTooltip();\n        if (this.hoverStartTime !== undefined) {\n            this.needsHoverTimeReset = true;\n        }\n    }\n    private getTooltipText(hover: HoverTarget): string | undefined {\n        return resolveHoverTooltipText(hover, this.strings, this.debugText.value);\n    }\n    private destroyTooltip(): void {\n        if (this.tooltip) {\n            this.uiScene.remove(this.tooltip);\n            this.tooltip.destroy();\n            this.tooltip = undefined;\n        }\n    }\n    dispose(): void {\n        this.disposables.dispose();\n        this.destroyTooltip();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/UnitSelectionHandler.ts",
    "content": "import * as THREE from 'three';\nimport { equals } from '@/util/array';\nimport { rectContainsPoint } from '@/util/geometry';\nimport { clamp } from '@/util/math';\nimport { EventDispatcher } from '@/util/event';\nimport { HealthLevel } from '@/game/gameobject/unit/HealthLevel';\nenum QueryType {\n    None = 0,\n    OnScreen = 1,\n    OnMap = 2,\n    Veteran = 3,\n    Health = 4\n}\ninterface SelectionUpdate {\n    selection: any[];\n    queryType?: QueryType;\n    veteranLevel?: number;\n    healthLevel?: number;\n}\nexport class UnitSelectionHandler {\n    private readonly _onUserSelectionChange = new EventDispatcher<UnitSelectionHandler, SelectionUpdate>();\n    private readonly _onUserSelectionUpdate = new EventDispatcher<UnitSelectionHandler, SelectionUpdate>();\n    private shouldSelectByTypeOnMap = false;\n    private shouldSelectCombatantsOnMap = false;\n    private selectVeteranState?: number;\n    private selectHealthState?: number;\n    private vetNavSelectionSet: any[] = [];\n    private healthNavSelectionSet: any[] = [];\n    private boxSelectOrigin?: {\n        x: number;\n        y: number;\n    };\n    private selectBox?: THREE.Line;\n    constructor(private readonly worldScene: any, private readonly uiScene: any, public readonly player: any, private readonly unitSelection: any, private readonly entityIntersectHelper: any, private readonly veteranCap: number) {\n        this._onUserSelectionChange.subscribe(() => {\n            this.shouldSelectByTypeOnMap = false;\n            this.shouldSelectCombatantsOnMap = false;\n        });\n        this._onUserSelectionUpdate.subscribe(() => {\n            this.selectVeteranState = undefined;\n            this.selectHealthState = undefined;\n        });\n    }\n    get onUserSelectionChange() {\n        return this._onUserSelectionChange.asEvent();\n    }\n    get onUserSelectionUpdate() {\n        return this._onUserSelectionUpdate.asEvent();\n    }\n    addToSelection(unit: any): void {\n        if (!unit?.rules?.selectable) {\n            return;\n        }\n        const selected = this.unitSelection.getSelectedUnits();\n        if (selected.length &&\n            ((unit.owner === this.player && !selected.find((selectedUnit: any) => selectedUnit.owner !== unit.owner)) ||\n                !this.player)) {\n            this.unitSelection.addToSelection(unit);\n            return;\n        }\n        if (selected.length) {\n            this.unitSelection.deselectAll();\n        }\n        this.unitSelection.addToSelection(unit);\n    }\n    selectSingleUnit(unit: any): void {\n        if (!unit?.rules?.selectable) {\n            return;\n        }\n        const previousSelection = this.unitSelection.getSelectedUnits();\n        if (previousSelection.length) {\n            this.unitSelection.deselectAll();\n        }\n        this.unitSelection.addToSelection(unit);\n        const currentSelection = this.unitSelection.getSelectedUnits();\n        const event = { selection: currentSelection };\n        if (!(currentSelection.length === previousSelection.length && currentSelection[0] === previousSelection[0])) {\n            this._onUserSelectionChange.dispatch(this, event);\n        }\n        this._onUserSelectionUpdate.dispatch(this, event);\n    }\n    toggleSelection(unit: any): void {\n        if (!unit?.rules?.selectable) {\n            return;\n        }\n        if (this.unitSelection.isSelected(unit)) {\n            this.unitSelection.removeFromSelection([unit]);\n        }\n        else {\n            this.addToSelection(unit);\n        }\n        const event = { selection: this.unitSelection.getSelectedUnits() };\n        this._onUserSelectionChange.dispatch(this, event);\n        this._onUserSelectionUpdate.dispatch(this, event);\n    }\n    deselectAll(): void {\n        const event = { selection: [] };\n        if (this.unitSelection.getSelectedUnits().length) {\n            this.unitSelection.deselectAll();\n            this._onUserSelectionChange.dispatch(this, event);\n        }\n        this._onUserSelectionUpdate.dispatch(this, event);\n    }\n    selectMultipleUnits(units: any[], meta: {\n        queryType?: QueryType;\n        veteranLevel?: number;\n        healthLevel?: number;\n    } = {}, clearExisting: boolean = true): void {\n        const previousSelection = this.unitSelection.getSelectedUnits();\n        if (clearExisting) {\n            this.unitSelection.deselectAll();\n        }\n        units.forEach((unit) => this.addToSelection(unit));\n        const currentSelection = this.unitSelection.getSelectedUnits();\n        const event = {\n            selection: currentSelection,\n            queryType: meta.queryType ?? QueryType.None,\n            veteranLevel: meta.veteranLevel,\n            healthLevel: meta.healthLevel,\n        };\n        if (!equals(previousSelection, currentSelection)) {\n            this._onUserSelectionChange.dispatch(this, event);\n        }\n        this._onUserSelectionUpdate.dispatch(this, event);\n    }\n    getSelectedUnits(): any[] {\n        return this.unitSelection.getSelectedUnits();\n    }\n    startBoxSelect(pointer: {\n        x: number;\n        y: number;\n    }): void {\n        this.boxSelectOrigin = pointer;\n        this.disposeBoxSelect();\n        this.selectBox = this.createSelectBox(new THREE.Box2());\n        this.uiScene.get3DObject().add(this.selectBox);\n    }\n    updateBoxSelect(pointer: {\n        x: number;\n        y: number;\n    }): void {\n        if (!this.boxSelectOrigin || !this.selectBox) {\n            return;\n        }\n        const clamped = this.clampPointerToWorldViewport(pointer);\n        const box = new THREE.Box2().setFromPoints([\n            new THREE.Vector2(this.boxSelectOrigin.x, this.boxSelectOrigin.y),\n            new THREE.Vector2(clamped.x, clamped.y),\n        ]);\n        this.selectBox.geometry.dispose();\n        this.selectBox.geometry = this.createBoxGeometry(box);\n    }\n    finishBoxSelect(pointer: {\n        x: number;\n        y: number;\n    }, clearExisting: boolean): boolean {\n        if (!this.boxSelectOrigin) {\n            return false;\n        }\n        const origin = this.boxSelectOrigin;\n        this.boxSelectOrigin = undefined;\n        this.disposeBoxSelect();\n        if (rectContainsPoint({ x: origin.x, y: origin.y, width: 0, height: 0 }, pointer)) {\n            return false;\n        }\n        if (origin.x === pointer.x && origin.y === pointer.y) {\n            return false;\n        }\n        const clamped = this.clampPointerToWorldViewport(pointer);\n        const box = new THREE.Box2().setFromPoints([\n            new THREE.Vector2(origin.x, origin.y),\n            new THREE.Vector2(clamped.x, clamped.y),\n        ]);\n        const units = this.entityIntersectHelper\n            .getEntitiesAtScreenBox(box)\n            ?.map((renderable: any) => renderable.gameObject)\n            .filter((gameObject: any) => gameObject.isTechno?.() && gameObject.rules.selectable && gameObject.owner === this.player);\n        if (!units?.length) {\n            return false;\n        }\n        const selection = units.length === 1 ? [units[0]] : units.filter((unit: any) => !unit.isBuilding?.());\n        if (!selection.length) {\n            return false;\n        }\n        this.selectMultipleUnits(selection, { queryType: QueryType.None }, clearExisting);\n        return true;\n    }\n    cancelBoxSelect(): void {\n        this.boxSelectOrigin = undefined;\n        this.disposeBoxSelect();\n    }\n    createGroup(groupNumber: number): void {\n        const selectedUnits = this.unitSelection.getSelectedUnits();\n        if (selectedUnits.length === 1 && selectedUnits[0].owner !== this.player) {\n            return;\n        }\n        this.unitSelection.createGroup(groupNumber);\n    }\n    getGroupUnits(groupNumber: number): any[] {\n        return this.unitSelection.getGroupUnits(groupNumber);\n    }\n    addGroupToSelection(groupNumber: number): void {\n        const previousSelection = this.getSelectedUnits();\n        this.unitSelection.addGroupToSelection(groupNumber);\n        const currentSelection = this.getSelectedUnits();\n        const event = { selection: currentSelection };\n        if (!equals(currentSelection, previousSelection)) {\n            this._onUserSelectionChange.dispatch(this, event);\n        }\n        this._onUserSelectionUpdate.dispatch(this, event);\n    }\n    selectGroup(groupNumber: number): void {\n        const previousSelection = this.getSelectedUnits();\n        this.unitSelection.selectGroup(groupNumber);\n        const currentSelection = this.getSelectedUnits();\n        const event = { selection: currentSelection };\n        if (!equals(currentSelection, previousSelection)) {\n            this._onUserSelectionChange.dispatch(this, event);\n        }\n        this._onUserSelectionUpdate.dispatch(this, event);\n    }\n    selectByType(): void {\n        const owner = this.player ?? this.unitSelection.getSelectedUnits()[0]?.owner;\n        if (!owner) {\n            return;\n        }\n        const selectedNames = this.getSelectedUnits().reduce((set, unit) => set.add(unit.name), new Set<string>());\n        let candidates: any[] = [];\n        let matching: any[] = [];\n        if (!this.shouldSelectByTypeOnMap) {\n            candidates = this.getOwnedObjectsOnScreen(owner);\n            matching = candidates.filter((unit) => selectedNames.has(unit.name));\n            if (matching.every((unit) => this.unitSelection.isSelected(unit))) {\n                this.shouldSelectByTypeOnMap = true;\n            }\n        }\n        if (this.shouldSelectByTypeOnMap) {\n            candidates = owner.getOwnedObjects();\n            matching = candidates.filter((unit: any) => selectedNames.has(unit.name));\n        }\n        const queryType = this.shouldSelectByTypeOnMap ? QueryType.OnMap : QueryType.OnScreen;\n        if (matching.length) {\n            this.selectMultipleUnits(matching, { queryType }, false);\n        }\n        else if (!selectedNames.size) {\n            this.selectMultipleUnits([], { queryType });\n        }\n        this.shouldSelectByTypeOnMap = true;\n    }\n    selectCombatants(): void {\n        const owner = this.player ?? this.unitSelection.getSelectedUnits()[0]?.owner;\n        if (!owner) {\n            return;\n        }\n        const candidates = this.shouldSelectCombatantsOnMap ? owner.getOwnedObjects() : this.getOwnedObjectsOnScreen(owner);\n        const matching = candidates.filter((unit: any) => unit.isUnit?.() &&\n            unit.rules.selectable &&\n            unit.rules.isSelectableCombatant &&\n            unit.attackTrait &&\n            !unit.rules.harvester);\n        if (matching.length) {\n            this.selectMultipleUnits(matching, {\n                queryType: this.shouldSelectCombatantsOnMap ? QueryType.OnMap : QueryType.OnScreen,\n            });\n        }\n        else if (this.shouldSelectCombatantsOnMap) {\n            this.selectMultipleUnits([], { queryType: QueryType.OnMap });\n        }\n        else {\n            this.shouldSelectCombatantsOnMap = true;\n            this.selectCombatants();\n            return;\n        }\n        this.shouldSelectCombatantsOnMap = true;\n    }\n    selectByVeterancy(): void {\n        const owner = this.player ?? this.unitSelection.getSelectedUnits()[0]?.owner;\n        if (!owner) {\n            return;\n        }\n        let veteranLevel: number;\n        if (this.selectVeteranState === undefined) {\n            veteranLevel = this.veteranCap;\n            this.vetNavSelectionSet = this.unitSelection.getSelectedUnits();\n            if (!this.vetNavSelectionSet.length) {\n                this.vetNavSelectionSet = this.getOwnedObjectsOnScreen(owner).filter((unit) => unit.isUnit?.());\n            }\n        }\n        else {\n            const totalLevels = this.veteranCap + 1;\n            veteranLevel = (this.selectVeteranState - 1 + totalLevels) % totalLevels;\n        }\n        const candidates = this.vetNavSelectionSet.filter((unit) => unit.rules.selectable && !unit.isDestroyed && !unit.isCrashing && !unit.limboData && unit.owner === owner);\n        const matching = candidates.filter((unit) => unit.veteranLevel === veteranLevel);\n        this.selectMultipleUnits(matching, {\n            queryType: QueryType.Veteran,\n            veteranLevel: candidates.length ? veteranLevel : undefined,\n        });\n        this.selectVeteranState = veteranLevel;\n    }\n    selectByHealth(): void {\n        const owner = this.player ?? this.unitSelection.getSelectedUnits()[0]?.owner;\n        if (!owner) {\n            return;\n        }\n        const totalLevels = Object.keys(HealthLevel).filter((value) => !Number.isNaN(Number(value))).length;\n        let healthLevel: number;\n        if (this.selectHealthState === undefined) {\n            healthLevel = totalLevels - 1;\n            this.healthNavSelectionSet = this.unitSelection.getSelectedUnits();\n            if (!this.healthNavSelectionSet.length) {\n                this.healthNavSelectionSet = this.getOwnedObjectsOnScreen(owner).filter((unit) => unit.isUnit?.());\n            }\n        }\n        else {\n            healthLevel = (this.selectHealthState - 1 + totalLevels) % totalLevels;\n        }\n        const candidates = this.healthNavSelectionSet.filter((unit) => unit.rules.selectable && !unit.isDestroyed && !unit.isCrashing && !unit.limboData && unit.owner === owner);\n        const matching = candidates.filter((unit) => unit.healthTrait.level === healthLevel);\n        this.selectMultipleUnits(matching, {\n            queryType: QueryType.Health,\n            healthLevel: candidates.length ? healthLevel : undefined,\n        });\n        this.selectHealthState = healthLevel;\n    }\n    clearSelection(): void {\n        this.deselectAll();\n    }\n    getSelection(): any[] {\n        return this.getSelectedUnits();\n    }\n    getHash(): number {\n        return this.unitSelection.getHash();\n    }\n    dispose(): void {\n        this.cancelBoxSelect();\n    }\n    private getOwnedObjectsOnScreen(owner: any): any[] {\n        const viewport = this.worldScene.viewport;\n        const box = new THREE.Box2(new THREE.Vector2(viewport.x, viewport.y), new THREE.Vector2(viewport.x + viewport.width - 1, viewport.y + viewport.height - 1));\n        return (this.entityIntersectHelper\n            .getEntitiesAtScreenBox(box)\n            ?.map((renderable: any) => renderable.gameObject)\n            .filter((gameObject: any) => gameObject.isTechno?.() && gameObject.owner === owner) ?? []);\n    }\n    private disposeBoxSelect(): void {\n        if (!this.selectBox) {\n            return;\n        }\n        this.uiScene.get3DObject().remove(this.selectBox);\n        this.selectBox.geometry.dispose();\n        (this.selectBox.material as THREE.Material).dispose();\n        this.selectBox = undefined;\n    }\n    private clampPointerToWorldViewport(pointer: {\n        x: number;\n        y: number;\n    }): {\n        x: number;\n        y: number;\n    } {\n        const viewport = this.worldScene.viewport;\n        return {\n            x: clamp(pointer.x, viewport.x, viewport.x + viewport.width - 1),\n            y: clamp(pointer.y, viewport.y, viewport.y + viewport.height - 1),\n        };\n    }\n    private createSelectBox(box: THREE.Box2): THREE.Line {\n        const material = new THREE.LineBasicMaterial({\n            color: 0xffffff,\n            transparent: true,\n            depthTest: false,\n            depthWrite: false,\n        });\n        const geometry = this.createBoxGeometry(box);\n        return new THREE.Line(geometry, material);\n    }\n    private createBoxGeometry(box: THREE.Box2): THREE.BufferGeometry {\n        const min = { x: box.min.x, y: box.min.y };\n        const max = { x: box.max.x, y: box.max.y };\n        const topRight = { x: box.max.x, y: box.min.y };\n        const bottomLeft = { x: box.min.x, y: box.max.y };\n        const positions = new Float32Array([\n            min.x, min.y, 0,\n            bottomLeft.x, bottomLeft.y, 0,\n            max.x, max.y, 0,\n            topRight.x, topRight.y, 0,\n            min.x, min.y, 0,\n        ]);\n        const geometry = new THREE.BufferGeometry();\n        geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));\n        return geometry;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/WorldInteraction.ts",
    "content": "import { rectContainsPoint } from '@/util/geometry';\nimport { PointerType } from '@/engine/type/PointerType';\nimport { ActionFilter } from './DefaultActionHandler';\nimport { isMacFirefox } from '@/util/userAgent';\nexport class WorldInteraction {\n    private initialized = false;\n    private enabled = true;\n    private currentMode?: any;\n    private clearModeOnSelectionChange = false;\n    private clickOrigin = { x: 0, y: 0 };\n    private maybePan = false;\n    private hasDragged = false;\n    private mousePressed?: number;\n    private queuedMouseMoveEvent?: any;\n    private isMinimapHover = false;\n    private minimapHoverTile?: any;\n    private minimapDragButton?: number;\n    private suppressNextMinimapClick = false;\n    private lastSelectionHash?: number;\n    private lastDefaultActionUpdate?: number;\n    private lastFrameTime?: number;\n    private lastMouseDownEvent?: any;\n    private lastDefaultModeClickDetails?: any;\n    private lastKeyboardEvent?: KeyboardEvent;\n    private lastKeyMods?: KeyboardEvent;\n    private hasFaultyCtrlLeftClick = false;\n    public chatTypingHandler?: any;\n    constructor(private readonly worldScene: any, private readonly pointer: any, private readonly pointerEvents: any, private readonly cameraPanHandler: any, private readonly mapScrollHandler: any, private readonly mapHoverHandler: any, private readonly tooltipHandler: any, public readonly entityIntersectHelper: any, public readonly unitSelectionHandler: any, public readonly defaultActionHandler: any, public readonly keyboardHandler: any, public readonly arrowScrollHandler: any, public readonly customScrollHandler: any, public readonly minimapHandler: any, private readonly cameraZoom: any, private readonly document: Document, private readonly renderer: any, public readonly targetLines: any, private readonly rightClickMove: any, private readonly rightClickScroll: any, private readonly battleControlApi: any) { }\n    init(): void {\n        if (this.initialized) {\n            return;\n        }\n        this.setupHandlers();\n        this.worldScene.add(this.targetLines);\n        this.initialized = true;\n        this.hasFaultyCtrlLeftClick = isMacFirefox();\n        this.battleControlApi?._setWorldInteraction(this);\n        this.battleControlApi?._notifyToggle(true);\n    }\n    setShroud(shroud: any): void {\n        this.mapHoverHandler.setShroud(shroud);\n        this.minimapHandler.setShroud(shroud);\n    }\n    private setupHandlers(): void {\n        this.pointerEvents.addEventListener('canvas', 'mousemove', this.handleMouseMove);\n        this.pointerEvents.addEventListener('canvas', 'mousedown', this.handleMouseDown);\n        this.pointerEvents.addEventListener('canvas', 'mouseup', this.handleMouseUp);\n        this.pointerEvents.addEventListener('canvas', 'wheel', this.handleWheel);\n        this.document.addEventListener('keydown', this.handleKeyDown);\n        this.document.addEventListener('keyup', this.handleKeyUp);\n        this.mapHoverHandler.onHoverChange.subscribe(this.handleMapHoverChange);\n        this.renderer.onFrame.subscribe(this.handleFrame);\n        this.unitSelectionHandler.onUserSelectionChange.subscribe(this.handleSelectionChange);\n        this.minimapHandler.minimap.onClick.subscribe(this.handleMinimapClick);\n        this.minimapHandler.minimap.onRightClick.subscribe(this.handleMinimapRightClick);\n        this.minimapHandler.minimap.onMouseOver.subscribe(this.handleMinimapMouseOver);\n        this.minimapHandler.minimap.onMouseMove.subscribe(this.handleMinimapMouseMove);\n        this.minimapHandler.minimap.onMouseOut.subscribe(this.handleMinimapMouseOut);\n        this.tooltipHandler.init();\n    }\n    private teardownHandlers(): void {\n        this.pointerEvents.removeEventListener('canvas', 'mousemove', this.handleMouseMove);\n        this.pointerEvents.removeEventListener('canvas', 'mousedown', this.handleMouseDown);\n        this.pointerEvents.removeEventListener('canvas', 'mouseup', this.handleMouseUp);\n        this.pointerEvents.removeEventListener('canvas', 'wheel', this.handleWheel);\n        this.document.removeEventListener('keydown', this.handleKeyDown);\n        this.document.removeEventListener('keyup', this.handleKeyUp);\n        this.mapHoverHandler.onHoverChange.unsubscribe(this.handleMapHoverChange);\n        this.renderer.onFrame.unsubscribe(this.handleFrame);\n        this.unitSelectionHandler.onUserSelectionChange.unsubscribe(this.handleSelectionChange);\n        this.unitSelectionHandler.cancelBoxSelect();\n        this.minimapHandler.minimap.onClick.unsubscribe(this.handleMinimapClick);\n        this.minimapHandler.minimap.onRightClick.unsubscribe(this.handleMinimapRightClick);\n        this.minimapHandler.minimap.onMouseOver.unsubscribe(this.handleMinimapMouseOver);\n        this.minimapHandler.minimap.onMouseMove.unsubscribe(this.handleMinimapMouseMove);\n        this.minimapHandler.minimap.onMouseOut.unsubscribe(this.handleMinimapMouseOut);\n        this.tooltipHandler.dispose();\n        this.mapScrollHandler.cancel();\n        this.arrowScrollHandler.cancel();\n        this.customScrollHandler.cancel();\n    }\n    dispose(): void {\n        if (this.initialized && this.enabled) {\n            this.teardownHandlers();\n            this.pointer.setPointerType(PointerType.Default);\n            this.battleControlApi?._setWorldInteraction(undefined);\n            this.battleControlApi?._notifyToggle(false);\n        }\n        this.currentMode?.dispose?.();\n        this.mapScrollHandler.dispose();\n        this.cameraPanHandler.dispose();\n        this.mapHoverHandler.dispose();\n        this.unitSelectionHandler.dispose();\n        this.chatTypingHandler?.dispose?.();\n        this.keyboardHandler.dispose();\n        this.worldScene.remove(this.targetLines);\n        this.targetLines.dispose?.();\n        this.tooltipHandler.dispose();\n    }\n    setEnabled(enabled: boolean): void {\n        if (this.enabled === enabled) {\n            return;\n        }\n        this.enabled = enabled;\n        if (enabled) {\n            this.setupHandlers();\n        }\n        else {\n            this.teardownHandlers();\n            this.cancelMouseUp();\n            this.cancelKeyUp();\n            this.pointer.setPointerType(PointerType.Default);\n            this.chatTypingHandler?.endTyping?.();\n        }\n        this.battleControlApi?._setWorldInteraction(enabled ? this : undefined);\n        this.battleControlApi?._notifyToggle(enabled);\n    }\n    isEnabled(): boolean {\n        return this.enabled;\n    }\n    pausePanning(): void {\n        this.cameraPanHandler.setPaused(true);\n        this.mapScrollHandler.setPaused(true);\n    }\n    unpausePanning(): void {\n        this.cameraPanHandler.setPaused(false);\n        this.mapScrollHandler.setPaused(false);\n    }\n    setMode(mode: any): void {\n        if (this.currentMode !== mode) {\n            this.currentMode?.cancel?.();\n            this.pointer.setPointerType(PointerType.Default);\n        }\n        this.currentMode = mode;\n        this.clearModeOnSelectionChange = false;\n        if (mode) {\n            this.unitSelectionHandler.cancelBoxSelect();\n            this.unitSelectionHandler.deselectAll();\n            this.clearModeOnSelectionChange = true;\n            mode.enter();\n            this.mapHoverHandler.update(this.pointer.getPosition(), true);\n            const hover = this.getCurrentHover();\n            if (hover) {\n                mode.hover(hover, this.isMinimapHover);\n            }\n        }\n    }\n    getMode(): any {\n        return this.currentMode;\n    }\n    getLastKeyModifiers(): KeyboardEvent | undefined {\n        return this.lastKeyMods;\n    }\n    registerKeyCommand(type: string, command: any): this {\n        this.keyboardHandler.registerCommand(type, command);\n        return this;\n    }\n    unregisterKeyCommand(type: string): this {\n        this.keyboardHandler.unregisterCommand(type);\n        return this;\n    }\n    applyKeyModifiers(modifiers: any): void {\n        this.lastKeyMods = modifiers;\n        if (!this.currentMode && !(this.maybePan && this.hasDragged) && !this.mapScrollHandler.isScrolling()) {\n            this.updateDefaultAction(this.getCurrentHover(), this.unitSelectionHandler.getSelectedUnits(), modifiers);\n        }\n    }\n    private updateDefaultAction(hover: any, selection: any[], keyboardEvent: any): void {\n        const scrolling = this.mapScrollHandler.isScrolling();\n        if (hover) {\n            this.defaultActionHandler.update(hover, selection, this.isRightClickMove(), keyboardEvent, this.isMinimapHover);\n            if (!scrolling) {\n                this.pointer.setPointerType(this.defaultActionHandler.getPointerType(this.isMinimapHover));\n            }\n        }\n        else if (!scrolling) {\n            this.pointer.setPointerType(this.isMinimapHover ? PointerType.Mini : PointerType.Default);\n        }\n        this.lastDefaultActionUpdate = this.lastFrameTime;\n    }\n    private readonly handleSelectionChange = (): void => {\n        if (this.clearModeOnSelectionChange) {\n            this.setMode(undefined);\n        }\n    };\n    private readonly handleKeyDown = (event: KeyboardEvent): void => {\n        this.handleKeyModifierChange(event);\n        this.keyboardHandler.handleKeyDown(event);\n        this.arrowScrollHandler.handleKeyDown(event);\n        this.chatTypingHandler?.handleKeyDown?.(event);\n    };\n    private readonly handleKeyUp = (event: KeyboardEvent): void => {\n        this.handleKeyModifierChange(event);\n        this.keyboardHandler.handleKeyUp(event);\n        this.arrowScrollHandler.handleKeyUp(event);\n        this.chatTypingHandler?.handleKeyUp?.(event);\n        this.tooltipHandler.reset();\n    };\n    private handleKeyModifierChange(event: KeyboardEvent): void {\n        const previous = this.lastKeyMods;\n        this.lastKeyMods = event;\n        this.lastKeyboardEvent = event;\n        if (this.currentMode ||\n            (this.maybePan && this.hasDragged) ||\n            this.mapScrollHandler.isScrolling() ||\n            event.repeat ||\n            (event.shiftKey === previous?.shiftKey && event.ctrlKey === previous?.ctrlKey && event.altKey === previous?.altKey)) {\n            return;\n        }\n        this.updateDefaultAction(this.getCurrentHover(), this.unitSelectionHandler.getSelectedUnits(), event);\n    }\n    private readonly handleMapHoverChange = (hover: any): void => {\n        this.currentMode?.hover?.(hover, this.isMinimapHover);\n        if (!this.isMinimapHover && !this.currentMode) {\n            this.updateDefaultAction(hover, this.unitSelectionHandler.getSelectedUnits(), this.lastKeyMods);\n        }\n    };\n    private readonly handleMouseMove = (event: any): void => {\n        this.queuedMouseMoveEvent = event;\n    };\n    private readonly handleFrame = (time: number): void => {\n        this.lastFrameTime = time;\n        let shouldRefreshDefaultAction = false;\n        const selectionHash = this.unitSelectionHandler.getHash();\n        if (selectionHash !== this.lastSelectionHash && !this.currentMode) {\n            this.lastSelectionHash = selectionHash;\n            shouldRefreshDefaultAction = true;\n        }\n        if (this.queuedMouseMoveEvent) {\n            const event = this.queuedMouseMoveEvent;\n            this.queuedMouseMoveEvent = undefined;\n            this.processMouseMove(event);\n        }\n        if ((this.lastDefaultActionUpdate === undefined || time - this.lastDefaultActionUpdate >= 1000 / 15) &&\n            !this.currentMode &&\n            !this.mapScrollHandler.isScrolling() &&\n            !(this.hasDragged && this.maybePan)) {\n            shouldRefreshDefaultAction = true;\n        }\n        if (shouldRefreshDefaultAction) {\n            this.updateDefaultAction(this.getCurrentHover(), this.unitSelectionHandler.getSelectedUnits(), this.lastKeyMods);\n        }\n    };\n    private readonly handleMouseDown = (event: any): void => {\n        if (!rectContainsPoint(this.worldScene.viewport, event.pointer) || this.mousePressed !== undefined) {\n            return;\n        }\n        if (this.hasFaultyCtrlLeftClick && event.ctrlKey && event.button === 2) {\n            event.button = 0;\n        }\n        this.mapScrollHandler.cancel();\n        if (event.button === 0 &&\n            this.isMinimapHover &&\n            this.minimapHandler.isTileWithinViewport(this.minimapHoverTile)) {\n            this.clickOrigin = event.pointer;\n            this.mousePressed = event.button;\n            this.lastMouseDownEvent = event;\n            this.hasDragged = false;\n            this.minimapDragButton = event.button;\n            this.pointer.setPointerType(PointerType.Mini);\n            return;\n        }\n        this.pointerEvents.intersectionsEnabled = false;\n        this.clickOrigin = event.pointer;\n        this.mousePressed = event.button;\n        this.lastMouseDownEvent = event;\n        this.hasDragged = false;\n        if ((event.button === 2 && this.isRightClickPanAllowed()) || event.button === 1) {\n            this.maybePan = true;\n            this.cameraPanHandler.start(event.pointer);\n        }\n        if (event.button === 2) {\n            if (!this.isRightClickPanAllowed() && !this.isRightClickMove()) {\n                this.unitSelectionHandler.deselectAll();\n            }\n            this.chatTypingHandler?.endTyping?.();\n        }\n    };\n    private readonly handleMouseUp = (event: any): void => {\n        if (this.hasFaultyCtrlLeftClick && event.ctrlKey && event.button === 2) {\n            event.button = 0;\n        }\n        if (this.mousePressed !== event.button) {\n            return;\n        }\n        if (this.minimapDragButton === event.button) {\n            this.mousePressed = undefined;\n            this.lastMouseDownEvent = undefined;\n            this.suppressNextMinimapClick = this.hasDragged;\n            this.hasDragged = false;\n            this.minimapDragButton = undefined;\n            this.pointer.setPointerType(this.isMinimapHover ? PointerType.Mini : PointerType.Default);\n            return;\n        }\n        if (event.isTouch && this.lastKeyMods && this.lastKeyMods !== this.lastKeyboardEvent) {\n            event.ctrlKey = this.lastKeyMods.ctrlKey;\n            event.shiftKey = this.lastKeyMods.shiftKey;\n            event.altKey = this.lastKeyMods.altKey;\n        }\n        this.pointerEvents.intersectionsEnabled = true;\n        this.mousePressed = undefined;\n        const wasPanning = this.maybePan;\n        this.maybePan = false;\n        if (wasPanning) {\n            this.cameraPanHandler.finish();\n        }\n        if (wasPanning && this.hasDragged) {\n            this.mapHoverHandler.update(event.pointer, true);\n            this.currentMode?.hover?.(this.getCurrentHover(), this.isMinimapHover);\n            return;\n        }\n        if (this.currentMode) {\n            if (event.button === 0) {\n                this.mapHoverHandler.update(event.pointer, true);\n                if (this.currentMode.execute(this.getCurrentHover(), this.isMinimapHover) !== false) {\n                    this.currentMode = undefined;\n                }\n            }\n            else if (event.button === 2 && this.isClickRange(event.pointer)) {\n                this.currentMode.cancel?.();\n                this.currentMode = undefined;\n                this.pointer.setPointerType(PointerType.Default);\n            }\n            return;\n        }\n        let boxSelectionHandled = false;\n        if (event.button === 0 && this.hasDragged) {\n            boxSelectionHandled = this.unitSelectionHandler.finishBoxSelect(event.pointer, !event.shiftKey);\n            if (!boxSelectionHandled) {\n                this.mapHoverHandler.update(event.pointer, true);\n            }\n        }\n        if (event.button !== 0 && event.button !== 2) {\n            return;\n        }\n        const rightClickMove = this.isRightClickMove();\n        const executeDefaultClick = event.button === (rightClickMove ? 2 : 0);\n        const isClick = this.isClickRange(event.pointer);\n        let isDoubleSameClick = false;\n        const isTouchLongPress = isClick && event.isTouch && event.timeStamp - this.lastMouseDownEvent.timeStamp >= 500;\n        if (event.isTouch) {\n            this.mapHoverHandler.update(event.pointer, true);\n        }\n        const hover = this.mapHoverHandler.getCurrentHover();\n        if (isClick) {\n            const lastClick = this.lastDefaultModeClickDetails;\n            const currentClick = {\n                mouseUpEvent: event,\n                hoverObject: hover?.gameObject,\n                selectionHash: this.unitSelectionHandler.getHash(),\n                time: Date.now(),\n            };\n            if (lastClick) {\n                isDoubleSameClick =\n                    currentClick.mouseUpEvent.button === lastClick.mouseUpEvent.button &&\n                        currentClick.hoverObject === lastClick.hoverObject &&\n                        currentClick.selectionHash === lastClick.selectionHash &&\n                        currentClick.time - lastClick.time < 500;\n            }\n            this.lastDefaultModeClickDetails = isDoubleSameClick ? undefined : currentClick;\n        }\n        if (!executeDefaultClick && (!rightClickMove || !event.shiftKey || event.ctrlKey) && (!rightClickMove || !isDoubleSameClick)) {\n            if (!isClick) {\n                return;\n            }\n            this.unitSelectionHandler.deselectAll();\n        }\n        if (!boxSelectionHandled && (rightClickMove ? executeDefaultClick : executeDefaultClick || event.button === 0)) {\n            this.handleDefaultClickAction(rightClickMove, executeDefaultClick, isDoubleSameClick, isTouchLongPress, event, hover);\n            if (this.lastDefaultModeClickDetails) {\n                this.lastDefaultModeClickDetails.selectionHash = this.unitSelectionHandler.getHash();\n            }\n        }\n    };\n    private readonly handleWheel = (event: any): void => {\n        this.cameraZoom.applyStep(event.wheelDeltaY > 0 ? -0.1 : 0.1);\n    };\n    private readonly handleMinimapClick = (tile: any): void => {\n        if (this.suppressNextMinimapClick) {\n            this.suppressNextMinimapClick = false;\n            return;\n        }\n        this.executeMinimapClickCommand(tile, false);\n    };\n    private readonly handleMinimapRightClick = (tile: any): void => {\n        this.executeMinimapClickCommand(tile, true);\n    };\n    private readonly handleMinimapMouseOver = (): void => {\n        this.isMinimapHover = true;\n    };\n    private readonly handleMinimapMouseMove = (tile: any): void => {\n        this.minimapHoverTile = tile;\n        if (this.minimapDragButton === 0) {\n            if (!this.hasDragged && !this.isClickRange(this.pointer.getPosition())) {\n                this.hasDragged = true;\n            }\n            this.minimapHandler.panToTile(tile);\n            this.pointer.setPointerType(PointerType.Mini);\n            return;\n        }\n        const hover = this.minimapHandler.getHover(tile);\n        if (this.currentMode) {\n            this.currentMode.hover(hover, true);\n        }\n        else {\n            this.updateDefaultAction(hover, this.unitSelectionHandler.getSelectedUnits(), this.lastKeyMods);\n        }\n    };\n    private readonly handleMinimapMouseOut = (): void => {\n        if (this.minimapDragButton !== undefined) {\n            return;\n        }\n        this.pointer.setPointerType(PointerType.Default);\n        this.isMinimapHover = false;\n        this.minimapHoverTile = undefined;\n    };\n    private processMouseMove(event: any): void {\n        if (this.minimapDragButton !== undefined) {\n            if (!this.hasDragged && !this.isClickRange(event.pointer)) {\n                this.hasDragged = true;\n            }\n            return;\n        }\n        const scrolling = this.mapScrollHandler.isScrolling();\n        if (this.mousePressed === undefined) {\n            if (!event.isTouch) {\n                this.mapScrollHandler.update(event.pointer);\n            }\n        }\n        else if (!this.hasDragged && !this.isClickRange(event.pointer)) {\n            this.hasDragged = true;\n            if (!this.currentMode && this.mousePressed === 0) {\n                this.unitSelectionHandler.startBoxSelect(this.clickOrigin);\n            }\n        }\n        if (this.currentMode &&\n            !this.mapScrollHandler.isScrolling() &&\n            !(this.maybePan && this.hasDragged)) {\n            if (!this.isMinimapHover && scrolling) {\n                this.pointer.setPointerType(PointerType.Default);\n            }\n            this.mapHoverHandler.update(event.pointer);\n            this.currentMode.hover(this.getCurrentHover(), this.isMinimapHover);\n        }\n        if (this.mousePressed === undefined) {\n            if (!this.mapScrollHandler.isScrolling()) {\n                this.mapHoverHandler.update(event.pointer);\n                if (!this.currentMode) {\n                    this.updateDefaultAction(this.getCurrentHover(), this.unitSelectionHandler.getSelectedUnits(), event);\n                }\n            }\n            return;\n        }\n        if (!this.hasDragged ||\n            (((this.currentMode || (this.isRightClickMove() && this.mousePressed === 2)) && !this.maybePan))) {\n            this.mapHoverHandler.update(event.pointer);\n        }\n        else {\n            this.mapHoverHandler.finish();\n        }\n        if (!this.hasDragged) {\n            return;\n        }\n        if (this.maybePan) {\n            this.cameraPanHandler.update(event.pointer, event.isTouch);\n            return;\n        }\n        if (!this.currentMode && !(this.isRightClickMove() && this.mousePressed === 2)) {\n            this.pointer.setPointerType(PointerType.Default);\n            this.unitSelectionHandler.updateBoxSelect(event.pointer);\n        }\n    }\n    private handleDefaultClickAction(rightClickMove: boolean, executeDefaultClick: boolean, allowTypeSelect: boolean, touchForceAttack: boolean, event: any, hover: any): void {\n        if (!hover) {\n            return;\n        }\n        const selection = this.unitSelectionHandler.getSelectedUnits();\n        const filter = rightClickMove\n            ? executeDefaultClick\n                ? ActionFilter.NoSelect\n                : ActionFilter.SelectOnly\n            : ActionFilter.All;\n        this.defaultActionHandler.execute(hover, selection, filter, rightClickMove && !executeDefaultClick, allowTypeSelect, touchForceAttack ? { ...event, ctrlKey: true } : event);\n    }\n    private cancelMouseUp(): void {\n        if (this.mousePressed === undefined) {\n            return;\n        }\n        this.pointerEvents.intersectionsEnabled = true;\n        this.mousePressed = undefined;\n        this.minimapDragButton = undefined;\n        this.suppressNextMinimapClick = false;\n        if (this.maybePan) {\n            this.maybePan = false;\n            this.cameraPanHandler.finish();\n        }\n        if (this.currentMode) {\n            this.currentMode.cancel?.();\n            this.currentMode = undefined;\n        }\n        this.unitSelectionHandler.cancelBoxSelect();\n    }\n    private cancelKeyUp(): void {\n        if (this.lastKeyboardEvent?.type !== 'keydown') {\n            return;\n        }\n        const synthetic = new KeyboardEvent('keyup', {\n            key: this.lastKeyboardEvent.key,\n            keyCode: this.lastKeyboardEvent.keyCode,\n            ctrlKey: this.lastKeyboardEvent.ctrlKey,\n            altKey: this.lastKeyboardEvent.altKey,\n            shiftKey: this.lastKeyboardEvent.shiftKey,\n            metaKey: this.lastKeyboardEvent.metaKey,\n        });\n        this.handleKeyUp(synthetic);\n    }\n    private isClickRange(pointer: {\n        x: number;\n        y: number;\n    }): boolean {\n        return Math.abs(pointer.x - this.clickOrigin.x) <= 7 && Math.abs(pointer.y - this.clickOrigin.y) <= 7;\n    }\n    private isRightClickPanAllowed(): boolean {\n        return this.rightClickScroll.value;\n    }\n    private isRightClickMove(): boolean {\n        return this.rightClickMove.value;\n    }\n    private executeMinimapClickCommand(tile: any, rightClick: boolean): void {\n        let handled = false;\n        if (rightClick === this.isRightClickMove()) {\n            const hover = this.minimapHandler.getHover(tile);\n            if (this.currentMode) {\n                if (this.currentMode.execute(hover, true) !== false) {\n                    this.currentMode = undefined;\n                    handled = true;\n                }\n            }\n            else {\n                const selection = this.unitSelectionHandler.getSelectedUnits();\n                handled = this.defaultActionHandler.execute(hover, selection, ActionFilter.All, false, false, this.lastKeyMods, true);\n            }\n        }\n        if (!handled) {\n            this.minimapHandler.panToTile(tile);\n        }\n    }\n    private getCurrentHover(): any {\n        if (this.isMinimapHover) {\n            return this.minimapHoverTile ? this.minimapHandler.getHover(this.minimapHoverTile) : undefined;\n        }\n        return this.mapHoverHandler.getCurrentHover();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/WorldInteractionFactory.ts",
    "content": "import { EntityIntersectHelper } from '@/engine/util/EntityIntersectHelper';\nimport { MapTileIntersectHelper } from '@/engine/util/MapTileIntersectHelper';\nimport { RaycastHelper } from '@/engine/util/RaycastHelper';\nimport { WorldViewportHelper } from '@/engine/util/WorldViewportHelper';\nimport { TargetLines } from '@/engine/renderable/entity/TargetLines';\nimport { MapPanningHelper } from '@/engine/util/MapPanningHelper';\nimport { DefaultActionHandler } from './DefaultActionHandler';\nimport { CameraPanHandler } from './CameraPanHandler';\nimport { MapScrollHandler } from './MapScrollHandler';\nimport { MapHoverHandler } from './MapHoverHandler';\nimport { TooltipHandler } from './TooltipHandler';\nimport { ArrowScrollHandler } from './ArrowScrollHandler';\nimport { CustomScrollHandler } from './CustomScrollHandler';\nimport { MinimapHandler } from './MinimapHandler';\nimport { UnitSelectionHandler } from './UnitSelectionHandler';\nimport { WorldInteraction } from './WorldInteraction';\nimport { KeyboardHandler } from './keyboard/KeyboardHandler';\nexport class WorldInteractionFactory {\n    constructor(private localPlayer: any, private game: any, private unitSelection: any, private renderableManager: any, private uiScene: any, private worldScene: any, private pointer: any, private renderer: any, private keyBinds: any, private generalOptions: any, private freeCamera: any, private debugPaths: any, private devMode: boolean, private document: Document, private minimap: any, private strings: any, private textColor: string, private debugText: any, private battleControlApi: any) { }\n    create(): any {\n        const map = this.game.map;\n        const worldScene = this.worldScene;\n        const pointer = this.pointer;\n        const renderer = this.renderer;\n        const mapTileIntersectHelper = new MapTileIntersectHelper(map, worldScene);\n        const raycastHelper = new RaycastHelper(this.worldScene);\n        const worldViewportHelper = new WorldViewportHelper(this.worldScene);\n        const entityIntersectHelper = new EntityIntersectHelper(map, this.renderableManager, mapTileIntersectHelper, raycastHelper, this.worldScene, worldViewportHelper);\n        const unitSelectionHandler = new UnitSelectionHandler(this.worldScene, this.uiScene, this.localPlayer, this.unitSelection, entityIntersectHelper, this.game.rules.general.veteran.veteranCap);\n        const defaultActionHandler = DefaultActionHandler.factory(this.renderableManager, this.unitSelection, unitSelectionHandler, this.localPlayer, map, this.game, this.game.rules.audioVisual);\n        const shroud = this.localPlayer ? this.game.mapShroudTrait.getPlayerShroud(this.localPlayer) : undefined;\n        const keyboardHandler = new KeyboardHandler(this.keyBinds, this.devMode);\n        const mapHoverHandler = new MapHoverHandler(entityIntersectHelper, mapTileIntersectHelper, map, shroud, renderer);\n        const mapScrollHandler = new MapScrollHandler(renderer.getCanvas(), worldScene.cameraPan, pointer, this.generalOptions.scrollRate, worldScene);\n        const tooltipHandler = new TooltipHandler(mapHoverHandler, this.textColor, pointer, this.uiScene, renderer, this.strings, this.debugText);\n        const arrowScrollHandler = new ArrowScrollHandler(mapScrollHandler);\n        const customScrollHandler = new CustomScrollHandler(mapScrollHandler);\n        const minimapHandler = new MinimapHandler(this.minimap, map, shroud, worldScene, new MapPanningHelper(map));\n        const targetLines = new TargetLines(this.localPlayer, this.unitSelection, worldScene.camera, this.debugPaths, this.generalOptions.targetLines);\n        const worldInteraction = new WorldInteraction(worldScene, pointer, pointer.pointerEvents, new CameraPanHandler(worldScene.cameraPan, pointer, this.generalOptions.scrollRate, this.freeCamera, worldScene), mapScrollHandler, mapHoverHandler, tooltipHandler, entityIntersectHelper, unitSelectionHandler, defaultActionHandler, keyboardHandler, arrowScrollHandler, customScrollHandler, minimapHandler, worldScene.cameraZoom, this.document, renderer, targetLines, this.generalOptions.rightClickMove, this.generalOptions.rightClickScroll, this.battleControlApi);\n        const debugRoot = ((window as any).__ra2debug ??= {});\n        debugRoot.entityIntersectHelper = entityIntersectHelper;\n        debugRoot.mapTileIntersectHelper = mapTileIntersectHelper;\n        return worldInteraction;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/KeyBinds.ts",
    "content": "import { DataStream } from '@/data/DataStream';\nimport { IniFile } from '@/data/IniFile';\nimport { VirtualFile } from '@/data/vfs/VirtualFile';\nimport { KeyCommandType } from './KeyCommandType';\nconst numpadArrowMap = new Map([\n    [98, 40],\n    [100, 37],\n    [102, 39],\n    [104, 38],\n]);\nexport class KeyBinds {\n    static iniSection = \"Hotkey\";\n    private configDir: any;\n    private persistFileName: string;\n    private defaultIni: any;\n    private hotKeys: Map<number, string>;\n    constructor(configDir: any, persistFileName: string, defaultIni: any) {\n        this.configDir = configDir;\n        this.persistFileName = persistFileName;\n        this.defaultIni = defaultIni;\n        this.hotKeys = new Map();\n    }\n    async load(): Promise<void> {\n        this.hotKeys.clear();\n        let useDefault = true;\n        let iniFile: any;\n        try {\n            if (this.configDir &&\n                (await this.configDir.containsEntry(this.persistFileName))) {\n                iniFile = new IniFile(await this.configDir.openFile(this.persistFileName));\n                this.loadHotKeys(iniFile);\n                useDefault = false;\n            }\n        }\n        catch (error) {\n            console.log(`Failed to load hotkeys from local file \"${this.persistFileName}\"`, error);\n        }\n        if (useDefault) {\n            iniFile = this.defaultIni;\n            for (const [commandType, keyCode] of new Map([\n                [KeyCommandType.PreviousObject, \"M\".charCodeAt(0)],\n                [KeyCommandType.VeterancyNav, \"Y\".charCodeAt(0)],\n                [KeyCommandType.HealthNav, \"U\".charCodeAt(0)],\n                [KeyCommandType.FreeMoney, 582],\n                [KeyCommandType.BuildCheat, 593],\n                [KeyCommandType.ToggleFps, 512 + \"R\".charCodeAt(0)],\n                [KeyCommandType.ToggleShroud, 1024 + \"S\".charCodeAt(0)],\n            ])) {\n                this.addHotKey(commandType, keyCode);\n            }\n            this.loadHotKeys(iniFile);\n        }\n        this.addHotKey(KeyCommandType.Scoreboard, 9);\n    }\n    async saveIni(iniFile: any): Promise<void> {\n        await this.configDir?.writeFile(new VirtualFile(new DataStream().writeString(iniFile.toString()), this.persistFileName));\n    }\n    async resetAndReload(): Promise<void> {\n        if (this.configDir &&\n            (await this.configDir.containsEntry(this.persistFileName))) {\n            await this.configDir.deleteFile(this.persistFileName);\n        }\n        await this.load();\n    }\n    loadHotKeys(iniFile: any): this {\n        const section = iniFile.getSection(KeyBinds.iniSection);\n        if (!section)\n            throw new Error(`Missing [${KeyBinds.iniSection}] ini section`);\n        const commandTypes = Object.keys(KeyCommandType);\n        for (const key of section.entries.keys()) {\n            if (commandTypes.includes(key)) {\n                const keyCode = section.getNumber(key);\n                this.changeHotKey(key, keyCode);\n            }\n            else {\n                console.warn(\"Unknown keyboard command \" + key);\n            }\n        }\n        return this;\n    }\n    async save(): Promise<void> {\n        const iniFile = new IniFile();\n        const section = iniFile.getOrCreateSection(KeyBinds.iniSection);\n        for (const [keyCode, commandType] of this.hotKeys) {\n            section.set(commandType, \"\" + keyCode);\n        }\n        await this.saveIni(iniFile);\n    }\n    addHotKey(commandType: string, keyCode: number | KeyboardEvent): void {\n        this.hotKeys.set(typeof keyCode === \"number\" ? keyCode : this.getHotKeyCode(keyCode), commandType);\n    }\n    changeHotKey(commandType: string, keyCode: number): void {\n        for (const hotKeyCode of [...this.hotKeys.entries()]\n            .filter(([, type]) => type === commandType)\n            .map(([code]) => code)) {\n            this.hotKeys.delete(hotKeyCode);\n        }\n        if (keyCode) {\n            this.addHotKey(commandType, keyCode);\n        }\n    }\n    getCommandType(keyEvent: KeyboardEvent): string | undefined {\n        if (!(255 < keyEvent.keyCode)) {\n            const hotKeyCode = this.getHotKeyCode(keyEvent);\n            return this.hotKeys.get(hotKeyCode);\n        }\n    }\n    getHotKeyCode(keyEvent: KeyboardEvent): number {\n        let code = (Number(keyEvent.metaKey) << 12) +\n            (Number(keyEvent.altKey) << 10) +\n            (Number(keyEvent.ctrlKey) << 9) +\n            (Number(keyEvent.shiftKey) << 8) +\n            keyEvent.keyCode;\n        const arrowKey = numpadArrowMap.get(keyEvent.keyCode);\n        if (arrowKey) {\n            code += 2048 - keyEvent.keyCode + arrowKey;\n        }\n        return code;\n    }\n    getHotKey(commandType: string): KeyboardEvent | undefined {\n        const hotKeyCode = [...this.hotKeys.entries()].find(([, type]) => type === commandType)?.[0];\n        if (hotKeyCode !== undefined) {\n            let keyCode = 255 & hotKeyCode;\n            if (2048 & hotKeyCode) {\n                const originalKey = [...numpadArrowMap].find(([, arrow]) => arrow === keyCode)?.[0];\n                if (originalKey) {\n                    keyCode = originalKey;\n                }\n                else {\n                    console.error(`Expected an numpad arrow key code but got ${keyCode} (${hotKeyCode}) instead`);\n                }\n            }\n            return {\n                keyCode: keyCode,\n                shiftKey: Boolean(256 & hotKeyCode),\n                ctrlKey: Boolean(512 & hotKeyCode),\n                altKey: Boolean(1024 & hotKeyCode),\n                metaKey: Boolean(4096 & hotKeyCode),\n            } as KeyboardEvent;\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/KeyCommand.ts",
    "content": "export enum TriggerMode {\n    KeyDown = 0,\n    KeyUp = 1,\n    KeyDownUp = 2\n}\nexport interface KeyCommand {\n    triggerMode: TriggerMode;\n    execute(isKeyUp: boolean): void;\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/KeyCommandType.ts",
    "content": "export enum KeyCommandType {\n    CenterView = \"CenterView\",\n    Options = \"Options\",\n    CenterOnRadarEvent = \"CenterOnRadarEvent\",\n    RightSidebarUp = \"RightSidebarUp\",\n    RightSidebarDown = \"RightSidebarDown\",\n    LeftSidebarDown = \"LeftSidebarDown\",\n    LeftSidebarUp = \"LeftSidebarUp\",\n    Delete = \"Delete\",\n    TeamSelect_10 = \"TeamSelect_10\",\n    TeamSelect_1 = \"TeamSelect_1\",\n    TeamSelect_2 = \"TeamSelect_2\",\n    TeamSelect_3 = \"TeamSelect_3\",\n    TeamSelect_4 = \"TeamSelect_4\",\n    TeamSelect_5 = \"TeamSelect_5\",\n    TeamSelect_6 = \"TeamSelect_6\",\n    TeamSelect_7 = \"TeamSelect_7\",\n    TeamSelect_8 = \"TeamSelect_8\",\n    TeamSelect_9 = \"TeamSelect_9\",\n    ToggleAlliance = \"ToggleAlliance\",\n    PlaceBeacon = \"PlaceBeacon\",\n    AllToCheer = \"AllToCheer\",\n    DeployObject = \"DeployObject\",\n    InfantryTab = \"InfantryTab\",\n    Follow = \"Follow\",\n    GuardObject = \"GuardObject\",\n    CenterBase = \"CenterBase\",\n    ToggleRepair = \"ToggleRepair\",\n    ToggleSell = \"ToggleSell\",\n    PreviousObject = \"PreviousObject\",\n    NextObject = \"NextObject\",\n    CombatantSelect = \"CombatantSelect\",\n    StructureTab = \"StructureTab\",\n    UnitTab = \"UnitTab\",\n    StopObject = \"StopObject\",\n    TypeSelect = \"TypeSelect\",\n    PageUser = \"PageUser\",\n    DefenseTab = \"DefenseTab\",\n    ScatterObject = \"ScatterObject\",\n    HealthNav = \"HealthNav\",\n    VeterancyNav = \"VeterancyNav\",\n    PlanningMode = \"PlanningMode\",\n    View1 = \"View1\",\n    View2 = \"View2\",\n    View3 = \"View3\",\n    View4 = \"View4\",\n    Taunt_1 = \"Taunt_1\",\n    Taunt_2 = \"Taunt_2\",\n    Taunt_3 = \"Taunt_3\",\n    Taunt_4 = \"Taunt_4\",\n    Taunt_5 = \"Taunt_5\",\n    Taunt_6 = \"Taunt_6\",\n    Taunt_7 = \"Taunt_7\",\n    Taunt_8 = \"Taunt_8\",\n    RaiseCell = \"RaiseCell\",\n    LowerCell = \"LowerCell\",\n    DeleteObject = \"DeleteObject\",\n    TeamAddSelect_10 = \"TeamAddSelect_10\",\n    TeamAddSelect_1 = \"TeamAddSelect_1\",\n    TeamAddSelect_2 = \"TeamAddSelect_2\",\n    TeamAddSelect_3 = \"TeamAddSelect_3\",\n    TeamAddSelect_4 = \"TeamAddSelect_4\",\n    TeamAddSelect_5 = \"TeamAddSelect_5\",\n    TeamAddSelect_6 = \"TeamAddSelect_6\",\n    TeamAddSelect_7 = \"TeamAddSelect_7\",\n    TeamAddSelect_8 = \"TeamAddSelect_8\",\n    TeamAddSelect_9 = \"TeamAddSelect_9\",\n    IonStorm = \"IonStorm\",\n    TeamCreate_10 = \"TeamCreate_10\",\n    TeamCreate_1 = \"TeamCreate_1\",\n    TeamCreate_2 = \"TeamCreate_2\",\n    TeamCreate_3 = \"TeamCreate_3\",\n    TeamCreate_4 = \"TeamCreate_4\",\n    TeamCreate_5 = \"TeamCreate_5\",\n    TeamCreate_6 = \"TeamCreate_6\",\n    TeamCreate_7 = \"TeamCreate_7\",\n    TeamCreate_8 = \"TeamCreate_8\",\n    TeamCreate_9 = \"TeamCreate_9\",\n    ScreenCapture = \"ScreenCapture\",\n    ToggleElite = \"ToggleElite\",\n    FreeMoney = \"FreeMoney\",\n    IonBlast = \"IonBlast\",\n    LightningBolt = \"LightningBolt\",\n    ToggleMono = \"ToggleMono\",\n    NukeExplosion = \"NukeExplosion\",\n    BuildCheat = \"BuildCheat\",\n    ToggleShroud = \"ToggleShroud\",\n    ToggleThreatPrint = \"ToggleThreatPrint\",\n    SpecialWeapons = \"SpecialWeapons\",\n    ToggleAttackFriendlies = \"ToggleAttackFriendlies\",\n    SetView1 = \"SetView1\",\n    SetView2 = \"SetView2\",\n    SetView3 = \"SetView3\",\n    SetView4 = \"SetView4\",\n    Explosion = \"Explosion\",\n    PrevMonoPage = \"PrevMonoPage\",\n    NextMonoPage = \"NextMonoPage\",\n    TeamCenter_10 = \"TeamCenter_10\",\n    TeamCenter_1 = \"TeamCenter_1\",\n    TeamCenter_2 = \"TeamCenter_2\",\n    TeamCenter_3 = \"TeamCenter_3\",\n    TeamCenter_4 = \"TeamCenter_4\",\n    TeamCenter_5 = \"TeamCenter_5\",\n    TeamCenter_6 = \"TeamCenter_6\",\n    TeamCenter_7 = \"TeamCenter_7\",\n    TeamCenter_8 = \"TeamCenter_8\",\n    TeamCenter_9 = \"TeamCenter_9\",\n    ToggleMarbleMadness = \"ToggleMarbleMadness\",\n    ForceWin = \"ForceWin\",\n    BailOut = \"BailOut\",\n    SuperExplosion = \"SuperExplosion\",\n    SidebarPageUp = \"SidebarPageUp\",\n    SidebarUp = \"SidebarUp\",\n    SidebarPageDown = \"SidebarPageDown\",\n    SidebarDown = \"SidebarDown\",\n    ToggleFps = \"ToggleFps\",\n    Scoreboard = \"Scoreboard\"\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/KeyboardHandler.ts",
    "content": "import { KeyCommandType } from './KeyCommandType';\nimport { TriggerMode, KeyCommand } from './KeyCommand';\nexport class KeyboardHandler {\n    static anyModifierCommands = [KeyCommandType.PlanningMode];\n    private keyBinds: any;\n    private devMode: boolean;\n    private commands: Map<string, any>;\n    private isPaused: boolean;\n    constructor(keyBinds: any, devMode: boolean) {\n        this.keyBinds = keyBinds;\n        this.devMode = devMode;\n        this.commands = new Map();\n        this.isPaused = false;\n    }\n    registerCommand(commandType: string, command: any): void {\n        if (this.commands.has(commandType))\n            throw new Error(\"Duplicate command \" + commandType);\n        this.commands.set(commandType, command);\n    }\n    unregisterCommand(commandType: string): void {\n        this.commands.delete(commandType);\n    }\n    executeCommand(commandType: string): void {\n        const command = this.commands.get(commandType);\n        if (command && !this.isPaused) {\n            if (typeof command === \"function\") {\n                command();\n            }\n            else if (command.triggerMode !== TriggerMode.KeyDownUp) {\n                command.execute(command.triggerMode === TriggerMode.KeyUp);\n            }\n            else {\n                command.execute(false);\n                command.execute(true);\n            }\n        }\n    }\n    handleKeyDown(keyEvent: KeyboardEvent): void {\n        if (keyEvent.key === \"Backspace\") {\n            keyEvent.preventDefault();\n            keyEvent.stopPropagation();\n        }\n        if (!(keyEvent.repeat ||\n            ([\"F5\", \"F12\"].includes(keyEvent.key) && this.devMode))) {\n            let commandType = this.keyBinds.getCommandType(keyEvent);\n            if (commandType === undefined) {\n                commandType = this.getNoModCmdType(keyEvent.keyCode);\n            }\n            if (commandType !== undefined) {\n                keyEvent.preventDefault();\n                keyEvent.stopPropagation();\n                const command = this.commands.get(commandType);\n                if (command && !this.isPaused) {\n                    if (typeof command === \"function\") {\n                        command();\n                    }\n                    else if (command.triggerMode !== TriggerMode.KeyUp) {\n                        command.execute(false);\n                    }\n                }\n            }\n        }\n    }\n    handleKeyUp(keyEvent: KeyboardEvent): void {\n        if (keyEvent.key === \"Alt\") {\n            keyEvent.preventDefault();\n            keyEvent.stopPropagation();\n        }\n        else if (!this.isPaused) {\n            let commandType = this.keyBinds.getCommandType(keyEvent);\n            if (commandType === undefined) {\n                commandType = this.getNoModCmdType(keyEvent.keyCode);\n            }\n            if (commandType !== undefined) {\n                const command = this.commands.get(commandType);\n                if (command &&\n                    typeof command !== \"function\" &&\n                    (command.triggerMode === TriggerMode.KeyUp ||\n                        command.triggerMode === TriggerMode.KeyDownUp)) {\n                    command.execute(true);\n                }\n            }\n        }\n    }\n    getNoModCmdType(keyCode: number): string | undefined {\n        const commandType = this.keyBinds.getCommandType({\n            keyCode: keyCode,\n            altKey: false,\n            ctrlKey: false,\n            shiftKey: false,\n            metaKey: false,\n        });\n        if (commandType) {\n            const command = this.commands.get(commandType);\n            if (command &&\n                typeof command !== \"function\" &&\n                KeyboardHandler.anyModifierCommands.includes(commandType)) {\n                return commandType;\n            }\n        }\n    }\n    pause(): void {\n        this.isPaused = true;\n    }\n    unpause(): void {\n        this.isPaused = false;\n    }\n    dispose(): void {\n        this.commands.clear();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/command/CenterBaseCmd.ts",
    "content": "import { ObjectType } from '@/engine/type/ObjectType';\nimport { TechnoRules, FactoryType } from '@/game/rules/TechnoRules';\nexport class CenterBaseCmd {\n    private player: any;\n    private rules: any;\n    private mapPanningHelper: any;\n    private cameraPan: any;\n    constructor(player: any, rules: any, mapPanningHelper: any, cameraPan: any) {\n        this.player = player;\n        this.rules = rules;\n        this.mapPanningHelper = mapPanningHelper;\n        this.cameraPan = cameraPan;\n    }\n    execute(): void {\n        let tile: any;\n        const primaryFactory = this.player.production.getPrimaryFactory(FactoryType.BuildingType);\n        if (primaryFactory) {\n            tile = primaryFactory.centerTile;\n        }\n        else {\n            const baseUnit = this.player\n                .getOwnedObjectsByType(ObjectType.Vehicle)\n                .find((unit: any) => this.rules.general.baseUnit.includes(unit.name));\n            if (baseUnit) {\n                tile = baseUnit.tile;\n            }\n        }\n        if (tile) {\n            this.cameraPan.setPan(this.mapPanningHelper.computeCameraPanFromTile(tile.rx, tile.ry));\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/command/CenterGroupCmd.ts",
    "content": "export class CenterGroupCmd {\n    private groupNum: number;\n    private unitSelectionHandler: any;\n    private mapPanningHelper: any;\n    private cameraPan: any;\n    constructor(groupNum: number, unitSelectionHandler: any, mapPanningHelper: any, cameraPan: any) {\n        this.groupNum = groupNum;\n        this.unitSelectionHandler = unitSelectionHandler;\n        this.mapPanningHelper = mapPanningHelper;\n        this.cameraPan = cameraPan;\n    }\n    execute(): void {\n        this.unitSelectionHandler.selectGroup(this.groupNum);\n        const units = this.unitSelectionHandler.getGroupUnits(this.groupNum);\n        if (units.length) {\n            const panTile = this.computePanTile(units);\n            const cameraPan = this.mapPanningHelper.computeCameraPanFromTile(panTile.rx, panTile.ry);\n            this.cameraPan.setPan(cameraPan);\n        }\n    }\n    computePanTile(units: any[]): {\n        rx: number;\n        ry: number;\n    } {\n        return {\n            rx: Math.floor(units.reduce((sum, unit) => sum + unit.tile.rx, 0) / units.length),\n            ry: Math.floor(units.reduce((sum, unit) => sum + unit.tile.ry, 0) / units.length),\n        };\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/command/CenterViewCmd.ts",
    "content": "export class CenterViewCmd {\n    private unitSelectionHandler: any;\n    private mapPanningHelper: any;\n    private cameraPan: any;\n    constructor(unitSelectionHandler: any, mapPanningHelper: any, cameraPan: any) {\n        this.unitSelectionHandler = unitSelectionHandler;\n        this.mapPanningHelper = mapPanningHelper;\n        this.cameraPan = cameraPan;\n    }\n    execute(): void {\n        const selectedUnits = this.unitSelectionHandler.getSelectedUnits();\n        if (selectedUnits.length) {\n            const panTile = this.computePanTile(selectedUnits);\n            const cameraPan = this.mapPanningHelper.computeCameraPanFromTile(panTile.rx, panTile.ry);\n            this.cameraPan.setPan(cameraPan);\n        }\n    }\n    computePanTile(units: any[]): {\n        rx: number;\n        ry: number;\n    } {\n        return {\n            rx: Math.floor(units.reduce((sum, unit) => sum + unit.tile.rx, 0) / units.length),\n            ry: Math.floor(units.reduce((sum, unit) => sum + unit.tile.ry, 0) / units.length),\n        };\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/command/FollowUnitCmd.ts",
    "content": "import { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nexport class FollowUnitCmd {\n    private unitSelectionHandler: any;\n    private renderableManager: any;\n    private worldInteraction: any;\n    private mapPanningHelper: any;\n    private cameraPan: any;\n    private worldScene: any;\n    private disposables: any;\n    private unit?: any;\n    private handleUserSelectionChange: () => void;\n    private handleFrame: () => void;\n    constructor(unitSelectionHandler: any, renderableManager: any, worldInteraction: any, mapPanningHelper: any, cameraPan: any, worldScene: any) {\n        this.unitSelectionHandler = unitSelectionHandler;\n        this.renderableManager = renderableManager;\n        this.worldInteraction = worldInteraction;\n        this.mapPanningHelper = mapPanningHelper;\n        this.cameraPan = cameraPan;\n        this.worldScene = worldScene;\n        this.disposables = new CompositeDisposable();\n        this.handleUserSelectionChange = () => {\n            this.updateUnit(undefined);\n        };\n        this.handleFrame = () => {\n            const selectedUnits = this.unitSelectionHandler.getSelectedUnits();\n            if (this.unit && !selectedUnits.includes(this.unit)) {\n                this.updateUnit(undefined);\n            }\n            if (this.unit) {\n                this.updatePan(this.unit);\n            }\n        };\n    }\n    init(): void {\n        this.unitSelectionHandler.onUserSelectionUpdate.subscribe(this.handleUserSelectionChange);\n        this.disposables.add(() => this.unitSelectionHandler.onUserSelectionUpdate.unsubscribe(this.handleUserSelectionChange));\n        this.worldScene.onBeforeCameraUpdate.subscribe(this.handleFrame);\n        this.disposables.add(() => this.worldScene.onBeforeCameraUpdate.unsubscribe(this.handleFrame));\n    }\n    execute(): void {\n        const selectedUnits = this.unitSelectionHandler.getSelectedUnits();\n        if (this.unit && !selectedUnits.includes(this.unit)) {\n            this.updateUnit(undefined);\n        }\n        this.updateUnit(this.unit ? undefined : selectedUnits[0]);\n        if (this.unit) {\n            this.updatePan(this.unit);\n        }\n    }\n    updateUnit(unit: any): void {\n        this.unit = unit;\n        if (unit) {\n            this.worldInteraction.pausePanning();\n        }\n        else {\n            this.worldInteraction.unpausePanning();\n        }\n    }\n    updatePan(unit: any): void {\n        const renderable = this.renderableManager.getRenderableByGameObject(unit);\n        if (renderable) {\n            const cameraPan = this.mapPanningHelper.computeCameraPanFromWorld(renderable.getPosition());\n            this.cameraPan.setPan(cameraPan);\n        }\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/command/GoToCameraLocationCmd.ts",
    "content": "export class GoToCameraLocationCmd {\n    private cameraPan: any;\n    private cameraLocations: Map<any, any>;\n    private idx: any;\n    private defaultLocation: any;\n    constructor(cameraPan: any, cameraLocations: Map<any, any>, idx: any, defaultLocation: any) {\n        this.cameraPan = cameraPan;\n        this.cameraLocations = cameraLocations;\n        this.idx = idx;\n        this.defaultLocation = defaultLocation;\n    }\n    execute(): void {\n        const location = this.cameraLocations.get(this.idx) || this.defaultLocation;\n        if (location) {\n            this.cameraPan.setPan(location);\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/command/LastRadarEventCmd.ts",
    "content": "import { EventType } from '@/game/event/EventType';\nimport { SuperWeaponType } from '@/game/type/SuperWeaponType';\nexport class LastRadarEventCmd {\n    private player: any;\n    private mapPanningHelper: any;\n    private cameraPan: any;\n    private eventHistory: any[];\n    private eventPointer: number;\n    private lastRun?: number;\n    constructor(player: any, mapPanningHelper: any, cameraPan: any) {\n        this.player = player;\n        this.mapPanningHelper = mapPanningHelper;\n        this.cameraPan = cameraPan;\n        this.eventHistory = [];\n        this.eventPointer = -1;\n    }\n    execute(): void {\n        if (this.eventHistory.length) {\n            if (this.lastRun) {\n                const now = Date.now();\n                const timeDiff = now - this.lastRun;\n                this.lastRun = now;\n                if (timeDiff > 400) {\n                    this.eventPointer = this.eventHistory.length - 1;\n                }\n                else {\n                    this.eventPointer--;\n                    if (this.eventPointer < 0) {\n                        this.eventPointer = this.eventHistory.length - 1;\n                    }\n                }\n            }\n            else {\n                this.lastRun = Date.now();\n            }\n            const event = this.eventHistory[this.eventPointer];\n            if (event) {\n                const cameraPan = this.mapPanningHelper.computeCameraPanFromTile(event.rx, event.ry);\n                this.cameraPan.setPan(cameraPan);\n            }\n        }\n    }\n    recordEvent(tile: any): void {\n        this.eventHistory.push(tile);\n        this.eventHistory = this.eventHistory.slice(-8);\n        this.eventPointer = this.eventHistory.length - 1;\n    }\n    handleGameEvent(gameEvent: any): void {\n        switch (gameEvent.type) {\n            case EventType.RadarEvent:\n                if (gameEvent.target === this.player) {\n                    this.recordEvent(gameEvent.tile);\n                }\n                break;\n            case EventType.BridgeRepair:\n                if (gameEvent.source === this.player) {\n                    this.recordEvent(gameEvent.tile);\n                }\n                break;\n            case EventType.ObjectDestroy:\n                {\n                    const target = gameEvent.target;\n                    if (target.isUnit() &&\n                        target.owner === this.player) {\n                        this.recordEvent(target.tile);\n                    }\n                    if (target.isProjectile() && target.isNuke) {\n                        this.recordEvent(target.tile);\n                    }\n                }\n                break;\n            case EventType.FactoryProduceUnit:\n                const target = gameEvent.target;\n                if (target.owner === this.player) {\n                    this.recordEvent(target.tile);\n                }\n                break;\n            case EventType.SuperWeaponActivate:\n                const superWeaponEvent = gameEvent;\n                if ([\n                    SuperWeaponType.IronCurtain,\n                    SuperWeaponType.ChronoSphere,\n                ].includes(superWeaponEvent.target)) {\n                    this.recordEvent(superWeaponEvent.atTile2 ?? superWeaponEvent.atTile);\n                }\n                break;\n            case EventType.LightningStormManifest:\n                this.recordEvent(gameEvent.target);\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/command/SelectGroupCmd.ts",
    "content": "export class SelectGroupCmd {\n    private groupNum: number;\n    private unitSelectionHandler: any;\n    private targetLines: any;\n    private mapPanningHelper: any;\n    private cameraPan: any;\n    private lastSelectTime?: number;\n    constructor(groupNum: number, unitSelectionHandler: any, targetLines: any, mapPanningHelper: any, cameraPan: any) {\n        this.groupNum = groupNum;\n        this.unitSelectionHandler = unitSelectionHandler;\n        this.targetLines = targetLines;\n        this.mapPanningHelper = mapPanningHelper;\n        this.cameraPan = cameraPan;\n    }\n    execute(): void {\n        this.unitSelectionHandler.selectGroup(this.groupNum);\n        this.targetLines.forceShow();\n        const now = performance.now();\n        let shouldCenter = true;\n        if (!this.lastSelectTime || now - this.lastSelectTime > 400) {\n            shouldCenter = false;\n            this.lastSelectTime = now;\n        }\n        if (shouldCenter) {\n            const selectedUnits = this.unitSelectionHandler.getSelectedUnits();\n            if (selectedUnits.length) {\n                const panTile = this.computePanTile(selectedUnits);\n                const cameraPan = this.mapPanningHelper.computeCameraPanFromTile(panTile.rx, panTile.ry);\n                this.cameraPan.setPan(cameraPan);\n            }\n        }\n    }\n    computePanTile(units: any[]): {\n        rx: number;\n        ry: number;\n    } {\n        return {\n            rx: Math.floor(units.reduce((sum, unit) => sum + unit.tile.rx, 0) / units.length),\n            ry: Math.floor(units.reduce((sum, unit) => sum + unit.tile.ry, 0) / units.length),\n        };\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/command/SelectNextUnitCmd.ts",
    "content": "import { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nexport class SelectNextUnitCmd {\n    private unitSelectionHandler: any;\n    private mapPanningHelper: any;\n    private cameraPan: any;\n    private player: any;\n    private world: any;\n    private reverse: boolean;\n    private unitList: any[];\n    private disposables: any;\n    private generator?: Generator<any, void, unknown>;\n    private lastSelectionHash?: string;\n    constructor(unitSelectionHandler: any, mapPanningHelper: any, cameraPan: any, player: any, world: any) {\n        this.unitSelectionHandler = unitSelectionHandler;\n        this.mapPanningHelper = mapPanningHelper;\n        this.cameraPan = cameraPan;\n        this.player = player;\n        this.world = world;\n        this.reverse = false;\n        this.unitList = [];\n        this.disposables = new CompositeDisposable();\n        const onObjectSpawned = (gameObject: any) => {\n            if (gameObject.isTechno() && gameObject.owner === player) {\n                this.unitList.push(gameObject);\n            }\n        };\n        this.world.onObjectSpawned.subscribe(onObjectSpawned);\n        this.disposables.add(() => this.world.onObjectSpawned.unsubscribe(onObjectSpawned));\n    }\n    getNextUnit(): any {\n        if (!this.generator) {\n            this.generator = this.generate();\n        }\n        return this.generator.next().value;\n    }\n    *generate(): Generator<any, void, unknown> {\n        while (true) {\n            const sortedUnits = (this.unitList = this.player\n                .getOwnedObjects()\n                .filter((obj: any) => obj.isUnit())\n                .sort((a: any, b: any) => a.tile.dx +\n                1000 * a.tile.dy -\n                (b.tile.dx + 1000 * b.tile.dy) +\n                0.1 * (b.position.subCell - a.position.subCell)));\n            if (sortedUnits.length) {\n                let index = this.reverse ? sortedUnits.length : -1;\n                const selectedUnits = this.unitSelectionHandler.getSelectedUnits();\n                if (selectedUnits.length > 1 &&\n                    selectedUnits[0].isUnit()) {\n                    const foundIndex = sortedUnits.indexOf(selectedUnits[0]);\n                    if (foundIndex !== -1) {\n                        index = foundIndex;\n                    }\n                }\n                while (this.reverse ? --index >= 0 : ++index < sortedUnits.length) {\n                    if (this.unitSelectionHandler.getHash() !== this.lastSelectionHash) {\n                        this.lastSelectionHash = this.unitSelectionHandler.getHash();\n                        break;\n                    }\n                    const unit = sortedUnits[index];\n                    if (unit.owner === this.player && unit.isSpawned) {\n                        yield unit;\n                    }\n                }\n            }\n            else {\n                yield undefined;\n            }\n        }\n    }\n    setReverse(reverse: boolean): void {\n        this.reverse = reverse;\n    }\n    execute(): void {\n        const nextUnit = this.getNextUnit();\n        if (nextUnit) {\n            this.unitSelectionHandler.selectSingleUnit(nextUnit);\n            this.lastSelectionHash = this.unitSelectionHandler.getHash();\n            const tile = nextUnit.tile;\n            const cameraPan = this.mapPanningHelper.computeCameraPanFromTile(tile.rx, tile.ry);\n            this.cameraPan.setPan(cameraPan);\n        }\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/command/SelectPlayerCmd.ts",
    "content": "import { CenterBaseCmd } from './CenterBaseCmd';\nexport class SelectPlayerCmd {\n    private playerNum: number;\n    private player: any;\n    private mapPanningHelper: any;\n    private cameraPan: any;\n    private game: any;\n    private lastSelectTime?: number;\n    constructor(playerNum: number, player: any, mapPanningHelper: any, cameraPan: any, game: any) {\n        this.playerNum = playerNum;\n        this.player = player;\n        this.mapPanningHelper = mapPanningHelper;\n        this.cameraPan = cameraPan;\n        this.game = game;\n    }\n    execute(): void {\n        const now = performance.now();\n        let shouldCenter = true;\n        if (!this.lastSelectTime || now - this.lastSelectTime > 400) {\n            shouldCenter = false;\n            this.lastSelectTime = now;\n        }\n        let selectedPlayer: any = undefined;\n        const combatants = this.game.getCombatants();\n        if (this.playerNum < combatants.length) {\n            selectedPlayer = combatants[this.playerNum];\n        }\n        if (selectedPlayer &&\n            (this.player.value === selectedPlayer ||\n                (shouldCenter && !this.player.value))) {\n            if (shouldCenter) {\n                const centerBaseCmd = new CenterBaseCmd(selectedPlayer, this.game.rules, this.mapPanningHelper, this.cameraPan);\n                centerBaseCmd.execute();\n            }\n            else {\n                selectedPlayer = undefined;\n            }\n        }\n        this.player.value = selectedPlayer;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/command/SelectTypeByCmd.ts",
    "content": "import { TriggerMode } from '../KeyCommand';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nexport class SelectByTypeCmd {\n    public triggerMode = TriggerMode.KeyDownUp;\n    private unitSelectionHandler: any;\n    private disposables: any;\n    private keyDownTime?: number;\n    private handleUserSelectionUpdate: (e: any) => void;\n    constructor(unitSelectionHandler: any) {\n        this.unitSelectionHandler = unitSelectionHandler;\n        this.disposables = new CompositeDisposable();\n        this.handleUserSelectionUpdate = (selectionUpdate: any) => {\n            if (!selectionUpdate.queryType &&\n                this.keyDownTime) {\n                this.unitSelectionHandler.selectByType();\n            }\n        };\n    }\n    init(): void {\n        this.unitSelectionHandler.onUserSelectionUpdate.subscribe(this.handleUserSelectionUpdate);\n        this.disposables.add(() => this.unitSelectionHandler.onUserSelectionUpdate.unsubscribe(this.handleUserSelectionUpdate));\n    }\n    execute(isKeyUp: boolean): void {\n        const now = Date.now();\n        if (isKeyUp) {\n            if (this.keyDownTime &&\n                now - this.keyDownTime <= 1000) {\n                this.unitSelectionHandler.selectByType();\n            }\n            this.keyDownTime = undefined;\n        }\n        else {\n            if (this.keyDownTime === undefined) {\n                this.keyDownTime = now;\n            }\n        }\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/keyboard/command/SetCameraLocationCmd.ts",
    "content": "export class SetCameraLocationCmd {\n    private cameraPan: any;\n    private cameraLocations: Map<any, any>;\n    private idx: any;\n    constructor(cameraPan: any, cameraLocations: Map<any, any>, idx: any) {\n        this.cameraPan = cameraPan;\n        this.cameraLocations = cameraLocations;\n        this.idx = idx;\n    }\n    execute(): void {\n        this.cameraLocations.set(this.idx, this.cameraPan.getPan());\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/placementMode/PlacementGrid.ts",
    "content": "import * as THREE from 'three';\nimport { Coords } from '@/game/Coords';\nimport { rampHeights } from '@/game/theater/rampHeights';\nimport { OverlayUtils } from '@/engine/gfx/OverlayUtils';\nimport { pointEquals } from '@/util/geometry';\nimport { SpriteUtils } from '@/engine/gfx/SpriteUtils';\nimport { IsoCoords } from '@/engine/IsoCoords';\nexport class PlacementGrid {\n    private target?: THREE.Object3D;\n    private tilesObject?: THREE.Object3D;\n    private rangeObject?: THREE.Line;\n    private readonly tileOverlays = new Map<number, THREE.Mesh>();\n    private textureCache?: THREE.Texture;\n    private lastRangeCircle?: any;\n    constructor(private readonly viewModel: any, private readonly camera: any, private readonly mapTiles: any) { }\n    get3DObject(): THREE.Object3D | undefined {\n        return this.target;\n    }\n    create3DObject(): void {\n        const object = new THREE.Object3D();\n        object.name = 'placement_grid';\n        this.target = object;\n        this.createTileOverlays();\n    }\n    update(): void {\n        this.refreshRangeCircle();\n        if (this.viewModel.visible || !this.tilesObject) {\n            const tilesContainer = new THREE.Object3D();\n            tilesContainer.visible = true;\n            for (const tile of this.viewModel.tiles) {\n                const mapTile = this.mapTiles.getByMapCoords(tile.rx, tile.ry);\n                if (!mapTile) {\n                    throw new Error(`Map tile not found for coords (${tile.rx}, ${tile.ry})`);\n                }\n                const overlay = this.tileOverlays.get(mapTile.rampType);\n                if (!overlay) {\n                    throw new Error(`Missing overlay mesh for rampType ${mapTile.rampType}`);\n                }\n                const mesh = overlay.clone();\n                const material = (overlay.material as THREE.MeshBasicMaterial).clone();\n                material.color.set(tile.buildable ? (this.viewModel.showBusy ? 0xffff00 : 0x00ff00) : 0xff0000);\n                mesh.material = material;\n                mesh.position.copy(this.getTilePosition(mapTile));\n                tilesContainer.add(mesh);\n            }\n            const container = this.get3DObject();\n            if (!container) {\n                throw new Error('Placement grid 3D object was not created');\n            }\n            this.disposeTilesObject();\n            this.tilesObject = tilesContainer;\n            container.add(tilesContainer);\n        }\n        else {\n            this.tilesObject.visible = false;\n        }\n    }\n    private refreshRangeCircle(): void {\n        if (!(this.viewModel.visible || !this.rangeObject)) {\n            if (this.rangeObject) {\n                this.rangeObject.visible = false;\n            }\n            return;\n        }\n        if (this.rangeObject) {\n            this.rangeObject.visible = true;\n        }\n        const container = this.get3DObject();\n        if (!container) {\n            throw new Error('Placement grid 3D object was not created');\n        }\n        const rangeIndicator = this.viewModel.rangeIndicator;\n        if (!rangeIndicator) {\n            this.disposeRangeObject(container);\n            this.lastRangeCircle = undefined;\n            return;\n        }\n        if (!this.lastRangeCircle || rangeIndicator.radius !== this.lastRangeCircle.radius) {\n            const rangeObject = OverlayUtils.createGroundCircle(rangeIndicator.radius * Coords.getWorldTileSize(), this.viewModel.rangeIndicatorColor);\n            this.disposeRangeObject(container);\n            container.add(rangeObject);\n            this.rangeObject = rangeObject;\n        }\n        if (!this.lastRangeCircle || !pointEquals(rangeIndicator.center, this.lastRangeCircle.center)) {\n            const tileX = Math.floor(rangeIndicator.center.x);\n            const tileY = Math.floor(rangeIndicator.center.y);\n            const mapTile = this.mapTiles.getByMapCoords(tileX, tileY);\n            if (!mapTile) {\n                console.warn(`[PlacementGrid] Map tile not found for coords (${tileX}, ${tileY})`);\n                return;\n            }\n            const position = this.getTilePosition(mapTile);\n            position.x += (rangeIndicator.center.x % 1) * Coords.getWorldTileSize();\n            position.z += (rangeIndicator.center.y % 1) * Coords.getWorldTileSize();\n            this.rangeObject?.position.copy(position);\n        }\n        this.lastRangeCircle = rangeIndicator;\n    }\n    private createTileOverlays(): void {\n        for (let rampType = 0; rampType < rampHeights.length; rampType++) {\n            this.tileOverlays.set(rampType, this.createTileOverlay(rampType));\n        }\n    }\n    private createTileOverlay(rampType: number): THREE.Mesh {\n        const screenTileSize = IsoCoords.getScreenTileSize();\n        const geometry = SpriteUtils.createSpriteGeometry({\n            texture: this.getTileOverlayTexture(),\n            textureArea: {\n                x: 0,\n                y: 2 * rampType * screenTileSize.height,\n                width: screenTileSize.width,\n                height: 2 * screenTileSize.height,\n            },\n            align: { x: 0, y: -1 },\n            camera: this.camera,\n            scale: Coords.ISO_WORLD_SCALE,\n        });\n        geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, Coords.tileHeightToWorld(1), 0));\n        const material = new THREE.MeshBasicMaterial({\n            map: this.getTileOverlayTexture(),\n            alphaTest: 0.5,\n            transparent: true,\n            opacity: 0.7,\n            depthTest: false,\n            depthWrite: false,\n        });\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.renderOrder = 1000000;\n        mesh.frustumCulled = false;\n        return mesh;\n    }\n    private getTilePosition(tile: any): any {\n        return Coords.tile3dToWorld(tile.rx, tile.ry, tile.z);\n    }\n    private getTileOverlayTexture(): THREE.Texture {\n        let texture = this.textureCache;\n        if (texture) {\n            return texture;\n        }\n        const screenTileSize = IsoCoords.getScreenTileSize();\n        const canvas = document.createElement('canvas');\n        const context = canvas.getContext('2d');\n        if (!context) {\n            throw new Error(\"Couldn't acquire canvas 2d context\");\n        }\n        canvas.width = THREE.MathUtils.ceilPowerOfTwo(screenTileSize.width);\n        canvas.height = THREE.MathUtils.ceilPowerOfTwo(2 * screenTileSize.height * rampHeights.length);\n        const tileOrigin = IsoCoords.tileToScreen(0, 0);\n        tileOrigin.x += -screenTileSize.width / 2;\n        const halfTileHeight = Coords.ISO_TILE_SIZE / 2;\n        for (let rampType = 0; rampType < rampHeights.length; rampType++) {\n            const heights = rampHeights[rampType];\n            const corners = [\n                [0, 1],\n                [0, 0],\n                [1, 0],\n                [1, 1],\n            ];\n            context.beginPath();\n            const first = IsoCoords.tileToScreen(corners[0][0], corners[0][1]);\n            context.moveTo(-tileOrigin.x + first.x, -tileOrigin.y + first.y + (1 - heights[0]) * halfTileHeight + 2 * rampType * screenTileSize.height);\n            for (let cornerIndex = 1; cornerIndex < corners.length; cornerIndex++) {\n                const screen = IsoCoords.tileToScreen(corners[cornerIndex][0], corners[cornerIndex][1]);\n                context.lineTo(-tileOrigin.x + screen.x, -tileOrigin.y + screen.y + (1 - heights[cornerIndex]) * halfTileHeight + 2 * rampType * screenTileSize.height);\n            }\n            context.closePath();\n            context.lineWidth = 1;\n            context.fillStyle = '#ffffff';\n            context.fill();\n            context.strokeStyle = '#000000';\n            context.stroke();\n        }\n        texture = new THREE.Texture(canvas);\n        texture.needsUpdate = true;\n        this.textureCache = texture;\n        return texture;\n    }\n    private disposeTilesObject(): void {\n        const container = this.get3DObject();\n        if (this.tilesObject && container) {\n            container.remove(this.tilesObject);\n            this.tilesObject.traverse((object: THREE.Object3D) => {\n                const mesh = object as THREE.Mesh;\n                if (mesh.material && 'dispose' in mesh.material) {\n                    (mesh.material as THREE.Material).dispose();\n                }\n            });\n        }\n        this.tilesObject = undefined;\n    }\n    private disposeRangeObject(container: THREE.Object3D): void {\n        if (!this.rangeObject) {\n            return;\n        }\n        container.remove(this.rangeObject);\n        this.rangeObject.geometry.dispose();\n        (this.rangeObject.material as THREE.Material).dispose();\n        this.rangeObject = undefined;\n    }\n    dispose(): void {\n        this.disposeTilesObject();\n        if (this.target) {\n            this.disposeRangeObject(this.target);\n        }\n        this.tileOverlays.forEach((overlay) => {\n            overlay.geometry.dispose();\n            (overlay.material as THREE.Material).dispose();\n        });\n        this.tileOverlays.clear();\n        this.textureCache?.dispose();\n        this.textureCache = undefined;\n        this.lastRangeCircle = undefined;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/game/worldInteraction/placementMode/PlacementGridModel.ts",
    "content": "export class PlacementGridModel {\n    public visible = false;\n    public showBusy = false;\n    public tiles: Array<{\n        rx: number;\n        ry: number;\n        buildable: boolean;\n    }> = [];\n    public rangeIndicator?: {\n        center: {\n            x: number;\n            y: number;\n        };\n        radius: number;\n    };\n    public rangeIndicatorColor = 0x00ff00;\n    constructor() { }\n    setTiles(tiles: Array<{\n        rx: number;\n        ry: number;\n        buildable: boolean;\n    }>): void {\n        this.tiles = tiles;\n    }\n    setRangeIndicator(center: {\n        x: number;\n        y: number;\n    }, radius: number): void {\n        this.rangeIndicator = { center, radius };\n    }\n    clearRangeIndicator(): void {\n        this.rangeIndicator = undefined;\n    }\n    show(): void {\n        this.visible = true;\n    }\n    hide(): void {\n        this.visible = false;\n    }\n    setBusyState(busy: boolean): void {\n        this.showBusy = busy;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/MainMenuController.ts",
    "content": "import { Controller, Screen } from '../Controller';\nimport { MainMenuScreenType } from '../ScreenType';\nimport { EventDispatcher } from '../../../util/event';\nimport { SoundKey } from '../../../engine/sound/SoundKey';\nimport { ChannelType } from '../../../engine/sound/ChannelType';\nexport class MainMenuController extends Controller {\n    private mainMenu: any;\n    private sound?: any;\n    private music?: any;\n    private uiSoundSuppressionDepth: number = 0;\n    private rerenderQueue: Promise<void> = Promise.resolve();\n    constructor(mainMenu: any, sound?: any, music?: any) {\n        super();\n        this.mainMenu = mainMenu;\n        this.sound = sound;\n        this.music = music;\n        console.log('[MainMenuController] Initialized');\n    }\n    private async withUiSoundSuppressed(task: () => Promise<void> | void): Promise<void> {\n        this.uiSoundSuppressionDepth += 1;\n        try {\n            await task();\n        }\n        finally {\n            this.uiSoundSuppressionDepth = Math.max(0, this.uiSoundSuppressionDepth - 1);\n        }\n    }\n    private shouldPlayUiSound(): boolean {\n        return this.uiSoundSuppressionDepth === 0;\n    }\n    async goToScreenBlocking(screenType: MainMenuScreenType, params?: any): Promise<void> {\n        return super.goToScreenBlocking(screenType, params);\n    }\n    goToScreen(screenType: MainMenuScreenType, params?: any): void {\n        return super.goToScreen(screenType, params);\n    }\n    async pushScreen(screenType: MainMenuScreenType, params?: any): Promise<void> {\n        this.setMainComponent();\n        this.setSidebarTitle(\"\");\n        await super.pushScreen(screenType, params);\n        const screen = this.screens.get(screenType);\n        if (screen?.title) {\n            this.setSidebarTitle(screen.title);\n        }\n        if (screen && 'musicType' in screen && screen.musicType !== undefined && this.music) {\n            console.log(`[MainMenuController] Playing music for screen ${screenType}: ${screen.musicType}`);\n            try {\n                await this.music.play(screen.musicType);\n            }\n            catch (error) {\n                console.error(`[MainMenuController] Failed to play music for screen ${screenType}:`, error);\n            }\n        }\n    }\n    async popScreen(params?: any): Promise<void> {\n        this.setMainComponent();\n        this.setSidebarTitle(\"\");\n        await super.popScreen(params);\n        const currentScreen = this.getCurrentScreen();\n        if (currentScreen?.title) {\n            this.setSidebarTitle(currentScreen.title);\n        }\n    }\n    setSidebarButtons(buttons: any[], mpSlotEnabled?: boolean): void {\n        console.log(`[MainMenuController] Setting ${buttons.length} sidebar buttons`);\n        if (this.mainMenu && this.mainMenu.setButtons) {\n            this.mainMenu.setButtons(buttons, !!mpSlotEnabled);\n        }\n    }\n    showSidebarButtons(): void {\n        console.log('[MainMenuController] Showing sidebar buttons');\n        if (this.mainMenu && this.mainMenu.isSidebarCollapsed && this.mainMenu.isSidebarCollapsed()) {\n            if (this.sound && this.shouldPlayUiSound()) {\n                this.sound.play(SoundKey.GUIMoveInSound, ChannelType.Ui);\n            }\n            if (this.mainMenu.showButtons) {\n                this.mainMenu.showButtons();\n            }\n        }\n    }\n    async hideSidebarButtons(): Promise<void> {\n        console.log('[MainMenuController] Hiding sidebar buttons');\n        if (this.mainMenu && this.mainMenu.isSidebarCollapsed && !this.mainMenu.isSidebarCollapsed()) {\n            if (this.sound && this.shouldPlayUiSound()) {\n                this.sound.play(SoundKey.GUIMoveOutSound, ChannelType.Ui);\n            }\n            return new Promise((resolve) => {\n                if (this.mainMenu && this.mainMenu.onSidebarToggle) {\n                    const handler = () => {\n                        this.mainMenu!.onSidebarToggle.unsubscribe(handler);\n                        resolve();\n                    };\n                    this.mainMenu.onSidebarToggle.subscribe(handler);\n                    this.mainMenu.hideButtons();\n                }\n                else {\n                    if (this.mainMenu && this.mainMenu.hideButtons) {\n                        this.mainMenu.hideButtons();\n                    }\n                    setTimeout(resolve, 300);\n                }\n            });\n        }\n    }\n    toggleMainVideo(show: boolean): void {\n        console.log(`[MainMenuController] ${show ? 'Showing' : 'Hiding'} main video`);\n        if (this.mainMenu && this.mainMenu.toggleVideo) {\n            this.mainMenu.toggleVideo(show);\n        }\n    }\n    showVersion(version: string): void {\n        console.log(`[MainMenuController] Showing version: ${version}`);\n        if (this.mainMenu && this.mainMenu.showVersion) {\n            this.mainMenu.showVersion(version);\n        }\n    }\n    hideVersion(): void {\n        console.log('[MainMenuController] Hiding version');\n        if (this.mainMenu && this.mainMenu.hideVersion) {\n            this.mainMenu.hideVersion();\n        }\n    }\n    setSidebarTitle(title: string): void {\n        console.log(`[MainMenuController] Setting sidebar title: ${title}`);\n        if (this.mainMenu && this.mainMenu.setSidebarTitle) {\n            this.mainMenu.setSidebarTitle(title);\n        }\n    }\n    setMainComponent(component?: any): void {\n        if (this.mainMenu && this.mainMenu.setContentComponent) {\n            this.mainMenu.setContentComponent(component);\n        }\n    }\n    setSidebarMpContent(content: any): void {\n        if (this.mainMenu && this.mainMenu.setSidebarMpContent) {\n            this.mainMenu.setSidebarMpContent(content);\n        }\n    }\n    toggleSidebarPreview(show: boolean): void {\n        console.log(`[MainMenuController] ${show ? 'Showing' : 'Hiding'} sidebar preview`);\n        if (this.mainMenu && this.mainMenu.toggleSidebarPreview) {\n            this.mainMenu.toggleSidebarPreview(show);\n        }\n    }\n    setSidebarPreview(preview?: any): void {\n        if (this.mainMenu && this.mainMenu.setSidebarPreview) {\n            this.mainMenu.setSidebarPreview(preview);\n        }\n    }\n    getSidebarPreviewSize(): any {\n        return this.mainMenu.getSidebarPreviewSize();\n    }\n    rerenderCurrentScreen(silent: boolean = false): void {\n        console.log('[MainMenuController] Rerendering current screen', { silent });\n        const currentScreen = this.getCurrentScreen();\n        const currentScreenType = this.getCurrentScreenType();\n        if (currentScreen && currentScreenType !== undefined) {\n            this.rerenderQueue = this.rerenderQueue\n                .catch(() => undefined)\n                .then(async () => {\n                if (this.getCurrentScreen() !== currentScreen || this.getCurrentScreenType() !== currentScreenType) {\n                    return;\n                }\n                const rerender = async () => {\n                    await currentScreen.onLeave();\n                    await currentScreen.onEnter();\n                };\n                if (silent) {\n                    await this.withUiSoundSuppressed(rerender);\n                    return;\n                }\n                await rerender();\n            })\n                .catch((error) => {\n                console.error('[MainMenuController] Failed to rerender current screen', error);\n            });\n        }\n    }\n    destroy(): void {\n        console.log('[MainMenuController] Destroying');\n        super.destroy();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/MainMenuRootScreen.ts",
    "content": "import { RootScreen } from '../RootScreen';\nimport { MainMenu } from './component/MainMenu';\nimport { MainMenuController } from './MainMenuController';\nimport { MainMenuScreenType } from '../ScreenType';\nimport { ScoreScreen } from './score/ScoreScreen';\nimport { Strings } from '../../../data/Strings';\nimport { ShpFile } from '../../../data/ShpFile';\nimport { JsxRenderer } from '../../jsx/JsxRenderer';\nimport { LazyResourceCollection } from '../../../engine/LazyResourceCollection';\nimport { MessageBoxApi } from '../../component/MessageBoxApi';\nimport { Config } from '../../../Config';\nimport { browserFileSystemAccess } from '../../../engine/gameRes/browserFileSystemAccess';\nexport interface UiScene {\n    menuViewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    add(object: any): void;\n    remove(object: any): void;\n}\nexport class MainMenuRootScreen extends RootScreen {\n    private subScreens: Map<MainMenuScreenType, any>;\n    private uiScene: UiScene;\n    private strings: Strings;\n    private images: LazyResourceCollection<ShpFile>;\n    private jsxRenderer: JsxRenderer;\n    private messageBoxApi: MessageBoxApi;\n    private videoSrc?: string | File;\n    private sound?: any;\n    private music?: any;\n    private appVersion: string;\n    private generalOptions?: any;\n    private localPrefs?: any;\n    private fullScreen?: any;\n    private mixer?: any;\n    private keyBinds?: any;\n    private rootController?: any;\n    private config: Config;\n    private mainMenu?: MainMenu;\n    private mainMenuCtrl?: MainMenuController;\n    constructor(subScreens: Map<MainMenuScreenType, any>, uiScene: UiScene, strings: Strings, images: LazyResourceCollection<ShpFile>, jsxRenderer: JsxRenderer, messageBoxApi: MessageBoxApi, appVersion: string, config: Config, videoSrc?: string | File, sound?: any, music?: any, generalOptions?: any, localPrefs?: any, fullScreen?: any, mixer?: any, keyBinds?: any, rootController?: any) {\n        super();\n        this.subScreens = subScreens;\n        this.uiScene = uiScene;\n        this.strings = strings;\n        this.images = images;\n        this.jsxRenderer = jsxRenderer;\n        this.messageBoxApi = messageBoxApi;\n        this.appVersion = appVersion;\n        this.config = config;\n        this.videoSrc = videoSrc;\n        this.sound = sound;\n        this.music = music;\n        this.generalOptions = generalOptions;\n        this.localPrefs = localPrefs;\n        this.fullScreen = fullScreen;\n        this.mixer = mixer;\n        this.keyBinds = keyBinds;\n        this.rootController = rootController;\n    }\n    createView(): void {\n        console.log('[MainMenuRootScreen] Creating view');\n        console.log('[MainMenuRootScreen] Using menuViewport:', this.uiScene.menuViewport);\n        console.log('[MainMenuRootScreen] Full viewport:', this.uiScene.viewport);\n        this.mainMenu = new MainMenu(this.uiScene.menuViewport, this.images, this.jsxRenderer, this.videoSrc as string);\n    }\n    createViewAndController(): MainMenuController {\n        console.log('[MainMenuRootScreen] Creating view and controller');\n        this.createView();\n        this.mainMenuCtrl = new MainMenuController(this.mainMenu, this.sound, this.music);\n        const debugRoot = ((window as any).__ra2debug ??= {});\n        debugRoot.mainMenu = this.mainMenu;\n        debugRoot.mainMenuController = this.mainMenuCtrl;\n        this.mainMenuCtrl.onScreenChange.subscribe((screenType, _controller) => {\n            if (screenType !== undefined) {\n                console.log(`[MainMenuRootScreen] Navigated to screen: ${screenType}`);\n            }\n            else {\n                console.log('[MainMenuRootScreen] Navigated to previous screen');\n            }\n        });\n        return this.mainMenuCtrl;\n    }\n    onViewportChange(): void {\n        console.log('[MainMenuRootScreen] Viewport changed');\n        console.log('[MainMenuRootScreen] New menuViewport:', this.uiScene.menuViewport);\n        if (this.mainMenu) {\n            this.mainMenu.setViewport(this.uiScene.menuViewport);\n        }\n        if (this.mainMenuCtrl) {\n            this.mainMenuCtrl.rerenderCurrentScreen(true);\n        }\n    }\n    async onEnter(params?: any): Promise<void> {\n        console.log('[MainMenuRootScreen] Entering main menu root screen');\n        const controller = this.createViewAndController();\n        if (!this.subScreens.has(MainMenuScreenType.Score)) {\n            this.subScreens.set(MainMenuScreenType.Score, ScoreScreen as any);\n        }\n        for (const [screenType, screenClass] of this.subScreens) {\n            const screen: any = await this.createScreen(screenType, screenClass, controller);\n            if (screen) {\n                if (screen.setController) {\n                    screen.setController(controller);\n                }\n                controller.addScreen(screenType, screen);\n            }\n        }\n        if (this.mainMenu) {\n            this.uiScene.add(this.mainMenu);\n        }\n        setTimeout(() => {\n            if (params?.route) {\n                controller.goToScreen(params.route.screenType, params.route.params);\n            }\n            else {\n                controller.goToScreen(MainMenuScreenType.Home);\n            }\n        }, 0);\n    }\n    private async createScreen(screenType: MainMenuScreenType, screenClass: any, _controller: any): Promise<any> {\n        let screen: any;\n        if (screenType === MainMenuScreenType.InfoAndCredits) {\n            screen = new screenClass(this.strings, this.messageBoxApi);\n        }\n        else if (screenType === MainMenuScreenType.Credits) {\n            screen = new screenClass(this.strings, this.jsxRenderer);\n        }\n        else if (screenType === MainMenuScreenType.Options) {\n            screen = new screenClass(this.strings, this.jsxRenderer, this.generalOptions, this.localPrefs, this.fullScreen, false, true);\n        }\n        else if (screenType === MainMenuScreenType.OptionsSound) {\n            screen = new screenClass(this.strings, this.jsxRenderer, this.mixer, this.music, this.localPrefs);\n        }\n        else if (screenType === MainMenuScreenType.OptionsKeyboard) {\n            screen = new screenClass(this.strings, this.jsxRenderer, this.keyBinds);\n        }\n        else if (screenType === MainMenuScreenType.Skirmish) {\n            console.log('[MainMenuRootScreen] Creating SkirmishScreen with real dependencies');\n            const { ErrorHandler } = await import('../../../ErrorHandler.js');\n            const { Rules } = await import('../../../game/rules/Rules.js');\n            const { MapFileLoader } = await import('../game/MapFileLoader.js');\n            const { Engine } = await import('../../../engine/Engine.js');\n            const errorHandler = new ErrorHandler(this.messageBoxApi, this.strings);\n            const rules = new Rules(Engine.getRules());\n            const { ResourceLoader } = await import('../../../engine/ResourceLoader.js');\n            const mapResourceLoader = new ResourceLoader(this.config.mapsBaseUrl ?? '');\n            const mapFileLoader = new MapFileLoader(mapResourceLoader, Engine.vfs);\n            const mapList = Engine.getMapList();\n            const gameModes = Engine.getMpModes();\n            screen = new screenClass(this.rootController, errorHandler, this.messageBoxApi, this.strings, rules, this.jsxRenderer, mapFileLoader, mapList, gameModes, this.localPrefs);\n        }\n        else if (screenType === MainMenuScreenType.MapSelection) {\n            console.log('[MainMenuRootScreen] Creating MapSelScreen with real dependencies');\n            const { ErrorHandler } = await import('../../../ErrorHandler.js');\n            const { MapFileLoader } = await import('../game/MapFileLoader.js');\n            const { Engine } = await import('../../../engine/Engine.js');\n            const errorHandler = new ErrorHandler(this.messageBoxApi, this.strings);\n            const { ResourceLoader } = await import('../../../engine/ResourceLoader.js');\n            const mapResourceLoader = new ResourceLoader(this.config.mapsBaseUrl ?? '');\n            const mapFileLoader = new MapFileLoader(mapResourceLoader, Engine.vfs);\n            const mapList = Engine.getMapList();\n            const gameModes = Engine.getMpModes();\n            let mapDir: any = undefined;\n            try {\n                const mapDirHandle = await Engine.getMapDir();\n                if (mapDirHandle) {\n                    const { RealFileSystemDir } = await import('../../../data/vfs/RealFileSystemDir.js');\n                    mapDir = new RealFileSystemDir(mapDirHandle);\n                }\n            }\n            catch (e) {\n                console.error(\"[MainMenuRootScreen] Couldn't get map dir\", e);\n            }\n            const fsAccessLib = browserFileSystemAccess;\n            const sentry = undefined as any;\n            screen = new screenClass(this.strings, this.jsxRenderer, mapFileLoader, errorHandler, this.messageBoxApi, this.localPrefs, mapList, gameModes, mapDir, fsAccessLib, sentry);\n        }\n        else if (screenType === MainMenuScreenType.Score) {\n            screen = new screenClass(this.strings, this.jsxRenderer, (this as any).wolService);\n        }\n        else if (screenType === MainMenuScreenType.ReplaySelection) {\n            const { ErrorHandler } = await import('../../../ErrorHandler.js');\n            const { Rules } = await import('../../../game/rules/Rules.js');\n            const { Engine } = await import('../../../engine/Engine.js');\n            const errorHandler = new ErrorHandler(this.messageBoxApi, this.strings);\n            const rules = new Rules(Engine.getRules());\n            const replayManager = (this as any).replayManager;\n            const engineVersion = this.appVersion;\n            const engineModHash = Engine.getActiveMod?.() ?? '';\n            screen = new screenClass(engineVersion, engineModHash, undefined, undefined, this.rootController, this.strings, this.jsxRenderer, errorHandler, this.messageBoxApi, replayManager, undefined, rules);\n        }\n        else if (screenType === MainMenuScreenType.LanSetup) {\n            const { ErrorHandler } = await import('../../../ErrorHandler.js');\n            const { Rules } = await import('../../../game/rules/Rules.js');\n            const { MapFileLoader } = await import('../game/MapFileLoader.js');\n            const { Engine } = await import('../../../engine/Engine.js');\n            const errorHandler = new ErrorHandler(this.messageBoxApi, this.strings);\n            const rules = new Rules(Engine.getRules());\n            const { ResourceLoader } = await import('../../../engine/ResourceLoader.js');\n            const mapResourceLoader = new ResourceLoader(this.config.mapsBaseUrl ?? '');\n            const mapFileLoader = new MapFileLoader(mapResourceLoader, Engine.vfs);\n            const mapList = Engine.getMapList();\n            const gameModes = Engine.getMpModes();\n            let mapDir: any = undefined;\n            try {\n                const mapDirHandle = await Engine.getMapDir();\n                if (mapDirHandle) {\n                    const { RealFileSystemDir } = await import('../../../data/vfs/RealFileSystemDir.js');\n                    mapDir = new RealFileSystemDir(mapDirHandle);\n                }\n            }\n            catch (error) {\n                console.error(\"[MainMenuRootScreen] Couldn't get map dir for LAN setup\", error);\n            }\n            screen = new screenClass(this.rootController, this.strings, this.jsxRenderer, rules, mapFileLoader, mapList, gameModes, this.localPrefs, this.messageBoxApi, mapDir);\n        }\n        else if (screenType === MainMenuScreenType.Home) {\n            screen = new screenClass(this.strings, this.messageBoxApi, this.appVersion, false, false, this.fullScreen);\n        }\n        else {\n            screen = new screenClass(this.strings, this.messageBoxApi, this.appVersion, false, false);\n        }\n        return screen;\n    }\n    async onLeave(): Promise<void> {\n        console.log('[MainMenuRootScreen] Leaving main menu root screen');\n        if (this.mainMenuCtrl) {\n            this.mainMenuCtrl.toggleMainVideo(false);\n            await this.mainMenuCtrl.leaveCurrentScreen();\n            this.mainMenuCtrl.destroy();\n            this.mainMenuCtrl = undefined;\n        }\n        const debugRoot = (window as any).__ra2debug;\n        if (debugRoot) {\n            delete debugRoot.mainMenu;\n            delete debugRoot.mainMenuController;\n        }\n        if (this.mainMenu) {\n            this.uiScene.remove(this.mainMenu);\n            this.mainMenu.destroy();\n            this.mainMenu = undefined;\n        }\n    }\n    update(deltaTime: number): void {\n        if (this.mainMenuCtrl) {\n            this.mainMenuCtrl.update(deltaTime);\n        }\n        if (this.mainMenu) {\n            this.mainMenu.update(deltaTime);\n        }\n    }\n    destroy(): void {\n        console.log('[MainMenuRootScreen] Destroying');\n        if (this.mainMenuCtrl) {\n            this.mainMenuCtrl.destroy();\n        }\n        if (this.mainMenu) {\n            this.mainMenu.destroy();\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/MainMenuRoute.ts",
    "content": "import { MainMenuScreenType } from '../ScreenType';\nexport class MainMenuRoute {\n    screenType: MainMenuScreenType;\n    params: any;\n    constructor(screenType: MainMenuScreenType, params: any) {\n        this.screenType = screenType;\n        this.params = params;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/MainMenuScreen.ts",
    "content": "export class MainMenuScreen {\n    protected controller: any;\n    protected title?: string;\n    protected musicType?: unknown;\n    setController(controller: any): void {\n        this.controller = controller;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/ScreenType.ts",
    "content": "export enum ScreenType {\n    Home = 0,\n    Skirmish = 1,\n    QuickGame = 2,\n    CustomGame = 3,\n    Login = 4,\n    NewAccount = 5,\n    Lobby = 6,\n    MapSelection = 7,\n    Ladder = 8,\n    LadderRules = 9,\n    ReplaySelection = 10,\n    ModSelection = 11,\n    Score = 12,\n    InfoAndCredits = 13,\n    PatchNotes = 14,\n    Credits = 15,\n    Options = 16,\n    OptionsSound = 17,\n    OptionsKeyboard = 18,\n    OptionsStorage = 19\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/Iframe.tsx",
    "content": "import React from 'react';\ninterface IframeProps {\n    src: string;\n    className?: string;\n}\nexport const Iframe: React.FC<IframeProps> = ({ src, className }) => {\n    return <iframe src={src} className={className}/>;\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/MainMenu.ts",
    "content": "import { jsx } from \"../../../jsx/jsx\";\nimport { UiObject } from \"../../../UiObject\";\nimport { HtmlContainer } from \"../../../HtmlContainer\";\nimport { MenuVideo } from \"./MenuVideo\";\nimport { MenuButton } from \"../../../component/MenuButton\";\nimport { EventDispatcher } from \"../../../../util/event\";\nimport { MenuSlotAnimationRunner, MenuButtonState } from \"./MenuSlotAnimationRunner\";\nimport { HtmlView } from \"../../../jsx/HtmlView\";\nimport { MenuMpSlotAnimRunner } from \"./MenuMpSlotAnimRunner\";\nimport { MenuMpSlotText } from \"./MenuMpSlotText\";\nimport { SidebarPreview } from \"./SidebarPreview\";\nimport { MenuTooltip } from \"./MenuTooltip\";\nimport { VersionString } from \"./VersionString\";\nimport * as THREE from 'three';\nexport interface Viewport {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\nexport interface ButtonConfig {\n    label: string;\n    tooltip?: string;\n    disabled?: boolean;\n    isBottom?: boolean;\n    onClick?: () => void;\n}\nexport interface SidebarMpContent {\n    text: string;\n    icon?: string;\n    tooltip?: string;\n}\nexport interface ImageMap {\n    get(name: string): any;\n}\nexport interface JsxRenderer {\n    render(jsx: any): UiObject[];\n}\nexport class MainMenu extends UiObject {\n    private viewport: Viewport;\n    private images: ImageMap;\n    private jsxRenderer: JsxRenderer;\n    private videoSrc: string;\n    private rootObjects: UiObject[] = [];\n    private sidebarObjects: UiObject[] = [];\n    private sidebarSlots: any[] = [];\n    private sidebarButtons: any[] = [];\n    private sidebarButtonConfigs: ButtonConfig[] = [];\n    private sidebarButtonsRawConfigs?: ButtonConfig[];\n    private sidebarMpSlotEnabled: boolean = false;\n    private sidebarCollapsed: boolean = true;\n    private sidebarNeedsRefresh?: boolean;\n    private sidebarMpSlotContent?: SidebarMpContent;\n    private contentComponent?: UiObject;\n    private sidebarPreviewInner?: UiObject;\n    private _onSidebarToggle = new EventDispatcher<MainMenu, boolean>();\n    private mainContainer!: UiObject;\n    private statusBar!: UiObject;\n    private sidebarContainer!: UiObject;\n    private sidebarMpSlotContainer!: UiObject;\n    private sidebarMpSlot!: any;\n    private sidebarMpSlotContentEl!: any;\n    private sidebarPreview!: SidebarPreview;\n    private menuVideo!: HtmlView;\n    private version!: HtmlView;\n    constructor(viewport: Viewport, images: ImageMap, jsxRenderer: JsxRenderer, videoSrc: string) {\n        super(new THREE.Object3D(), new HtmlContainer());\n        this.viewport = viewport;\n        this.images = images;\n        this.jsxRenderer = jsxRenderer;\n        this.videoSrc = videoSrc;\n        this.create3DObject();\n    }\n    get onSidebarToggle() {\n        return this._onSidebarToggle;\n    }\n    setViewport(viewport: Viewport): void {\n        this.viewport = viewport;\n        this.setPosition(this.viewport.x, this.viewport.y);\n        const statusBarImage = this.getImage(\"lwscrnl.shp\");\n        this.statusBar.setPosition(0, this.viewport.height - statusBarImage.height);\n        const sidebarImage = this.getImage(\"sdtp.shp\");\n        const sidebarViewport = this.computeSidebarViewport(sidebarImage);\n        this.sidebarContainer.setPosition(sidebarViewport.x, sidebarViewport.y);\n        this.sidebarContainer.remove(...this.sidebarObjects);\n        this.sidebarObjects.forEach((obj) => obj.destroy());\n        this.createSidebarButtons(this.computeSidebarButtonsViewport(sidebarImage));\n        this.updateButtons(this.sidebarButtonsRawConfigs ?? []);\n        if (!this.sidebarCollapsed) {\n            this.showButtons();\n        }\n    }\n    setContentComponent(component?: UiObject): void {\n        let container = this.mainContainer;\n        if (this.contentComponent) {\n            container.remove(this.contentComponent);\n            this.contentComponent.destroy();\n            this.contentComponent = undefined;\n        }\n        if (component) {\n            container.add(component);\n            this.contentComponent = component;\n        }\n    }\n    setSlots(slotCount: number, hasBottomSlot: boolean, mpSlotEnabled: boolean = false): void {\n        let totalSlots = this.sidebarSlots.length;\n        if (!totalSlots) {\n            throw new Error(\"Cannot call setButtons prior to render\");\n        }\n        this.sidebarMpSlotContainer.setVisible(mpSlotEnabled);\n        this.sidebarSlots[0].setVisible(!mpSlotEnabled);\n        this.sidebarSlots.forEach((slot, index) => {\n            let animRunner = slot.getAnimationRunner();\n            if (index < slotCount + (mpSlotEnabled ? 1 : 0)) {\n                (animRunner as any).buttonState = MenuButtonState.Unlit;\n            }\n            else if (index === totalSlots - 1) {\n                (animRunner as any).buttonState = hasBottomSlot\n                    ? MenuButtonState.Unlit\n                    : MenuButtonState.Hidden;\n            }\n            else {\n                (animRunner as any).buttonState = MenuButtonState.Hidden;\n            }\n        });\n    }\n    setButtons(buttons: ButtonConfig[], mpSlotEnabled: boolean = false): void {\n        console.log('[MainMenu] setButtons called, sidebarButtons.length:', this.sidebarButtons.length);\n        this.sidebarButtonsRawConfigs = buttons;\n        this.sidebarMpSlotEnabled = mpSlotEnabled;\n        this.updateButtons(buttons);\n    }\n    updateButtons(buttons: ButtonConfig[]): void {\n        console.log('[MainMenu] updateButtons called, sidebarButtons.length:', this.sidebarButtons.length);\n        let mpSlotEnabled = this.sidebarMpSlotEnabled;\n        const hasBottomButton = !!buttons.find((btn) => !!btn.isBottom);\n        this.setSlots(buttons.length - (hasBottomButton ? 1 : 0), hasBottomButton, mpSlotEnabled);\n        this.updateSidebarMpContent();\n        this.sidebarButtons.forEach((btn) => btn.applyOptions((options: any) => (options.buttonConfig = undefined)));\n        buttons.forEach((buttonConfig, index) => {\n            const slotIndex = buttonConfig.isBottom\n                ? this.sidebarButtons.length - 1\n                : mpSlotEnabled\n                    ? index + 1\n                    : index;\n            console.log('[MainMenu] Setting button config for slotIndex:', slotIndex, 'buttonConfig:', buttonConfig);\n            this.sidebarButtonConfigs[slotIndex] = buttonConfig;\n            if (this.sidebarButtons[slotIndex]) {\n                this.sidebarButtons[slotIndex].applyOptions((options: any) => (options.buttonConfig = {\n                    label: buttonConfig.label,\n                    tooltip: buttonConfig.tooltip,\n                    disabled: !!buttonConfig.disabled,\n                }));\n            }\n            else {\n                console.warn('[MainMenu] sidebarButtons[' + slotIndex + '] is undefined');\n            }\n        });\n        this.sidebarNeedsRefresh = true;\n    }\n    isSidebarCollapsed(): boolean {\n        return this.sidebarCollapsed;\n    }\n    showButtons(): void {\n        this.sidebarCollapsed = false;\n        this.sidebarNeedsRefresh = true;\n        this.sidebarMpSlot.getAnimationRunner().slideIn();\n        this.sidebarSlots.forEach((slot) => {\n            let animRunner = slot.getAnimationRunner();\n            animRunner.slideIn();\n        });\n    }\n    hideButtons(): void {\n        this.sidebarCollapsed = true;\n        this.updateSidebarButtons();\n        this.sidebarNeedsRefresh = true;\n        this.sidebarMpSlot.getAnimationRunner().slideOut();\n        this.sidebarSlots.forEach((slot) => {\n            let animRunner = slot.getAnimationRunner();\n            animRunner.slideOut();\n        });\n    }\n    setSidebarTitle(title: string): void {\n        this.sidebarPreview.setTitle(title);\n    }\n    toggleSidebarPreview(visible: boolean): void {\n        this.sidebarPreview.toggleSidebarPreview(visible);\n    }\n    setSidebarPreview(preview: UiObject): void {\n        if (this.sidebarPreviewInner) {\n            this.sidebarPreviewInner.destroy();\n        }\n        this.sidebarPreview.setPreview(preview);\n        this.sidebarPreviewInner = preview;\n    }\n    getSidebarPreviewSize(): {\n        width: number;\n        height: number;\n    } {\n        return this.sidebarPreview.getPreviewSize();\n    }\n    toggleVideo(visible: boolean): void {\n        console.log('[MainMenu] toggleVideo called, visible:', visible, 'menuVideo exists:', !!this.menuVideo);\n        if (!this.menuVideo) {\n            throw new Error(\"Cannot call toggleVideo prior to render\");\n        }\n        this.menuVideo.getUiObject().setVisible(visible);\n        console.log('[MainMenu] Video visibility set to:', visible);\n    }\n    showVersion(version: string): void {\n        console.log('[MainMenu] showVersion called, version:', version, 'version element exists:', !!this.version);\n        this.version.getUiObject().setVisible(true);\n        this.version.getElement().applyOptions((options: any) => (options.value = version));\n        console.log('[MainMenu] Version shown:', version);\n    }\n    hideVersion(): void {\n        this.version.getUiObject().setVisible(false);\n    }\n    setSidebarMpContent(content: SidebarMpContent): void {\n        this.sidebarMpSlotContent = content;\n        this.updateSidebarMpContent();\n    }\n    updateSidebarMpContent(): void {\n        this.sidebarMpSlotContentEl.applyOptions((options: any) => {\n            if (this.sidebarMpSlotContent) {\n                options.text = this.sidebarMpSlotContent.text;\n                options.icon = this.sidebarMpSlotContent.icon;\n                options.tooltip = this.sidebarMpSlotContent.tooltip;\n            }\n        });\n    }\n    getImage(name: string): any {\n        const image = this.images.get(name);\n        if (!image) {\n            throw new Error(`Missing image \"${name}\"`);\n        }\n        return image;\n    }\n    create3DObject(): void {\n        console.log('[MainMenu] Creating 3D object');\n        super.create3DObject();\n        if (!this.rootObjects.length) {\n            console.log('[MainMenu] Creating root objects');\n            this.setPosition(this.viewport.x, this.viewport.y);\n            const mainImage = this.getImage(\"mnscrnl.shp\");\n            const statusBarImage = this.getImage(\"lwscrnl.shp\");\n            const sidebarImage = this.getImage(\"sdtp.shp\");\n            const sidebarAnimImage = this.getImage(\"sdwrnanm.shp\");\n            const sidebarViewport = this.computeSidebarViewport(sidebarImage);\n            console.log('[MainMenu] Image sizes:');\n            console.log('  mainImage:', mainImage.width, 'x', mainImage.height);\n            console.log('  statusBarImage:', statusBarImage.width, 'x', statusBarImage.height);\n            console.log('  sidebarImage:', sidebarImage.width, 'x', sidebarImage.height);\n            console.log('[MainMenu] Viewport:', this.viewport);\n            console.log('[MainMenu] Status bar position will be:', 0, this.viewport.height - statusBarImage.height);\n            const statusBarY = this.viewport.height - statusBarImage.height;\n            console.log('[MainMenu] Status bar position:', 0, statusBarY);\n            console.log('[MainMenu] Camera is inverted, so Y coordinates might need adjustment');\n            this.rootObjects = this.jsxRenderer.render(jsx(\"fragment\", null, jsx(\"container\", {\n                width: mainImage.width,\n                height: mainImage.height,\n                ref: (ref: UiObject) => (this.mainContainer = ref),\n            }, jsx(\"sprite\", { image: mainImage, palette: \"shell.pal\" }), jsx(HtmlView, {\n                component: MenuVideo,\n                props: { src: this.videoSrc },\n                hidden: true,\n                ref: (ref: HtmlView) => (this.menuVideo = ref),\n            })), jsx(\"container\", {\n                x: 0,\n                y: statusBarY,\n                ref: (ref: UiObject) => (this.statusBar = ref),\n            }, jsx(\"sprite\", { image: statusBarImage, palette: \"shell.pal\" }), jsx(HtmlView, {\n                component: MenuTooltip,\n                props: { monitorContainer: this.getHtmlContainer() },\n                width: statusBarImage.width,\n                height: statusBarImage.height,\n            })), jsx(\"container\", {\n                x: sidebarViewport.x,\n                y: sidebarViewport.y,\n                ref: (ref: UiObject) => (this.sidebarContainer = ref),\n            }, jsx(SidebarPreview, {\n                sdtpImg: sidebarImage,\n                sdtpAnimImg: sidebarAnimImage,\n                closed: true,\n                ref: (ref: SidebarPreview) => (this.sidebarPreview = ref),\n            }), jsx(HtmlView, {\n                component: VersionString,\n                props: { value: \"\" },\n                width: sidebarViewport.width,\n                y: sidebarViewport.height - 20,\n                ref: (ref: HtmlView) => (this.version = ref),\n                hidden: true,\n            }))));\n            console.log('[MainMenu] JSX rendered, rootObjects count:', this.rootObjects.length);\n            console.log('[MainMenu] Root objects:', this.rootObjects);\n            this.add(...this.rootObjects);\n            this.createSidebarButtons(this.computeSidebarButtonsViewport(sidebarImage));\n        }\n    }\n    createSidebarButtons(viewport: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }): void {\n        let buttonBgImage = this.getImage(\"sdbtnbkgd.shp\");\n        let buttonAnimImage = this.getImage(\"sdbtnanm.shp\");\n        const slotCount = Math.floor(viewport.height / buttonBgImage.height);\n        let bottomImage = this.getImage(\"sdbtm.shp\");\n        const remainingHeight = viewport.height - buttonBgImage.height * slotCount;\n        const clippedBottomImage = bottomImage.clip(bottomImage.width, remainingHeight);\n        this.sidebarSlots = [];\n        this.sidebarButtons = [];\n        this.sidebarObjects = this.jsxRenderer.render(jsx(\"fragment\", null, new Array(slotCount).fill(0).map((_, slotIndex) => {\n            let animRunner = new MenuSlotAnimationRunner(slotIndex);\n            return jsx(\"fragment\", null, jsx(\"container\", { x: viewport.x, y: viewport.y + buttonBgImage.height * slotIndex }, jsx(\"sprite\", { image: buttonBgImage, palette: \"shell2.pal\" }), !slotIndex\n                ? jsx(\"container\", {\n                    zIndex: 1,\n                    hidden: true,\n                    ref: (ref: UiObject) => (this.sidebarMpSlotContainer = ref),\n                    x: 12,\n                    y: -buttonBgImage.height,\n                }, jsx(\"sprite\", {\n                    image: \"sdmpbtn.shp\",\n                    palette: \"shell.pal\",\n                    ref: (ref: any) => (this.sidebarMpSlot = ref),\n                    animationRunner: new MenuMpSlotAnimRunner(),\n                }), jsx(HtmlView, {\n                    component: MenuMpSlotText,\n                    props: { text: \"\" },\n                    width: 146,\n                    height: 2 * buttonBgImage.height,\n                    innerRef: (ref: any) => (this.sidebarMpSlotContentEl = ref),\n                }))\n                : [], jsx(\"sprite\", {\n                image: buttonAnimImage,\n                palette: \"sdbtnanm.pal\",\n                ref: (ref: any) => this.sidebarSlots.push(ref),\n                x: 12,\n                animationRunner: animRunner,\n            }), jsx(HtmlView, {\n                x: 12,\n                hidden: true,\n                innerRef: (ref: any) => this.sidebarButtons.push(ref),\n                component: MenuButton,\n                props: {\n                    box: { x: 0, y: 0, width: 146, height: buttonAnimImage.height },\n                    buttonConfig: null,\n                    onMouseDown: () => {\n                        (animRunner as any).buttonState = MenuButtonState.Active;\n                        let mouseUpHandler = () => {\n                            (animRunner as any).buttonState = MenuButtonState.Normal;\n                            document.removeEventListener(\"mouseup\", mouseUpHandler);\n                        };\n                        document.addEventListener(\"mouseup\", mouseUpHandler);\n                    },\n                    onClick: () => {\n                        this.onSidebarButtonClick(slotIndex);\n                    },\n                },\n            })));\n        }), jsx(\"sprite\", {\n            image: clippedBottomImage,\n            palette: \"shell.pal\",\n            x: viewport.x,\n            y: viewport.y + buttonBgImage.height * slotCount,\n        })));\n        this.sidebarContainer.add(...this.sidebarObjects);\n    }\n    computeSidebarViewport(sidebarImage: any): {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    } {\n        return {\n            x: this.viewport.width - sidebarImage.width,\n            y: 0,\n            width: sidebarImage.width,\n            height: this.viewport.height,\n        };\n    }\n    computeSidebarButtonsViewport(sidebarImage: any): {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    } {\n        return {\n            x: 0,\n            y: sidebarImage.height,\n            width: sidebarImage.width,\n            height: this.viewport.height - sidebarImage.height,\n        };\n    }\n    update(deltaTime: number): void {\n        super.update(deltaTime);\n        if (this.sidebarNeedsRefresh) {\n            let lastSlot = this.sidebarSlots[this.sidebarSlots.length - 1];\n            let animRunner = lastSlot.getAnimationRunner();\n            if (animRunner.isStopped()) {\n                this.updateSidebarButtons();\n                this._onSidebarToggle.dispatch(this, !this.sidebarCollapsed);\n            }\n        }\n    }\n    updateSidebarButtons(): void {\n        if (this.sidebarCollapsed) {\n            this.sidebarButtons.forEach((btn) => btn.hide());\n            this.sidebarMpSlotContentEl.hide();\n            this.sidebarSlots.forEach((slot) => {\n                let animRunner = slot.getAnimationRunner();\n                if ((animRunner as any).buttonState === MenuButtonState.Normal) {\n                    (animRunner as any).buttonState = MenuButtonState.Unlit;\n                }\n            });\n        }\n        else {\n            this.sidebarButtons.forEach((btn) => btn.show());\n            this.sidebarMpSlotContentEl.show();\n            this.sidebarSlots.forEach((slot, slotIndex) => {\n                let animRunner = slot.getAnimationRunner();\n                if ((animRunner as any).buttonState === MenuButtonState.Hidden) {\n                    return;\n                }\n                const buttonConfig = this.sidebarButtonConfigs[slotIndex];\n                (animRunner as any).buttonState = buttonConfig?.disabled\n                    ? MenuButtonState.Unlit\n                    : MenuButtonState.Normal;\n            });\n        }\n        this.sidebarNeedsRefresh = false;\n    }\n    onSidebarButtonClick(slotIndex: number): void {\n        const onClick = this.sidebarButtonConfigs[slotIndex]?.onClick;\n        if (onClick) {\n            onClick();\n        }\n    }\n    destroy(): void {\n        this.sidebarButtons.length = 0;\n        this.remove(...this.rootObjects);\n        this.rootObjects.forEach((obj) => obj.destroy());\n        this.rootObjects.length = 0;\n        super.destroy();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/MenuMpSlotAnimRunner.ts",
    "content": "import { MenuSlotAnimationRunner, AnimationType } from \"./MenuSlotAnimationRunner\";\nimport { AnimationState } from \"../../../../engine/Animation\";\nexport class MenuMpSlotAnimRunner extends MenuSlotAnimationRunner {\n    getCurrentFrame(): number {\n        if (this.currentAnimationType === AnimationType.None ||\n            this.animation.getState() === AnimationState.DELAYED) {\n            return this.collapsed ? 6 : 0;\n        }\n        const direction: number = this.currentAnimationType === AnimationType.SlideIn ? -1 : 1;\n        let baseFrame: number = 1;\n        if (direction === -1) {\n            baseFrame += 5;\n        }\n        return baseFrame + direction * this.animation.getCurrentFrame();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/MenuMpSlotText.tsx",
    "content": "import React from 'react';\nimport { Image } from '../../../component/Image';\ninterface MenuMpSlotTextProps {\n    text: string;\n    icon?: string;\n    tooltip?: string;\n}\nexport const MenuMpSlotText: React.FC<MenuMpSlotTextProps> = ({ text, icon, tooltip }) => {\n    return (<div className=\"menu-mp-slot\">\n      <pre className=\"menu-mp-slot-text\">{text}</pre>\n      {icon && (<div className=\"menu-mp-slot-icon\" data-r-tooltip={tooltip}>\n          <Image src={icon}/>\n        </div>)}\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/MenuSdTopAnimRunner.ts",
    "content": "import { MenuSlotAnimationRunner, AnimationType } from './MenuSlotAnimationRunner';\nimport { AnimationState } from '@/engine/Animation';\nexport class MenuSdTopAnimRunner extends MenuSlotAnimationRunner {\n    getCurrentFrame(): number {\n        if (this.currentAnimationType === AnimationType.None ||\n            this.animation!.getState() === AnimationState.DELAYED) {\n            return this.collapsed ? 5 : 0;\n        }\n        else {\n            const direction = this.currentAnimationType === AnimationType.SlideIn ? -1 : 1;\n            let baseFrame = 0;\n            if (direction === -1) {\n                baseFrame += 5;\n            }\n            return baseFrame + direction * this.animation!.getCurrentFrame();\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/MenuSlotAnimationRunner.ts",
    "content": "import { IniSection } from '../../../../data/IniSection';\nimport { ShpFile } from '../../../../data/ShpFile';\nimport { Animation, AnimationState } from '../../../../engine/Animation';\nimport { AnimProps } from '../../../../engine/AnimProps';\nimport { Engine } from '../../../../engine/Engine';\nimport { BoxedVar } from '../../../../util/BoxedVar';\nexport enum AnimationType {\n    None = 0,\n    SlideIn = 1,\n    SlideOut = 2\n}\nexport enum MenuButtonState {\n    Hidden = 0,\n    Unlit = 1,\n    Normal = 2,\n    Active = 3\n}\nexport class MenuSlotAnimationRunner {\n    protected delayFrames: number;\n    protected buttonState: MenuButtonState = MenuButtonState.Hidden;\n    protected collapsed: boolean = true;\n    protected currentAnimationType: AnimationType = AnimationType.None;\n    protected animation?: Animation;\n    constructor(delayFrames: number = 0) {\n        this.delayFrames = delayFrames;\n    }\n    slideIn(): void {\n        this.currentAnimationType = AnimationType.SlideIn;\n        this.initAnimation();\n    }\n    slideOut(): void {\n        this.currentAnimationType = AnimationType.SlideOut;\n        this.initAnimation();\n    }\n    private initAnimation(): void {\n        const iniSection = new IniSection(\"\");\n        let animProps = new AnimProps(iniSection, new ShpFile());\n        animProps.loopEnd = 5;\n        const animation = new Animation(animProps, new BoxedVar(Engine.UI_ANIM_SPEED));\n        this.animation = animation;\n    }\n    tick(time: number): void {\n        let animation = this.animation;\n        const animationType = this.currentAnimationType;\n        if (animation && animationType !== AnimationType.None) {\n            switch (animation.getState()) {\n                case AnimationState.STOPPED:\n                    break;\n                case AnimationState.NOT_STARTED:\n                    animation.start(time, this.delayFrames);\n                // falls through\n                case AnimationState.RUNNING:\n                default:\n                    animation.update(time);\n            }\n            if (animation.getState() === AnimationState.STOPPED) {\n                this.collapsed = animationType === AnimationType.SlideOut;\n                this.currentAnimationType = AnimationType.None;\n            }\n        }\n    }\n    shouldUpdate(): boolean {\n        return true;\n    }\n    isStopped(): boolean {\n        return this.currentAnimationType === AnimationType.None;\n    }\n    getCurrentFrame(): number {\n        if (this.currentAnimationType !== AnimationType.None &&\n            this.animation!.getState() !== AnimationState.DELAYED) {\n            const direction = this.currentAnimationType === AnimationType.SlideIn ? -1 : 1;\n            let baseFrame = this.buttonState !== MenuButtonState.Hidden ? 5 : 11;\n            if (direction === -1) {\n                baseFrame += 5;\n            }\n            return baseFrame + direction * this.animation!.getCurrentFrame();\n        }\n        let frame: number;\n        if (this.collapsed) {\n            frame = this.buttonState === MenuButtonState.Hidden ? 16 : 10;\n        }\n        else if (this.buttonState === MenuButtonState.Hidden) {\n            frame = 0;\n        }\n        else if (this.buttonState === MenuButtonState.Unlit) {\n            frame = 1;\n        }\n        else if (this.buttonState === MenuButtonState.Normal) {\n            frame = 2;\n        }\n        else if (this.buttonState === MenuButtonState.Active) {\n            frame = 4;\n        }\n        else {\n            throw new Error(`Unknown buttonState \"${this.buttonState}\"`);\n        }\n        return frame;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/MenuTooltip.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport classNames from 'classnames';\ninterface MenuTooltipProps {\n    monitorContainer: {\n        getElement(): HTMLElement;\n    };\n}\nexport const MenuTooltip: React.FC<MenuTooltipProps> = ({ monitorContainer }) => {\n    const [tooltipText, setTooltipText] = useState(\"\");\n    const [isAnimating, setIsAnimating] = useState(false);\n    useEffect(() => {\n        let element = monitorContainer.getElement();\n        let lastTarget: EventTarget | null = null;\n        const handleMouseEvent = (event: MouseEvent) => {\n            let target = event.target as HTMLElement;\n            if (target !== lastTarget) {\n                lastTarget = target;\n                let tooltip = target.getAttribute?.(\"data-r-tooltip\");\n                while (target && target !== element && !tooltip) {\n                    target = target.parentElement as HTMLElement;\n                    tooltip = target.getAttribute(\"data-r-tooltip\");\n                }\n                setTooltipText(tooltip || \"\");\n            }\n        };\n        element.addEventListener(\"mousemove\", handleMouseEvent);\n        element.addEventListener(\"mouseleave\", handleMouseEvent);\n        return () => {\n            element.removeEventListener(\"mousemove\", handleMouseEvent);\n            element.removeEventListener(\"mouseleave\", handleMouseEvent);\n        };\n    }, []);\n    useEffect(() => {\n        setIsAnimating(false);\n        const timeout = setTimeout(() => setIsAnimating(true), 10);\n        return () => clearTimeout(timeout);\n    }, [tooltipText]);\n    return (<div className={classNames(\"menu-tooltip\", { anim: isAnimating })}>\n      {tooltipText}\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/MenuVideo.tsx",
    "content": "import React from \"react\";\nimport { CompositeDisposable } from \"../../../../util/disposable/CompositeDisposable\";\nconst mimeTypeMap = new Map([\n    [\"mp4\", \"video/mp4\"],\n    [\"webm\", \"video/webm\"],\n]);\ninterface MenuVideoProps {\n    src: string | File | undefined;\n}\ninterface MenuVideoState {\n}\nexport class MenuVideo extends React.Component<MenuVideoProps, MenuVideoState> {\n    private el: HTMLDivElement | null = null;\n    private disposables: CompositeDisposable = new CompositeDisposable();\n    private disposed: boolean = false;\n    private timeoutId?: number;\n    constructor(props: MenuVideoProps) {\n        super(props);\n    }\n    render() {\n        const src = this.props.src;\n        let url: string;\n        let mimeType: string;\n        if (typeof src === \"string\") {\n            url = src;\n            mimeType = mimeTypeMap.get(src.split(\"?\")[0].split(\".\").pop() ?? \"\") ?? \"video/webm\";\n        }\n        else if (src) {\n            url = URL.createObjectURL(src);\n            mimeType = src.type;\n            this.disposables.add(() => {\n                URL.revokeObjectURL(url);\n            });\n        }\n        else {\n            url = \"\";\n            mimeType = \"video/webm\";\n        }\n        return React.createElement(\"div\", {\n            className: \"video-wrapper\",\n            ref: (ref) => (this.el = ref as HTMLDivElement),\n            dangerouslySetInnerHTML: {\n                __html: `\n          <video style=\"outline: none;\" loop playsinline muted autoplay>\n              <source src=\"${url}\" type=\"${mimeType}\" />\n          </video>\n        `,\n            },\n        });\n    }\n    componentDidMount() {\n        const src = this.props.src;\n        const video = this.el?.querySelector(\"video\");\n        const logo = this.el?.querySelector(\"div\");\n        if (!src || !video || !logo) {\n            console.log('[MenuVideo] No video source provided or elements not found');\n            return;\n        }\n        if (src instanceof File && window.MediaSource) {\n            const errorHandler = async () => {\n                console.log('[MenuVideo] Video source error, trying MediaSource fallback');\n                this.applyMediaSourceFallback(video, await src.arrayBuffer());\n            };\n            const source = video.querySelector(\"source\");\n            if (source) {\n                source.addEventListener(\"error\", errorHandler, { once: true });\n                video.addEventListener(\"loadeddata\", () => {\n                    source.removeEventListener(\"error\", errorHandler);\n                    console.log('[MenuVideo] Video loaded successfully');\n                });\n            }\n        }\n        video.addEventListener(\"loadeddata\", () => {\n            logo.style.opacity = \"\";\n            console.log('[MenuVideo] Video data loaded, showing logo');\n        });\n        video.addEventListener(\"error\", (e) => {\n            console.error('[MenuVideo] Video error:', e);\n        });\n    }\n    private async applyMediaSourceFallback(video: HTMLVideoElement, buffer: ArrayBuffer): Promise<void> {\n        if (!this.disposed) {\n            const mediaSource = new MediaSource();\n            mediaSource.addEventListener(\"sourceopen\", () => {\n                try {\n                    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs=\"vp8\"');\n                    sourceBuffer.mode = \"sequence\";\n                    sourceBuffer.appendBuffer(buffer);\n                    this.timeoutId = setTimeout(() => this.processNextSegment(sourceBuffer, video, buffer), 1000);\n                    this.disposables.add(() => {\n                        if (this.timeoutId) {\n                            clearTimeout(this.timeoutId);\n                        }\n                    });\n                }\n                catch (error) {\n                    if ((error as Error).name !== \"NotSupportedError\") {\n                        console.error(error);\n                    }\n                    return;\n                }\n            });\n            const objectUrl = (video.src = URL.createObjectURL(mediaSource));\n            this.disposables.add(() => {\n                URL.revokeObjectURL(objectUrl);\n            });\n        }\n    }\n    private processNextSegment(sourceBuffer: SourceBuffer, video: HTMLVideoElement, buffer: ArrayBuffer): void {\n        try {\n            if (this.disposed || !sourceBuffer || sourceBuffer.updating) {\n                return;\n            }\n            if (!sourceBuffer.buffered) {\n                console.warn('[MenuVideo] SourceBuffer has been removed from MediaSource');\n                return;\n            }\n            if (sourceBuffer.buffered.length > 0) {\n                if (sourceBuffer.buffered.end(sourceBuffer.buffered.length - 1) - video.currentTime < 10) {\n                    sourceBuffer.appendBuffer(buffer);\n                }\n                if (video.paused) {\n                    video.play()?.catch((error) => console.error(error));\n                }\n            }\n        }\n        catch (error) {\n            console.error('[MenuVideo] Error in processNextSegment:', error);\n            return;\n        }\n        if (!this.disposed) {\n            this.timeoutId = setTimeout(() => this.processNextSegment(sourceBuffer, video, buffer), 1000);\n        }\n    }\n    componentWillUnmount() {\n        this.disposables.dispose();\n        this.disposed = true;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/PrefetchProgress.tsx",
    "content": "import React from 'react';\ninterface PrefetchProgressProps {\n    progress: number;\n    statusText: string;\n}\nexport const PrefetchProgress: React.FC<PrefetchProgressProps> = ({ progress, statusText }) => {\n    return (<div className=\"prefetch-progress\">\n      <div>\n        <label>{statusText}</label>\n        <progress value={progress} max={100}/>\n      </div>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/SidebarPreview.tsx",
    "content": "import * as THREE from 'three';\nimport { jsx } from '../../../jsx/jsx';\nimport { UiComponent } from '../../../jsx/UiComponent';\nimport { UiObject } from '../../../UiObject';\nimport { HtmlContainer } from '../../../HtmlContainer';\nimport { MenuSdTopAnimRunner } from './MenuSdTopAnimRunner';\nimport { IniSection } from '../../../../data/IniSection';\nimport { AnimProps } from '../../../../engine/AnimProps';\nimport { Animation } from '../../../../engine/Animation';\nimport { SimpleRunner } from '../../../../engine/animation/SimpleRunner';\nimport { HtmlView } from '../../../jsx/HtmlView';\nimport { SidebarTitle } from './SidebarTitle';\nimport { Engine } from '../../../../engine/Engine';\nimport { BoxedVar } from '../../../../util/BoxedVar';\nimport { ShpFile } from '../../../../data/ShpFile';\ninterface SidebarPreviewProps {\n    closed: boolean;\n    preview?: any;\n    title?: string;\n    sdtpImg: string;\n    sdtpAnimImg: ShpFile;\n}\nexport class SidebarPreview extends UiComponent {\n    private sidebarPreviewNeedsRefresh: boolean = false;\n    private closed: boolean;\n    private preview?: any;\n    private title?: string;\n    private sidebarTop?: any;\n    private titleView?: any;\n    private sidebarTopPreviewAnim?: any;\n    private sidebarTopClosedAnim?: any;\n    private previewContainer?: any;\n    constructor(props: SidebarPreviewProps) {\n        super(props);\n        this.props = props;\n        this.closed = props.closed;\n        this.preview = props.preview;\n        this.title = props.title;\n    }\n    createUiObject(): UiObject {\n        let uiObject = new UiObject(new THREE.Object3D(), new HtmlContainer());\n        uiObject.onFrame.subscribe((frameData) => this.handleFrame(frameData));\n        return uiObject;\n    }\n    defineChildren(): any {\n        const { sdtpImg, sdtpAnimImg } = this.props;\n        const closed = this.closed;\n        let preview = this.preview;\n        const title = this.title || \"\";\n        const iniSection = new IniSection(\"\");\n        let animProps = new AnimProps(iniSection, sdtpAnimImg);\n        animProps.loopCount = -1;\n        const animation = new Animation(animProps, new BoxedVar(Engine.UI_ANIM_SPEED));\n        let runner = new SimpleRunner();\n        runner.animation = animation;\n        const previewSize = this.getPreviewSize();\n        return jsx(\"fragment\", null, jsx(\"sprite\", {\n            image: sdtpImg,\n            palette: \"shell.pal\",\n            frame: closed ? 0 : 1,\n            ref: (element: any) => (this.sidebarTop = element),\n        }), jsx(HtmlView, {\n            component: SidebarTitle,\n            props: { title: title },\n            innerRef: (element: any) => (this.titleView = element),\n            x: 25,\n            y: 3,\n            width: 118,\n            height: this.closed ? 32 : 18,\n        }), jsx(\"sprite\", {\n            image: \"sdwrntmp.shp\",\n            palette: \"shell.pal\",\n            hidden: true,\n            ref: (element: any) => (this.sidebarTopPreviewAnim = element),\n            animationRunner: new MenuSdTopAnimRunner(),\n        }), jsx(\"sprite\", {\n            image: sdtpAnimImg,\n            palette: \"shell2.pal\",\n            x: 38,\n            y: 48,\n            hidden: !closed,\n            ref: (element: any) => (this.sidebarTopClosedAnim = element),\n            animationRunner: runner,\n        }), jsx(\"container\", {\n            hidden: !preview || closed,\n            ref: (element: any) => {\n                this.previewContainer = element;\n                if (preview && this.previewContainer) {\n                    this.previewContainer.add(preview);\n                }\n            },\n            x: 12,\n            y: 40,\n            width: previewSize.width,\n            height: previewSize.height,\n        }));\n    }\n    getPreviewSize(): {\n        width: number;\n        height: number;\n    } {\n        return { width: 146, height: 112 };\n    }\n    toggleSidebarPreview(show: boolean): void {\n        if (this.closed !== !show) {\n            let animRunner = this.sidebarTopPreviewAnim.getAnimationRunner();\n            if (show) {\n                animRunner.slideIn();\n            }\n            else {\n                animRunner.slideOut();\n            }\n            this.closed = !show;\n            this.sidebarPreviewNeedsRefresh = true;\n            this.sidebarTopPreviewAnim.setVisible(true);\n            (show ? this.sidebarTopClosedAnim : this.previewContainer).setVisible(false);\n            this.titleView.setVisible(false);\n            this.updateTitleSize();\n        }\n    }\n    setPreview(preview: any): void {\n        if (this.preview && this.previewContainer) {\n            this.previewContainer.remove(this.preview);\n        }\n        if (preview && this.previewContainer) {\n            this.previewContainer.add(preview);\n        }\n        this.preview = preview;\n    }\n    setTitle(title: string): void {\n        console.log('[SidebarPreview] setTitle called with:', title);\n        console.log('[SidebarPreview] titleView exists:', !!this.titleView);\n        this.title = title;\n        if (this.titleView) {\n            console.log('[SidebarPreview] Applying title to titleView');\n            this.titleView.applyOptions((options: any) => {\n                console.log('[SidebarPreview] Setting options.title to:', title);\n                options.title = title;\n            });\n            this.updateTitleSize();\n        }\n        else {\n            console.warn('[SidebarPreview] titleView is null, cannot set title');\n        }\n    }\n    private updateTitleSize(): void {\n        if (this.titleView) {\n            this.titleView.setSize(this.titleView.getSize().width, this.closed ? 32 : 18);\n        }\n    }\n    private handleFrame(frameData: any): void {\n        if (this.sidebarPreviewNeedsRefresh) {\n            let animRunner = this.sidebarTopPreviewAnim.getAnimationRunner();\n            if (animRunner.isStopped()) {\n                (this.closed ? this.sidebarTopClosedAnim : this.previewContainer).setVisible(true);\n                this.sidebarTopPreviewAnim.setVisible(false);\n                this.sidebarTop.setFrame(this.closed ? 0 : 1);\n                this.titleView.setVisible(true);\n                this.sidebarPreviewNeedsRefresh = false;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/SidebarTitle.tsx",
    "content": "import React from 'react';\ninterface SidebarTitleProps {\n    title: string;\n}\nexport const SidebarTitle: React.FC<SidebarTitleProps> = ({ title }) => {\n    console.log('[SidebarTitle] Rendering with title:', title);\n    return (<div className=\"sidebar-title\">\n      {title}\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/VersionString.tsx",
    "content": "import React from 'react';\ninterface VersionStringProps {\n    value: string;\n}\nexport const VersionString: React.FC<VersionStringProps> = ({ value }) => {\n    return (<div className=\"menu-version-string\">\n      v{value}\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/component/viewmodel/MenuButtonConfig.ts",
    "content": "export interface MenuButtonConfig {\n    label: string;\n    action: () => void;\n    icon?: string;\n    disabled?: boolean;\n    visible?: boolean;\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/credits/Credits.tsx",
    "content": "import React from 'react';\nimport { Strings } from '../../../../data/Strings';\nexport interface CreditsProps {\n    contentTpl: string;\n    strings: Strings;\n}\nexport const Credits: React.FC<CreditsProps> = ({ contentTpl, strings }) => {\n    const processedContent = contentTpl\n        .replace(/\\{([^}]+)\\}/g, (match, key) => strings.get(key) || match)\n        .replace(/<([^>]+)>/g, (match, url) => url.match(/^(https?|mailto):(\\/\\/)?/)\n        ? `<a href='${encodeURI(url)}' target='_blank' rel='noopener'>${encodeURI(url)}</a>`\n        : \"\")\n        .replace(/\\t*\\r?\\n/g, \"<br />\")\n        .replace(/([^>]+)\\t+([^<]+)<br \\/>/g, `<div class='def'>\n        <span class='title'>$1</span>\n        <span class='filler'></span>\n        <span class='name'>$2</span>\n      </div>`);\n    return (<div className=\"credits-container\">\n      <div className=\"credits\" dangerouslySetInnerHTML={{ __html: processedContent }}/>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/credits/CreditsScreen.ts",
    "content": "import { Screen } from '../../Controller';\nimport { MainMenuController } from '../MainMenuController';\nimport { Strings } from '../../../../data/Strings';\nimport { JsxRenderer } from '../../../jsx/JsxRenderer';\nimport { Engine } from '../../../../engine/Engine';\nimport { Credits } from './Credits';\nimport { jsx } from '../../../jsx/jsx';\nimport { HtmlView } from '../../../jsx/HtmlView';\nexport class CreditsScreen implements Screen {\n    private strings: Strings;\n    private jsxRenderer: JsxRenderer;\n    private controller?: MainMenuController;\n    public title: string;\n    constructor(strings: Strings, jsxRenderer: JsxRenderer) {\n        this.strings = strings;\n        this.jsxRenderer = jsxRenderer;\n        this.title = this.strings.get(\"GUI:Credits\") || \"Credits\";\n    }\n    setController(controller: MainMenuController): void {\n        this.controller = controller;\n    }\n    onEnter(): void {\n        console.log('[CreditsScreen] Entering credits screen');\n        this.controller?.setSidebarButtons([\n            {\n                label: this.strings.get(\"GUI:Back\") || \"Back\",\n                isBottom: true,\n                onClick: () => {\n                    console.log('[CreditsScreen] Back clicked');\n                    this.controller?.leaveCurrentScreen();\n                }\n            }\n        ]);\n        this.controller?.showSidebarButtons();\n        this.controller?.toggleMainVideo(false);\n        let creditscdContent = \"\";\n        let creditsContent = \"\";\n        try {\n            if (Engine.vfs) {\n                try {\n                    creditscdContent = Engine.vfs.openFile(\"creditscd.txt\").readAsString(\"utf-8\") || \"\";\n                }\n                catch (e) {\n                    console.warn('[CreditsScreen] creditscd.txt not found, using empty content');\n                    creditscdContent = \"\";\n                }\n                try {\n                    creditsContent = Engine.vfs.openFile(\"credits.txt\").readAsString() || \"\";\n                }\n                catch (e) {\n                    console.warn('[CreditsScreen] credits.txt not found, using fallback content');\n                    creditsContent = this.getFallbackCreditsContent();\n                }\n            }\n            else {\n                console.warn('[CreditsScreen] VFS not available, using fallback content');\n                creditsContent = this.getFallbackCreditsContent();\n            }\n        }\n        catch (error) {\n            console.error('[CreditsScreen] Error reading credits files:', error);\n            creditsContent = this.getFallbackCreditsContent();\n        }\n        const finalContent = creditsContent.replace(/\\s+\\{CRD:CREDITS\\}\\s+/, creditscdContent);\n        try {\n            const [renderedElement] = this.jsxRenderer.render(jsx(HtmlView, {\n                width: \"100%\",\n                height: \"100%\",\n                component: Credits,\n                props: {\n                    contentTpl: finalContent,\n                    strings: this.strings\n                }\n            }));\n            this.controller?.setMainComponent(renderedElement);\n        }\n        catch (error) {\n            console.error('[CreditsScreen] Error rendering credits:', error);\n            this.controller?.setMainComponent(this.createFallbackElement(finalContent));\n        }\n    }\n    async onLeave(): Promise<void> {\n        console.log('[CreditsScreen] Leaving credits screen');\n        if (this.controller) {\n            await this.controller.hideSidebarButtons();\n        }\n    }\n    async onStack(): Promise<void> {\n        await this.onLeave();\n    }\n    onUnstack(): void {\n        this.onEnter();\n    }\n    private getFallbackCreditsContent(): string {\n        return `网页红井制作组\\t\\n\\n` +\n            `原项目开发\\tChronodivide\\n` +\n            `React迁移\\t网页红井制作组\\n` +\n            `技术支持\\t思牛逼公众号\\n\\n` +\n            `{TS:Disclaimer}\\n\\n` +\n            `{TXT_Copyright}`;\n    }\n    private createFallbackElement(content: string): HTMLElement {\n        const div = document.createElement('div');\n        div.className = 'credits-container';\n        div.style.cssText = `\n      width: 100%;\n      height: 100%;\n      overflow-y: auto;\n      padding: 20px;\n      color: white;\n      background: rgba(0, 0, 0, 0.8);\n    `;\n        const creditsDiv = document.createElement('div');\n        creditsDiv.className = 'credits';\n        creditsDiv.innerHTML = content\n            .replace(/\\{([^}]+)\\}/g, (match, key) => this.strings.get(key) || match)\n            .replace(/\\t*\\r?\\n/g, \"<br />\")\n            .replace(/([^>]+)\\t+([^<]+)<br \\/>/g, `<div style=\"display: flex; justify-content: space-between; margin: 5px 0;\">\n          <span>$1</span>\n          <span>$2</span>\n        </div>`);\n        div.appendChild(creditsDiv);\n        return div;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/customGame/CustomGameScreen.ts",
    "content": "import { GameBrowser } from './component/GameBrowser';\nimport { ScreenType } from '../../ScreenType';\nimport { CompositeDisposable } from '../../../../util/disposable/CompositeDisposable';\nimport { jsx } from '../../../jsx/jsx';\nimport { HtmlView } from '../../../jsx/HtmlView';\nimport { SoundKey } from '../../../../engine/sound/SoundKey';\nimport { ChannelType } from '../../../../engine/sound/ChannelType';\nimport { MusicType } from '../../../../engine/sound/Music';\nimport { IrcConnection } from '../../../../network/IrcConnection';\nimport { MainMenuScreen } from '../MainMenuScreen';\nimport { Task } from '@puzzl/core/lib/async/Task';\nimport { OperationCanceledError } from '@puzzl/core/lib/async/cancellation/OperationCanceledError';\nimport { MAX_LIST_SEARCH_COUNT } from '../../../../network/ladder/wladderConfig';\nimport { MainMenuRoute } from '../MainMenuRoute';\nimport { ChatMessage, ChatRecipientType } from '../../../../network/chat/ChatMessage';\nimport { ChatHistory } from '../../../chat/ChatHistory';\nimport { WolError } from '../../../../network/WolError';\nimport { CancellationTokenSource, CancellationToken } from '@puzzl/core/lib/async/cancellation';\ninterface Game {\n    name: string;\n    mapName: string;\n    description: string;\n    hostName: string;\n    hostPing?: number;\n    hostMuted: boolean;\n    humanPlayers: number;\n    aiPlayers: number;\n    maxPlayers: number;\n    passLocked: boolean;\n    observable: boolean;\n    observers?: number;\n    tournament?: boolean;\n    modName?: string;\n    modHash?: string;\n}\ninterface User {\n    name: string;\n    operator: boolean;\n}\ninterface PlayerProfile {\n    name: string;\n    rank?: number;\n}\ninterface ServerRegion {\n    name: string;\n}\ninterface ServerRegions {\n    getSize(): number;\n}\ninterface WolService {\n    getConfig(): {\n        getClientChannelType(): number;\n        getGlobalChannelPass(): string;\n    };\n    closeWolConnection(): void;\n    onWolConnectionLost: {\n        subscribe(handler: (error: any) => void): void;\n        unsubscribe(handler: (error: any) => void): void;\n    };\n}\ninterface WladderService {\n    getUrl(): string | null;\n    listSearch(names: string[], cancellationToken: CancellationToken): Promise<PlayerProfile[]>;\n}\ninterface MapList {\n    getByName(name: string): any;\n}\ninterface Sound {\n    play(key: SoundKey, channel: ChannelType): void;\n}\ninterface JsxRenderer {\n    render(jsx: any): any[];\n}\ninterface ErrorHandler {\n    handle(error: any, message: string, onClose: () => void): void;\n}\nexport class CustomGameScreen extends MainMenuScreen {\n    public title: string;\n    public musicType: MusicType;\n    private engineModHash: string;\n    private strings: any;\n    private wolCon: IrcConnection;\n    private wolService: WolService;\n    private wladderService: WladderService;\n    private jsxRenderer: JsxRenderer;\n    private sound: Sound;\n    private serverRegions: ServerRegions;\n    private mapList: MapList;\n    private errorHandler: ErrorHandler;\n    private playerProfiles = new Map<string, PlayerProfile>();\n    private disposables = new CompositeDisposable();\n    private channelName?: string;\n    private users: User[] = [];\n    private games: Game[] = [];\n    private selectedGame?: Game;\n    private messages: any[] = [];\n    private chatHistory?: ChatHistory;\n    private gameBrowser?: any;\n    private refreshTimeoutId?: number;\n    private ranksUpdateTask?: Task<void>;\n    constructor(engineModHash: string, strings: any, wolCon: IrcConnection, wolService: WolService, wladderService: WladderService, jsxRenderer: JsxRenderer, sound: Sound, serverRegions: ServerRegions, mapList: MapList, errorHandler: ErrorHandler) {\n        super();\n        this.engineModHash = engineModHash;\n        this.strings = strings;\n        this.wolCon = wolCon;\n        this.wolService = wolService;\n        this.wladderService = wladderService;\n        this.jsxRenderer = jsxRenderer;\n        this.sound = sound;\n        this.serverRegions = serverRegions;\n        this.mapList = mapList;\n        this.errorHandler = errorHandler;\n        this.title = this.strings.get(\"GUI:CustomMatch\");\n        this.musicType = MusicType.NormalShuffle;\n    }\n    private onChannelJoinLeave = (event: any) => {\n        let channelName = event.channel;\n        const lobbyMatch = channelName.match(/#Lob \\d+ (\\d)/i);\n        if (lobbyMatch) {\n            const [, lobbyNum] = lobbyMatch.map(Number);\n            channelName = this.strings.get(\"TXT_LOB_\" + (lobbyNum + 1));\n        }\n        if (event.user.name === this.wolCon.getCurrentUser()) {\n            this.addSystemMessage(this.strings.get(event.type === \"join\" ? \"TXT_JOINED_S\" : \"TXT_YOULEFT\", channelName));\n        }\n        else if (event.channel === this.channelName) {\n            if (event.type === \"join\") {\n                this.users.push(event.user);\n                this.users.sort((a, b) => Number(b.operator) - Number(a.operator));\n            }\n            else {\n                const index = this.users.findIndex(user => user.name === event.user.name);\n                if (index !== -1) {\n                    this.users.splice(index, 1);\n                }\n            }\n            this.gameBrowser?.refresh();\n        }\n    };\n    private onChannelUsers = (event: any) => {\n        if (event.channelName === this.channelName) {\n            this.users = event.users;\n            this.gameBrowser?.applyOptions((options: any) => {\n                options.users = this.users;\n            });\n        }\n    };\n    private onChannelMessage = (message: any) => {\n        if (message.to.type === ChatRecipientType.Page || message.to.type === ChatRecipientType.Whisper) {\n            this.sound.play(SoundKey.IncomingMessage, ChannelType.Ui);\n        }\n        const messageWithOperator = {\n            ...message,\n            operator: this.users.find(user => user.name === message.from)?.operator,\n        };\n        this.messages.push(messageWithOperator);\n        this.gameBrowser?.refresh();\n        if (message.to.type === ChatRecipientType.Whisper &&\n            message.to.name !== this.wolCon.getServerName() &&\n            message.from !== this.wolCon.getCurrentUser()) {\n            this.chatHistory!.lastWhisperFrom.value = message.from;\n        }\n    };\n    private onWolClose = () => {\n        if (this.refreshTimeoutId) {\n            clearInterval(this.refreshTimeoutId);\n        }\n    };\n    private onWolConLost = (error: any) => {\n        this.handleError(error, this.strings.get(\"TXT_YOURE_DISCON\"));\n    };\n    private addSystemMessage(text: string) {\n        this.messages.push({ text });\n        this.gameBrowser?.refresh();\n    }\n    private async refreshGames(cancellationToken: CancellationToken) {\n        if (!this.wolCon.isOpen()) {\n            this.onWolClose();\n            return;\n        }\n        if (!this.channelName || !this.wolCon.getCurrentUser()) {\n            return;\n        }\n        try {\n            const clientChannelType = this.wolService.getConfig().getClientChannelType();\n            const games = await this.wolCon.listGames(clientChannelType, clientChannelType);\n            if (cancellationToken?.isCancelled()) {\n                return;\n            }\n            games.sort((a, b) => Number(a.passLocked) - Number(b.passLocked));\n            this.games = games;\n            const currentSelection = this.selectedGame;\n            if (currentSelection) {\n                this.onGameSelectionChange(games.find(game => game.name === currentSelection.name));\n            }\n            this.gameBrowser?.applyOptions((options: any) => {\n                options.games = games;\n            });\n            this.refreshPlayerRanks();\n        }\n        catch (error) {\n            if (error instanceof IrcConnection.SocketError || error instanceof IrcConnection.NoReplyError) {\n                return;\n            }\n            throw error;\n        }\n    }\n    private refreshPlayerRanks() {\n        if (!this.wladderService.getUrl()) {\n            return;\n        }\n        this.ranksUpdateTask?.cancel();\n        const task = new Task<void>(async (cancellationToken) => {\n            const uniqueNames = [\n                ...new Set([\n                    ...this.users.map(user => user.name),\n                    ...this.games.map(game => game.hostName),\n                ])\n            ].filter(name => !this.playerProfiles.has(name));\n            if (uniqueNames.length > 0) {\n                while (uniqueNames.length > 0) {\n                    const batch = uniqueNames.splice(0, MAX_LIST_SEARCH_COUNT);\n                    const profiles = await this.wladderService.listSearch(batch, cancellationToken);\n                    if (cancellationToken.isCancelled()) {\n                        return;\n                    }\n                    for (const profile of profiles) {\n                        this.playerProfiles.set(profile.name, profile);\n                    }\n                }\n                this.gameBrowser?.refresh();\n            }\n        });\n        this.ranksUpdateTask = task;\n        task.start().catch(error => {\n            if (!(error instanceof OperationCanceledError)) {\n                console.error(error);\n            }\n        });\n    }\n    private onGameSelectionChange(game?: Game) {\n        this.selectedGame = game;\n        this.refreshSidebarButtons();\n    }\n    private gameIsFull(game: Game): boolean {\n        return game.humanPlayers + game.aiPlayers === game.maxPlayers - (game.observable ? 1 : 0);\n    }\n    private refreshSidebarButtons() {\n        const game = this.selectedGame;\n        const buttons = [\n            {\n                label: this.strings.get(\"GUI:CreateGame\"),\n                tooltip: this.strings.get(\"STT:LobbyButtonNew\"),\n                onClick: () => this.createGame(),\n            },\n            {\n                label: this.strings.get(\"GUI:JoinGame\"),\n                tooltip: this.strings.get(\"STT:LobbyButtonJoin\"),\n                disabled: !game || this.gameIsFull(game),\n                onClick: () => this.joinGame(game!),\n            },\n            {\n                label: this.strings.get(\"GUI:Observe\"),\n                tooltip: this.strings.get(\"STT:LobbyButtonObserve\"),\n                disabled: !game || !game.observable || !!game.observers,\n                onClick: () => this.observeGame(game!),\n            },\n            ...(this.serverRegions.getSize() > 1 ? [{\n                    label: this.strings.get(\"GUI:ChangeServer\"),\n                    tooltip: this.strings.get(\"STT:ChangeServer\"),\n                    onClick: () => {\n                        this.wolService.closeWolConnection();\n                        this.controller?.goToScreen(ScreenType.Login, {\n                            clearCredentials: true,\n                            afterLogin: (messages: any) => new MainMenuRoute(ScreenType.CustomGame, { messages }),\n                        });\n                    },\n                }] : []),\n            {\n                label: this.strings.get(\"GUI:Back\"),\n                tooltip: this.strings.get(\"STT:LobbyButtonBack\"),\n                isBottom: true,\n                onClick: () => {\n                    this.wolService.closeWolConnection();\n                    this.controller?.goToScreen(ScreenType.Home);\n                },\n            },\n        ];\n        this.controller.setSidebarButtons(buttons);\n    }\n    private initView(cancellationToken: CancellationToken) {\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            innerRef: (ref: any) => (this.gameBrowser = ref),\n            component: GameBrowser,\n            props: {\n                strings: this.strings,\n                messages: this.messages,\n                chatHistory: this.chatHistory,\n                channels: [this.channelName!],\n                localUsername: this.wolCon.getCurrentUser(),\n                users: this.users,\n                games: this.games,\n                playerProfiles: this.playerProfiles,\n                mapList: this.mapList,\n                onSendMessage: (message: any) => {\n                    if (message.value.length) {\n                        if (this.wolCon.isOpen()) {\n                            this.wolCon.sendChatMessage(message.value, message.recipient);\n                            if (message.recipient.type === ChatRecipientType.Whisper) {\n                                this.chatHistory!.lastWhisperTo.value = message.recipient.name;\n                            }\n                        }\n                    }\n                    else {\n                        this.addSystemMessage(this.strings.get(\"TXT_ENTER_MESSAGE\"));\n                    }\n                },\n                onRefreshClick: () => this.refreshGames(cancellationToken),\n                onSelectGame: (game: Game) => this.onGameSelectionChange(game),\n                onDoubleClickGame: (game: Game) => {\n                    if (!this.gameIsFull(game)) {\n                        this.joinGame(game);\n                    }\n                },\n            },\n        }));\n        this.controller.setMainComponent(component);\n        this.refreshSidebarButtons();\n        this.controller.showSidebarButtons();\n    }\n    async onEnter(params?: any): Promise<void> {\n        this.messages = params?.messages ?? [];\n        this.chatHistory = new ChatHistory();\n        this.controller.toggleMainVideo(false);\n        const tokenSource = new CancellationTokenSource();\n        this.disposables.add(() => tokenSource.cancel());\n        const cancellationToken = tokenSource.token;\n        if (this.wolCon.getCurrentUser()) {\n            await this.loadChannel(cancellationToken);\n        }\n        else {\n            this.controller.goToScreen(ScreenType.Login, {\n                afterLogin: (messages: any) => new MainMenuRoute(ScreenType.CustomGame, { messages }),\n            });\n        }\n    }\n    private async loadChannel(cancellationToken: CancellationToken) {\n        this.channelName = undefined;\n        this.users = [];\n        this.games = [];\n        this.selectedGame = undefined;\n        this.wolCon.onJoinChannel.subscribe(this.onChannelJoinLeave);\n        this.wolCon.onLeaveChannel.subscribe(this.onChannelJoinLeave);\n        this.wolCon.onChannelUsers.subscribe(this.onChannelUsers);\n        this.wolCon.onChatMessage.subscribe(this.onChannelMessage);\n        this.wolCon.onClose.subscribe(this.onWolClose);\n        this.wolService.onWolConnectionLost.subscribe(this.onWolConLost);\n        try {\n            const config = this.wolService.getConfig();\n            const channelName = `#Lob ${config.getClientChannelType()} 0`;\n            await this.wolCon.joinChannel(channelName, config.getGlobalChannelPass());\n            if (cancellationToken.isCancelled()) {\n                return;\n            }\n            this.channelName = channelName;\n            this.playerProfiles.clear();\n            this.initView(cancellationToken);\n            this.gameBrowser?.applyOptions((options: any) => {\n                options.users = this.users;\n            });\n            this.refreshGames(cancellationToken);\n            this.refreshTimeoutId = setInterval(() => this.refreshGames(cancellationToken), 5000);\n        }\n        catch (error) {\n            let message = this.strings.get(\"WOL:MatchBadParameters\");\n            if (error instanceof WolError) {\n                const errorMessages = new Map([\n                    [WolError.Code.NoSuchChannel, \"WOL:ChannelJoinFailure\"],\n                    [WolError.Code.BadChannelPass, \"TXT_BADPASS\"],\n                    [WolError.Code.ChannelFull, \"TXT_CHANNEL_FULL\"],\n                    [WolError.Code.BannedFromChannel, \"TXT_JOINBAN\"],\n                ]);\n                const errorMessage = errorMessages.get(error.code);\n                if (errorMessage) {\n                    message = this.strings.get(errorMessage);\n                }\n            }\n            this.handleError(error, message);\n        }\n    }\n    async onLeave(): Promise<void> {\n        this.disposables.dispose();\n        if (this.refreshTimeoutId) {\n            clearInterval(this.refreshTimeoutId);\n        }\n        if (this.ranksUpdateTask) {\n            this.ranksUpdateTask.cancel();\n            this.ranksUpdateTask = undefined;\n        }\n        if (this.wolCon.isOpen() && this.channelName) {\n            this.wolCon.leaveChannel(this.channelName);\n        }\n        this.wolCon.onJoinChannel.unsubscribe(this.onChannelJoinLeave);\n        this.wolCon.onLeaveChannel.unsubscribe(this.onChannelJoinLeave);\n        this.wolCon.onChannelUsers.unsubscribe(this.onChannelUsers);\n        this.wolCon.onChatMessage.unsubscribe(this.onChannelMessage);\n        this.wolCon.onClose.unsubscribe(this.onWolClose);\n        this.wolService.onWolConnectionLost.unsubscribe(this.onWolConLost);\n        await this.controller.hideSidebarButtons();\n        this.channelName = undefined;\n        this.gameBrowser = undefined;\n        this.messages = [];\n        this.users = [];\n        this.playerProfiles.clear();\n        this.games = [];\n        this.selectedGame = undefined;\n    }\n    private async createGame() {\n        this.controller.goToScreen(ScreenType.Lobby, { create: true });\n    }\n    private async joinGame(game: Game) {\n        this.joinRoom(game, false);\n    }\n    private observeGame(game: Game) {\n        this.joinRoom(game, true);\n    }\n    private joinRoom(game: Game, observe: boolean) {\n        if (game.modHash === this.engineModHash) {\n            this.controller.goToScreen(ScreenType.Lobby, {\n                game,\n                observe,\n            });\n        }\n        else if (game.modHash !== undefined) {\n            this.addSystemMessage(this.strings.get(\"TXT_MISMATCH\"));\n        }\n    }\n    private handleError(error: any, message: string) {\n        this.errorHandler.handle(error, message, () => {\n            this.wolService.closeWolConnection();\n            this.controller?.goToScreen(ScreenType.Home);\n        });\n        if (this.refreshTimeoutId) {\n            clearInterval(this.refreshTimeoutId);\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/customGame/component/GameBrowser.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { Chat } from '../../../../component/Chat';\nimport { List, ListHeader, ListItem } from '../../../../component/List';\nimport { Image } from '../../../../component/Image';\nimport { RankIndicator } from '../../lobby/component/RankIndicator';\nimport { ChannelUser } from '../../../../component/ChannelUser';\nimport { ChatRecipientType } from '../../../../../network/chat/ChatMessage';\nimport { Strings } from '../../../../../data/Strings';\ninterface Game {\n    name: string;\n    mapName: string;\n    description: string;\n    hostName: string;\n    hostPing?: number;\n    hostMuted: boolean;\n    humanPlayers: number;\n    aiPlayers: number;\n    maxPlayers: number;\n    passLocked: boolean;\n    observable: boolean;\n    observers?: number;\n    tournament?: boolean;\n    modName?: string;\n    modHash?: string;\n}\ninterface User {\n    name: string;\n    operator: boolean;\n}\ninterface PlayerProfile {\n    name: string;\n    rank?: number;\n}\ninterface MapInfo {\n    official: boolean;\n    getFullMapTitle(strings: Strings): string;\n}\ninterface MapList {\n    getByName(name: string): MapInfo | undefined;\n}\ninterface ChatHistory {\n    lastComposeTarget: {\n        value: any;\n    };\n    lastWhisperFrom: {\n        value: string;\n    };\n    lastWhisperTo: {\n        value: string;\n    };\n}\ninterface GameBrowserProps {\n    strings: Strings;\n    messages: any[];\n    chatHistory: ChatHistory;\n    channels: string[];\n    localUsername: string;\n    users: User[];\n    games: Game[];\n    playerProfiles: Map<string, PlayerProfile>;\n    mapList: MapList;\n    onSendMessage: (message: {\n        value: string;\n        recipient: any;\n    }) => void;\n    onRefreshClick: () => void;\n    onSelectGame: (game: Game | undefined) => void;\n    onDoubleClickGame: (game: Game) => void;\n}\ninterface GameListProps {\n    games: Game[];\n    selectedGame?: Game;\n    mapList: MapList;\n    onClickGame: (game: Game) => void;\n    onDoubleClickGame: (game: Game) => void;\n    tooltip: string;\n    strings: Strings;\n    playerProfiles: Map<string, PlayerProfile>;\n}\ninterface GameItemProps {\n    game: Game;\n    uiMapName: string;\n    customMap: boolean;\n    selected: boolean;\n    tooltip: string;\n    ping?: number;\n    hostProfile?: PlayerProfile;\n    strings: Strings;\n    onClick: (game: Game) => void;\n    onDoubleClick: (game: Game) => void;\n}\nconst GameItem: React.FC<GameItemProps> = ({ game, uiMapName, customMap, selected, tooltip, ping, hostProfile, strings, onClick, onDoubleClick }) => (<ListItem key={game.name} className=\"game\" selected={selected} tooltip={tooltip} onClick={() => onClick(game)} onDoubleClick={() => onDoubleClick(game)}>\n    <span className=\"game-flags\">\n      <span className=\"game-type\" title={game.modName || !customMap\n        ? strings.get(\"GUI:GameMod\", game.modName || strings.get(\"GUI:Official\"))\n        : strings.get(\"GUI:CustomMap\")}>\n        {game.modName || customMap ? (<Image src=\"settings.png\"/>) : (<Image src={game.tournament ? \"woltrny.pcx\" : \"gt18.pcx\"}/>)}\n      </span>\n      <span className=\"game-pass-locked\">\n        {game.passLocked && <Image src=\"wolpriv.pcx\"/>}\n      </span>\n      <span className=\"game-obs\">\n        {game.observable && <Image src=\"wolob.pcx\"/>}\n      </span>\n    </span>\n    <span className=\"game-map\" title={uiMapName}>\n      {uiMapName}\n    </span>\n    <span className=\"game-name\" title={game.hostMuted ? undefined : game.description}>\n      {game.hostMuted ? undefined : game.description}\n    </span>\n    <span className=\"game-players\">\n      {game.maxPlayers\n        ? `${game.humanPlayers + game.aiPlayers}/${game.maxPlayers - (game.observable ? 1 : 0)}`\n        : \"?/?\"}\n    </span>\n    <span className=\"game-host\">\n      {game.hostName}\n      {hostProfile !== undefined && (<RankIndicator playerProfile={hostProfile} strings={strings}/>)}\n    </span>\n    <span className=\"game-ping\">\n      {ping && (<meter value={ping} max={300} low={100} high={250} optimum={0} title={`${ping}ms`}/>)}\n    </span>\n  </ListItem>);\nconst GameList: React.FC<GameListProps> = ({ games, selectedGame, onClickGame, onDoubleClickGame, tooltip, strings, playerProfiles, mapList }) => (<>\n    <ListHeader className=\"game game-list-header\">\n      <span className=\"game-flags\">\n        <span className=\"game-type\"/>\n        <span className=\"game-pass-locked\"/>\n        <span className=\"game-obs\"/>\n      </span>\n      <span className=\"game-map\">{strings.get(\"GUI:Map\")}</span>\n      <span className=\"game-name\">{strings.get(\"GUI:RoomDesc\")}</span>\n      <span className=\"game-players\">👤</span>\n      <span className=\"game-host\">{strings.get(\"GUI:HostName\")}</span>\n      <span className=\"game-ping\">{strings.get(\"GUI:Ping\")}</span>\n    </ListHeader>\n    <List className=\"games-list\" tooltip={tooltip}>\n      {games.map((game) => {\n        const ping = game.hostPing;\n        const hostProfile = playerProfiles.get(game.hostName);\n        const mapInfo = mapList.getByName(game.mapName);\n        const uiMapName = !mapInfo?.official && game.hostMuted\n            ? strings.get(\"GUI:CustomMap\")\n            : mapInfo?.getFullMapTitle(strings) || game.mapName;\n        const tooltipParts = [\n            ...(game.modName ? [strings.get(\"GUI:GameMod\", game.modName)] : []),\n            strings.get(\"TXT_MAP\", uiMapName),\n            ping ? strings.get(\"WOL:GamePing\", ping) : strings.get(\"TXT_UNKNOWN_PING\"),\n            ...(hostProfile?.rank !== undefined ? [`${strings.get(\"TXT_HOST_RANK\")} ${hostProfile.rank}`] : [])\n        ];\n        return (<GameItem key={game.name} game={game} uiMapName={uiMapName} customMap={!mapInfo?.official} ping={ping} hostProfile={hostProfile} tooltip={tooltipParts.join(\", \")} selected={game.name === selectedGame?.name} strings={strings} onClick={onClickGame} onDoubleClick={onDoubleClickGame}/>);\n    })}\n    </List>\n  </>);\nexport const GameBrowser: React.FC<GameBrowserProps> = (props) => {\n    const [selectedGame, setSelectedGame] = useState<Game | undefined>(undefined);\n    useEffect(() => {\n        if (selectedGame && !props.games.find(game => game.name === selectedGame.name)) {\n            setSelectedGame(undefined);\n        }\n    }, [props.games, selectedGame]);\n    const handleSelectGame = (game: Game | undefined) => {\n        setSelectedGame(game);\n        props.onSelectGame(game);\n    };\n    return (<div className=\"gamebrowser-wrapper\">\n      <div className=\"gamebrowser-top\">\n        <div className=\"games\">\n          <div className=\"games-header\">\n            <button className=\"icon-button refresh-button\" onClick={props.onRefreshClick} data-r-tooltip={props.strings.get(\"STT:WOLLobbyRefreshChannels\")}/>\n            <span className=\"games-label\">\n              {props.strings.get(\"GUI:OpenGames\")}\n            </span>\n          </div>\n          <GameList games={props.games} selectedGame={selectedGame} mapList={props.mapList} onClickGame={handleSelectGame} onDoubleClickGame={(game) => {\n            handleSelectGame(game);\n            props.onDoubleClickGame(game);\n        }} tooltip={props.strings.get(\"STT:LobbyListGames\")} strings={props.strings} playerProfiles={props.playerProfiles}/>\n        </div>\n      </div>\n      <div className=\"gamebrowser-bottom\">\n        <Chat strings={props.strings} messages={props.messages} channels={props.channels ?? []} chatHistory={props.chatHistory} localUsername={props.localUsername} onSendMessage={props.onSendMessage} tooltips={{\n            input: props.strings.get(\"STT:LobbyEditInput\"),\n            output: props.strings.get(\"STT:LobbyEditOutput\"),\n            button: props.strings.get(\"STT:EmoteButton\"),\n        }}/>\n        <List className=\"players-list\" tooltip={props.strings.get(\"STT:LobbyListUsers\")}>\n          {props.users.map((user) => {\n            const playerProfile = props.playerProfiles.get(user.name);\n            return (<ChannelUser key={user.name} user={user} playerProfile={playerProfile} strings={props.strings} onClick={() => {\n                    props.chatHistory.lastComposeTarget.value = {\n                        type: ChatRecipientType.Whisper,\n                        name: user.name,\n                    };\n                }}/>);\n        })}\n        </List>\n      </div>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/infoAndCredits/InfoAndCreditsScreen.ts",
    "content": "import { Screen } from '../../Controller';\nimport { MainMenuController } from '../MainMenuController';\nimport { MainMenuScreenType } from '../../ScreenType';\nimport { Strings } from '../../../../data/Strings';\nimport { MessageBoxApi } from '../../../component/MessageBoxApi';\nimport { Config } from '../../../../Config';\nimport { ReportBug } from '../main/ReportBug';\nimport React from 'react';\ninterface SidebarButton {\n    label: string;\n    tooltip?: string;\n    disabled?: boolean;\n    isBottom?: boolean;\n    onClick: () => void | Promise<void>;\n}\nexport class InfoAndCreditsScreen implements Screen {\n    private strings: Strings;\n    private messageBoxApi: MessageBoxApi;\n    private controller?: MainMenuController;\n    public title: string;\n    constructor(strings: Strings, messageBoxApi: MessageBoxApi) {\n        this.strings = strings;\n        this.messageBoxApi = messageBoxApi;\n        this.title = this.strings.get(\"TS:InfoAndCredits\") || \"Info & Credits\";\n    }\n    setController(controller: MainMenuController): void {\n        this.controller = controller;\n    }\n    onEnter(): void {\n        console.log('[InfoAndCreditsScreen] Entering info and credits screen');\n        const buttons: SidebarButton[] = [];\n        buttons.push({\n            label: this.strings.get(\"GUI:ViewCredits\") || \"View Credits\",\n            onClick: () => {\n                console.log('[InfoAndCreditsScreen] View Credits clicked');\n                this.controller?.pushScreen(MainMenuScreenType.Credits);\n            }\n        });\n        buttons.push({\n            label: this.strings.get(\"GUI:Back\") || \"Back\",\n            isBottom: true,\n            onClick: () => {\n                console.log('[InfoAndCreditsScreen] Back clicked');\n                this.controller?.leaveCurrentScreen();\n            }\n        });\n        this.controller?.setSidebarButtons(buttons);\n        this.controller?.showSidebarButtons();\n        this.controller?.toggleMainVideo(true);\n        this.controller?.setMainComponent();\n    }\n    async onLeave(): Promise<void> {\n        console.log('[InfoAndCreditsScreen] Leaving info and credits screen');\n        if (this.controller) {\n            await this.controller.hideSidebarButtons();\n        }\n    }\n    async onStack(): Promise<void> {\n        await this.onLeave();\n    }\n    onUnstack(): void {\n        this.onEnter();\n    }\n    update(deltaTime: number): void {\n    }\n    destroy(): void {\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/ladder/LadderScreen.ts",
    "content": "import { CancellationTokenSource, CancellationToken } from '@puzzl/core/lib/async/cancellation';\nimport { Task } from '@puzzl/core/lib/async/Task';\nimport { HtmlView } from '../../../jsx/HtmlView';\nimport { jsx } from '../../../jsx/jsx';\nimport { LADDER_REFRESH_INTERVAL } from '../../../../network/ladder/wladderConfig';\nimport { WLadderService } from '../../../../network/ladder/WLadderService';\nimport { CompositeDisposable } from '../../../../util/disposable/CompositeDisposable';\nimport { MainMenuScreen } from '../MainMenuScreen';\nimport { Ladder } from './component/Ladder';\nimport { HttpRequest } from '../../../../network/HttpRequest';\ninterface LadderScreenParams {\n}\nexport class LadderScreen extends MainMenuScreen {\n    public title: string;\n    private strings: any;\n    private wladderService: WLadderService;\n    private jsxRenderer: any;\n    private disposables = new CompositeDisposable();\n    private ladderComponent?: any;\n    private refreshTask?: Task<void>;\n    constructor(strings: any, wladderService: WLadderService, jsxRenderer: any) {\n        super();\n        this.strings = strings;\n        this.wladderService = wladderService;\n        this.jsxRenderer = jsxRenderer;\n        this.title = this.strings.get(\"GUI:Ladder\") || \"Ladder\";\n    }\n    private initView() {\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            innerRef: (ref: any) => (this.ladderComponent = ref),\n            component: Ladder,\n            props: {\n                strings: this.strings,\n                wladderService: this.wladderService,\n                onError: (error: any) => this.handleError(error),\n            },\n        }));\n        this.controller.setMainComponent(component);\n        this.refreshSidebarButtons();\n        this.controller.showSidebarButtons();\n    }\n    private refreshSidebarButtons() {\n        const buttons = [\n            {\n                label: this.strings.get(\"GUI:Refresh\"),\n                tooltip: this.strings.get(\"STT:RefreshLadder\"),\n                onClick: () => this.refreshLadder(),\n            },\n            {\n                label: this.strings.get(\"GUI:Back\"),\n                tooltip: this.strings.get(\"STT:Back\"),\n                isBottom: true,\n                onClick: () => this.goBack(),\n            },\n        ];\n        this.controller.setSidebarButtons(buttons);\n    }\n    private refreshLadder() {\n        this.ladderComponent?.refresh();\n    }\n    private goBack() {\n        this.controller?.goBack();\n    }\n    private handleError(error: any) {\n        console.error('Ladder error:', error);\n        let message = this.strings.get(\"ERR:LadderError\");\n        if (error instanceof HttpRequest.NetworkError) {\n            message = this.strings.get(\"ERR:NetworkError\");\n        }\n        else if (error instanceof HttpRequest.TimeoutError) {\n            message = this.strings.get(\"ERR:TimeoutError\");\n        }\n        console.error('Ladder error:', message, error);\n    }\n    private startPeriodicRefresh(cancellationToken: CancellationToken) {\n        this.refreshTask = new Task(async (token) => {\n            while (!token.isCancelled()) {\n                await new Promise(resolve => setTimeout(resolve, LADDER_REFRESH_INTERVAL));\n                if (!token.isCancelled()) {\n                    this.refreshLadder();\n                }\n            }\n        });\n        this.refreshTask.start().catch(error => {\n            console.error('Periodic refresh error:', error);\n        });\n    }\n    async onEnter(params?: LadderScreenParams): Promise<void> {\n        console.log('[LadderScreen] Entering ladder screen');\n        this.controller.toggleMainVideo(false);\n        const tokenSource = new CancellationTokenSource();\n        this.disposables.add(() => tokenSource.cancel());\n        if (!this.wladderService.getUrl()) {\n            this.handleError(new Error('Ladder service not available'));\n            return;\n        }\n        this.initView();\n        this.startPeriodicRefresh(tokenSource.token);\n    }\n    async onLeave(): Promise<void> {\n        console.log('[LadderScreen] Leaving ladder screen');\n        this.disposables.dispose();\n        if (this.refreshTask) {\n            this.refreshTask.cancel();\n            this.refreshTask = undefined;\n        }\n        await this.controller.hideSidebarButtons();\n        this.ladderComponent = undefined;\n    }\n    onStack(): void {\n        if (this.refreshTask) {\n            this.refreshTask.cancel();\n        }\n    }\n    onUnstack(): void {\n        const tokenSource = new CancellationTokenSource();\n        this.disposables.add(() => tokenSource.cancel());\n        this.startPeriodicRefresh(tokenSource.token);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/ladder/component/Ladder.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport classNames from 'classnames';\nimport { RankIndicator } from '../../lobby/component/RankIndicator';\nimport { Select } from '../../../../component/Select';\nimport { Option } from '../../../../component/Option';\nimport { SEARCH_LIMIT } from '../../../../../network/ladder/wladderConfig';\nimport { List } from '../../../../component/List';\nimport { WLadderService } from '../../../../../network/ladder/WLadderService';\ninterface PlayerProfile {\n    name: string;\n    rank: number;\n    points: number;\n    wins: number;\n    losses: number;\n    disconnects: number;\n    lastGameDate?: string;\n}\ninterface LadderProps {\n    strings: any;\n    wladderService: WLadderService;\n    onError?: (error: any) => void;\n}\nfunction formatSeasonName(season: number, strings: any): string {\n    if (season === WLadderService.CURRENT_SEASON) {\n        return strings.get(\"GUI:LadderCurrent\");\n    }\n    if (season === WLadderService.PREV_SEASON) {\n        return strings.get(\"GUI:LadderPrev\");\n    }\n    return strings.get(\"GUI:LadderSeason\", season);\n}\nfunction formatDate(dateString: string): string {\n    return new Date(dateString).toLocaleDateString(undefined, { dateStyle: \"medium\" });\n}\nfunction formatTime(dateString: string): string {\n    return new Date(dateString).toLocaleTimeString(undefined, { timeStyle: \"short\" });\n}\nexport const Ladder: React.FC<LadderProps> = ({ strings, wladderService, onError }) => {\n    const [selectedSeason, setSelectedSeason] = useState<number>(WLadderService.CURRENT_SEASON);\n    const [searchQuery, setSearchQuery] = useState<string>('');\n    const [players, setPlayers] = useState<PlayerProfile[]>([]);\n    const [loading, setLoading] = useState<boolean>(false);\n    const [availableSeasons, setAvailableSeasons] = useState<number[]>([]);\n    useEffect(() => {\n        loadAvailableSeasons();\n    }, []);\n    useEffect(() => {\n        if (selectedSeason !== undefined) {\n            loadLadderData();\n        }\n    }, [selectedSeason]);\n    const loadAvailableSeasons = async () => {\n        try {\n            const seasons = await wladderService.getAvailableSeasons();\n            setAvailableSeasons(seasons);\n        }\n        catch (error) {\n            console.error('Failed to load seasons:', error);\n            onError?.(error);\n        }\n    };\n    const loadLadderData = async () => {\n        setLoading(true);\n        try {\n            let profiles: PlayerProfile[];\n            if (searchQuery.trim()) {\n                const searchTerms = searchQuery.trim().split(/\\s+/).slice(0, SEARCH_LIMIT);\n                profiles = await wladderService.listSearch(searchTerms, selectedSeason);\n            }\n            else {\n                profiles = await wladderService.getTopPlayers(selectedSeason, 100);\n            }\n            setPlayers(profiles);\n        }\n        catch (error) {\n            console.error('Failed to load ladder data:', error);\n            onError?.(error);\n            setPlayers([]);\n        }\n        finally {\n            setLoading(false);\n        }\n    };\n    const handleSearch = () => {\n        loadLadderData();\n    };\n    const handleSearchKeyPress = (e: React.KeyboardEvent) => {\n        if (e.key === 'Enter') {\n            handleSearch();\n        }\n    };\n    return (<div className=\"ladder-wrapper\">\n      <div className=\"ladder-controls\">\n        <div className=\"season-select\">\n          <label>{strings.get(\"GUI:Season\")}:</label>\n          <Select value={selectedSeason} onChange={(value) => setSelectedSeason(Number(value))}>\n            {availableSeasons.map(season => (<Option key={season} value={season}>\n                {formatSeasonName(season, strings)}\n              </Option>))}\n          </Select>\n        </div>\n        \n        <div className=\"search-controls\">\n          <input type=\"text\" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} onKeyPress={handleSearchKeyPress} placeholder={strings.get(\"GUI:SearchPlayers\")} maxLength={200}/>\n          <button onClick={handleSearch} disabled={loading}>\n            {strings.get(\"GUI:Search\")}\n          </button>\n        </div>\n      </div>\n\n      <div className=\"ladder-content\">\n        {loading ? (<div className=\"loading\">{strings.get(\"GUI:Loading\")}</div>) : (<List className=\"ladder-list\">\n            <div className=\"ladder-header\">\n              <span className=\"rank-col\">{strings.get(\"GUI:Rank\")}</span>\n              <span className=\"name-col\">{strings.get(\"GUI:Name\")}</span>\n              <span className=\"points-col\">{strings.get(\"GUI:Points\")}</span>\n              <span className=\"wins-col\">{strings.get(\"GUI:Wins\")}</span>\n              <span className=\"losses-col\">{strings.get(\"GUI:Losses\")}</span>\n              <span className=\"disconnects-col\">{strings.get(\"GUI:Disconnects\")}</span>\n              <span className=\"last-game-col\">{strings.get(\"GUI:LastGame\")}</span>\n            </div>\n            \n            {players.map((player, index) => (<div key={player.name} className=\"ladder-row\">\n                <span className=\"rank-col\">\n                  <RankIndicator playerProfile={player} strings={strings}/>\n                  {player.rank}\n                </span>\n                <span className=\"name-col\">{player.name}</span>\n                <span className=\"points-col\">{player.points}</span>\n                <span className=\"wins-col\">{player.wins}</span>\n                <span className=\"losses-col\">{player.losses}</span>\n                <span className=\"disconnects-col\">{player.disconnects}</span>\n                <span className=\"last-game-col\">\n                  {player.lastGameDate ? (<span title={formatTime(player.lastGameDate)}>\n                      {formatDate(player.lastGameDate)}\n                    </span>) : (strings.get(\"GUI:Never\"))}\n                </span>\n              </div>))}\n            \n            {players.length === 0 && !loading && (<div className=\"no-results\">\n                {searchQuery.trim()\n                    ? strings.get(\"GUI:NoPlayersFound\")\n                    : strings.get(\"GUI:NoLadderData\")}\n              </div>)}\n          </List>)}\n      </div>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/ladderRules/LadderRulesScreen.ts",
    "content": "import { jsx } from '../../../jsx/jsx';\nimport { HtmlView } from '../../../jsx/HtmlView';\nimport { MainMenuScreen } from '../MainMenuScreen';\nimport { Iframe } from '../component/Iframe';\nexport class LadderRulesScreen extends MainMenuScreen {\n    public title: string;\n    private strings: any;\n    private jsxRenderer: any;\n    private rulesUrl: string;\n    constructor(strings: any, jsxRenderer: any, rulesUrl: string) {\n        super();\n        this.strings = strings;\n        this.jsxRenderer = jsxRenderer;\n        this.rulesUrl = rulesUrl;\n        this.title = this.strings.get(\"GUI:Rules\");\n    }\n    onEnter(): void {\n        this.controller.setSidebarButtons([\n            {\n                label: this.strings.get(\"GUI:Back\"),\n                isBottom: true,\n                onClick: () => {\n                    this.controller?.leaveCurrentScreen();\n                },\n            },\n        ]);\n        this.controller.showSidebarButtons();\n        this.controller.toggleMainVideo(false);\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            component: Iframe,\n            props: {\n                src: this.rulesUrl,\n                className: \"ladder-rules-iframe\",\n            },\n        }));\n        this.controller.setMainComponent(component);\n    }\n    async onLeave(): Promise<void> {\n        await this.controller.hideSidebarButtons();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lan/LanRecentPlay.ts",
    "content": "export interface LanRecentPlayRecord {\n    gameId: string;\n    roomId: string;\n    role: 'host' | 'guest';\n    modeLabel: string;\n    mapTitle: string;\n    mapOfficial: boolean;\n    memberNames: string[];\n    memberCount: number;\n    timestamp: number;\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lan/LanSetupScreen.ts",
    "content": "import { MainMenuScreen } from '@/gui/screen/mainMenu/MainMenuScreen';\nimport { HtmlView } from '@/gui/jsx/HtmlView';\nimport { jsx } from '@/gui/jsx/jsx';\nimport { LanSetup } from '@/gui/screen/mainMenu/lan/component/LanSetup';\nimport { MusicType } from '@/engine/sound/Music';\nimport { LanMatchSession } from '@/network/lan/LanMatchSession';\nimport { LanMeshSession } from '@/network/lan/LanMeshSession';\nimport { ChatHistory } from '@/gui/chat/ChatHistory';\nimport { LanRoomSession } from '@/network/lan/LanRoomSession';\nimport { PregameController, PregameMapSelectionResult } from '@/gui/screen/mainMenu/lobby/PregameController';\nimport { MainMenuScreenType, ScreenType } from '@/gui/screen/ScreenType';\nimport { LobbyType } from '@/gui/screen/mainMenu/lobby/component/viewmodel/lobby';\nimport { MapPreviewRenderer } from '@/gui/screen/mainMenu/lobby/MapPreviewRenderer';\nimport { MapFile } from '@/data/MapFile';\nimport { MainMenuRoute } from '@/gui/screen/mainMenu/MainMenuRoute';\nimport { StorageKey } from '@/LocalPrefs';\nimport { uint8ArrayToBase64String } from '@/util/string';\nimport { LanRecentPlayRecord } from '@/gui/screen/mainMenu/lan/LanRecentPlay';\nimport { SlotType as NetSlotType } from '@/network/gameopt/SlotInfo';\nimport { OBS_COUNTRY_ID } from '@/game/gameopts/constants';\n\ninterface RootController {\n    goToScreen(screenType: number, params?: any): void;\n}\n\ninterface Rules {\n    getMultiplayerCountries(): any[];\n    getMultiplayerColors(): Map<number, any>;\n    mpDialogSettings: any;\n    general?: any;\n}\n\ninterface GameMode {\n    id: number;\n    label: string;\n    mpDialogSettings: any;\n}\n\ninterface GameModes {\n    getAll(): GameMode[];\n    getById(id: number): GameMode;\n}\n\ninterface MapListEntry {\n    fileName: string;\n    maxSlots: number;\n    getFullMapTitle(strings: any): string;\n}\n\ninterface MapList {\n    getAll(): MapListEntry[];\n    getByName(name: string): MapListEntry;\n    addFromMapFile(file: any): void;\n}\n\ninterface MapFileLoader {\n    load(mapName: string): Promise<any>;\n}\n\ninterface LocalPrefs {\n    getItem(key: string): string | undefined;\n    setItem(key: string, value: string): void;\n    removeItem(key: string): void;\n}\n\ninterface MessageBoxApi {\n    show(message: string, buttonText?: string, onClose?: () => void): void;\n}\n\ninterface MapDirectory {\n    containsEntry(entryName: string): Promise<boolean>;\n    writeFile(file: any): Promise<void>;\n}\n\nconst MAX_RECENT_LAN_PLAYS = 8;\n\nexport class LanSetupScreen extends MainMenuScreen {\n    declare public title: string;\n    declare public musicType: MusicType;\n\n    private form?: any;\n    private resetNonce = 0;\n    private inviteNonce = 0;\n    private joinNonce = 0;\n    private previewRequestId = 0;\n\n    private readonly meshSession = new LanMeshSession();\n    private readonly chatHistory = new ChatHistory();\n    private readonly roomSession: LanRoomSession;\n    private pregameController: PregameController;\n    private activeMatchSession?: LanMatchSession;\n\n    constructor(\n        private readonly rootController: RootController,\n        private readonly strings: any,\n        private readonly jsxRenderer: any,\n        private readonly rules: Rules,\n        private readonly mapFileLoader: MapFileLoader,\n        private readonly mapList: MapList,\n        private readonly gameModes: GameModes,\n        private readonly localPrefs: LocalPrefs,\n        private readonly messageBoxApi: MessageBoxApi,\n        private readonly mapDir?: MapDirectory\n    ) {\n        super();\n        this.title = '';\n        this.musicType = MusicType.Intro;\n        const savedLanPlayerName = this.localPrefs.getItem(StorageKey.LanPlayerName)?.trim();\n        if (savedLanPlayerName) {\n            this.meshSession.updateSelfName(savedLanPlayerName);\n        }\n        this.pregameController = this.createPregameController();\n        this.roomSession = new LanRoomSession(this.meshSession, this.gameModes, this.mapFileLoader, this.mapDir, this.mapList);\n    }\n\n    onEnter(): void {\n        this.controller.toggleMainVideo(false);\n        this.initView();\n        this.subscribeRoomEvents();\n        this.refreshSidebarButtons();\n        this.refreshSidebarMpText();\n        void this.refreshSidebarPreview();\n        this.controller.showSidebarButtons();\n    }\n\n    async onLeave(): Promise<void> {\n        this.previewRequestId += 1;\n        this.roomSession.onSnapshotChange.unsubscribe(this.handleRoomSnapshot);\n        this.meshSession.onSnapshotChange.unsubscribe(this.handleMeshSnapshot);\n        this.roomSession.onLaunch.unsubscribe(this.handleLaunch);\n        await this.controller.hideSidebarButtons();\n        this.form = undefined;\n    }\n\n    async onStack(): Promise<void> {\n        await this.onLeave();\n    }\n\n    onUnstack(params?: PregameMapSelectionResult): void {\n        this.subscribeRoomEvents();\n        if (params) {\n            this.pregameController.applyMapSelection(params);\n            this.pregameController.updateSelfName(this.meshSession.getSelf().name);\n            this.roomSession.startHosting(this.createLanHostSnapshot());\n        }\n        this.refreshSidebarButtons();\n        this.refreshSidebarMpText();\n        void this.refreshSidebarPreview();\n        this.refreshView();\n        this.controller.showSidebarButtons();\n    }\n\n    private handleMeshSnapshot = () => {\n        this.refreshSidebarButtons();\n    };\n\n    private handleRoomSnapshot = () => {\n        this.refreshSidebarButtons();\n        this.refreshSidebarMpText();\n        void this.refreshSidebarPreview();\n    };\n\n    private handleLaunch = (descriptor: any) => {\n        this.activeMatchSession?.dispose();\n        this.activeMatchSession = new LanMatchSession(this.meshSession, descriptor);\n        this.recordRecentPlay(descriptor);\n        const currentCustomMap = this.roomSession.getResolvedCustomMapFile();\n        this.rootController.goToScreen(ScreenType.Game, {\n            create: true,\n            lanLaunch: descriptor,\n            lanMatchSession: this.activeMatchSession,\n            lanMapDataBase64: currentCustomMap ? uint8ArrayToBase64String(currentCustomMap.getBytes()) : undefined,\n            returnTo: new MainMenuRoute(MainMenuScreenType.LanSetup, {}),\n        });\n    };\n\n    private subscribeRoomEvents(): void {\n        this.roomSession.onSnapshotChange.unsubscribe(this.handleRoomSnapshot);\n        this.meshSession.onSnapshotChange.unsubscribe(this.handleMeshSnapshot);\n        this.roomSession.onLaunch.unsubscribe(this.handleLaunch);\n        this.roomSession.onSnapshotChange.subscribe(this.handleRoomSnapshot);\n        this.meshSession.onSnapshotChange.subscribe(this.handleMeshSnapshot);\n        this.roomSession.onLaunch.subscribe(this.handleLaunch);\n    }\n\n    private createPregameController(): PregameController {\n        return new PregameController(\n            this.strings,\n            this.rules,\n            this.mapFileLoader,\n            this.mapList,\n            this.gameModes,\n            this.localPrefs,\n            this.meshSession.getSelf().name\n        );\n    }\n\n    private initView(): void {\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            innerRef: (ref: any) => (this.form = ref),\n            component: LanSetup,\n            props: this.buildComponentProps(),\n        }));\n        this.controller.setMainComponent(component);\n    }\n\n    private refreshView(): void {\n        if (!this.form) {\n            this.initView();\n            return;\n        }\n        this.form.applyOptions((options: any) => {\n            Object.assign(options, this.buildComponentProps());\n        });\n    }\n\n    private buildComponentProps(): any {\n        return {\n            strings: this.strings,\n            meshSession: this.meshSession,\n            roomSession: this.roomSession,\n            chatHistory: this.chatHistory,\n            pregameController: this.pregameController,\n            resetNonce: this.resetNonce,\n            inviteNonce: this.inviteNonce,\n            joinNonce: this.joinNonce,\n            recentSessions: this.getRecentPlays(),\n            onStartGame: async () => {\n                await this.startLanGame();\n            },\n            onLeaveRoom: async () => {\n                await this.handleLeaveRoom();\n            },\n            onChangeMap: async () => {\n                await this.handleChangeMap();\n            },\n            onToggleReady: async () => {\n                const selfMember = this.roomSession.getSnapshot().members.find((member) => member.isSelf);\n                if (!selfMember) {\n                    return;\n                }\n                await this.roomSession.setReady(!selfMember.ready);\n            },\n            onHostPregameChanged: () => {\n                this.roomSession.applyHostPregameSnapshot(this.pregameController.getSnapshot());\n                this.refreshSidebarMpText();\n                void this.refreshSidebarPreview();\n            },\n            onCommitName: (name: string) => {\n                this.persistLanPlayerName(name);\n            },\n        };\n    }\n\n    private async handleCreateRoom(): Promise<void> {\n        if (!this.pregameController.isInitialized()) {\n            await this.pregameController.initialize();\n        }\n        this.pregameController.updateSelfName(this.meshSession.getSelf().name);\n        await this.controller.pushScreen(MainMenuScreenType.MapSelection, {\n            lobbyType: LobbyType.MultiplayerHost,\n            gameOpts: this.pregameController.getGameOpts(),\n            usedSlots: () => this.pregameController.getUsedSlots(),\n        });\n    }\n\n    private async handleChangeMap(): Promise<void> {\n        if (!this.roomSession.getSnapshot().isHost || !this.roomSession.getSnapshot().roomState) {\n            return;\n        }\n        await this.controller.pushScreen(MainMenuScreenType.MapSelection, {\n            lobbyType: LobbyType.MultiplayerHost,\n            gameOpts: this.pregameController.getGameOpts(),\n            usedSlots: () => this.pregameController.getUsedSlots(),\n        });\n    }\n\n    private async handleLeaveRoom(): Promise<void> {\n        this.roomSession.leaveRoom();\n        if (this.meshSession.getSnapshot().isInRoom) {\n            this.meshSession.leaveRoom();\n        }\n        else {\n            this.meshSession.reset();\n        }\n        this.chatHistory.reset();\n        this.pregameController = this.createPregameController();\n        this.resetNonce += 1;\n        this.refreshSidebarButtons();\n        this.refreshSidebarMpText();\n        this.controller.setSidebarPreview();\n        this.refreshView();\n    }\n\n    private createLanHostSnapshot(): any {\n        const snapshot = this.pregameController.getSnapshot();\n        const visibleSlots = snapshot.gameOpts.humanPlayers[0]?.countryId === OBS_COUNTRY_ID\n            ? snapshot.gameOpts.maxSlots + 1\n            : snapshot.gameOpts.maxSlots;\n        for (let slotIndex = 1; slotIndex < visibleSlots; slotIndex += 1) {\n            if (snapshot.slotsInfo[slotIndex]?.type === NetSlotType.Player) {\n                continue;\n            }\n            snapshot.slotsInfo[slotIndex] = { type: NetSlotType.Open };\n            snapshot.gameOpts.aiPlayers[slotIndex] = undefined;\n        }\n        return snapshot;\n    }\n\n    private openInviteDialog(): void {\n        const roomSnapshot = this.roomSession.getSnapshot();\n        if (!roomSnapshot.canInvite) {\n            this.messageBoxApi.show('当前没有空闲玩家槽位，请先打开一个空位后再邀请。');\n            return;\n        }\n        this.inviteNonce += 1;\n        this.form?.applyOptions?.((options: any) => {\n            options.inviteNonce = this.inviteNonce;\n        });\n    }\n\n    private openJoinDialog(): void {\n        this.joinNonce += 1;\n        this.form?.applyOptions?.((options: any) => {\n            options.joinNonce = this.joinNonce;\n        });\n    }\n\n    private async startLanGame(): Promise<void> {\n        const roomSnapshot = this.roomSession.getSnapshot();\n        if (!roomSnapshot.isHost) {\n            return;\n        }\n        if (!roomSnapshot.canStart) {\n            this.messageBoxApi.show('当前还有成员未完成连接或地图同步。');\n            return;\n        }\n        this.roomSession.startGame({\n            screenType: MainMenuScreenType.LanSetup,\n            params: {},\n        });\n    }\n\n    private refreshSidebarButtons(): void {\n        const meshSnapshot = this.meshSession.getSnapshot();\n        const roomSnapshot = this.roomSession.getSnapshot();\n        const inWaitingRoom = roomSnapshot.isRoomActive || meshSnapshot.isInRoom;\n\n        if (!inWaitingRoom) {\n            this.controller.setSidebarButtons([\n                {\n                    label: '创建房间',\n                    tooltip: '先选择模式和地图，再进入等待页',\n                    onClick: () => {\n                        void this.handleCreateRoom();\n                    },\n                },\n                {\n                    label: '加入房间',\n                    tooltip: '扫码或粘贴二维码内容加入现有房间',\n                    onClick: () => this.openJoinDialog(),\n                },\n                {\n                    label: '返回',\n                    tooltip: '返回主菜单',\n                    isBottom: true,\n                    onClick: () => this.controller.popScreen(),\n                },\n            ]);\n            return;\n        }\n\n        const selfMember = roomSnapshot.members.find((member) => member.isSelf);\n        const buttons: any[] = [];\n\n        if (meshSnapshot.isInRoom) {\n            buttons.push({\n                label: '邀请玩家',\n                tooltip: roomSnapshot.canInvite\n                    ? '打开二维码邀请弹窗'\n                    : '当前没有空闲玩家槽位',\n                disabled: !roomSnapshot.canInvite,\n                onClick: () => this.openInviteDialog(),\n            });\n        }\n\n        buttons.push({\n            label: '开始游戏',\n            tooltip: roomSnapshot.isHost\n                ? roomSnapshot.canStart\n                    ? '向所有成员广播开局描述'\n                    : '等待连接和地图同步完成'\n                : '只有当前房主可以开始游戏',\n            disabled: !roomSnapshot.isHost || !roomSnapshot.canStart,\n            onClick: () => {\n                void this.startLanGame();\n            },\n        });\n\n        if (roomSnapshot.isRoomActive && roomSnapshot.isHost) {\n            buttons.push({\n                label: '更换地图',\n                tooltip: '重新选择模式和地图',\n                onClick: () => {\n                    void this.handleChangeMap();\n                },\n            });\n        }\n        else if (roomSnapshot.isRoomActive && selfMember) {\n            buttons.push({\n                label: selfMember.ready ? '取消准备' : '准备',\n                tooltip: '切换自己的等待状态',\n                onClick: () => {\n                    void this.roomSession.setReady(!selfMember.ready);\n                },\n            });\n        }\n\n        buttons.push({\n            label: '离开房间',\n            tooltip: '离开当前局域网房间并回到入口页',\n            isBottom: true,\n            onClick: () => {\n                void this.handleLeaveRoom();\n            },\n        });\n\n        this.controller.setSidebarButtons(buttons, true);\n    }\n\n    private refreshSidebarMpText(): void {\n        const roomSnapshot = this.roomSession.getSnapshot();\n        if (roomSnapshot.roomState) {\n            const gameOpts = roomSnapshot.roomState.gameOpts;\n            this.controller.setSidebarMpContent({\n                text: this.strings.get(this.gameModes.getById(gameOpts.gameMode).label) + '\\n\\n' + gameOpts.mapTitle,\n                icon: gameOpts.mapOfficial ? 'gt18.pcx' : 'settings.png',\n                tooltip: gameOpts.mapOfficial ? '当前房间使用官方地图' : '当前房间使用自定义地图',\n            });\n            return;\n        }\n        this.controller.setSidebarMpContent({\n            text: '',\n        });\n    }\n\n    private async refreshSidebarPreview(): Promise<void> {\n        const roomSnapshot = this.roomSession.getSnapshot();\n        const roomState = roomSnapshot.roomState;\n        if (!roomState) {\n            this.controller.toggleSidebarPreview(false);\n            this.controller.setSidebarPreview();\n            return;\n        }\n\n        const requestId = ++this.previewRequestId;\n        try {\n            let mapFile = this.roomSession.getResolvedCustomMapFile() ?? this.pregameController.getCurrentMapFile();\n            if (!mapFile) {\n                mapFile = await this.mapFileLoader.load(roomState.gameOpts.mapName);\n            }\n            if (requestId !== this.previewRequestId) {\n                return;\n            }\n            const preview = new MapPreviewRenderer(this.strings).render(\n                new MapFile(mapFile),\n                roomSnapshot.isHost ? LobbyType.MultiplayerHost : LobbyType.MultiplayerGuest,\n                this.controller.getSidebarPreviewSize()\n            );\n            this.controller.toggleSidebarPreview(true);\n            this.controller.setSidebarPreview(preview);\n        }\n        catch (error) {\n            if (requestId !== this.previewRequestId) {\n                return;\n            }\n            console.warn('[LanSetupScreen] Failed to refresh sidebar preview', error);\n            this.controller.setSidebarPreview();\n        }\n    }\n\n    private persistLanPlayerName(name: string): void {\n        const trimmed = name.trim();\n        if (!trimmed) {\n            this.localPrefs.removeItem(StorageKey.LanPlayerName);\n            return;\n        }\n        this.localPrefs.setItem(StorageKey.LanPlayerName, trimmed.slice(0, 24));\n    }\n\n    private getRecentPlays(): LanRecentPlayRecord[] {\n        const raw = this.localPrefs.getItem(StorageKey.LanRecentPlays);\n        if (!raw) {\n            return [];\n        }\n        try {\n            const parsed = JSON.parse(raw);\n            if (!Array.isArray(parsed)) {\n                return [];\n            }\n            return parsed\n                .filter((entry): entry is LanRecentPlayRecord => Boolean(\n                    entry &&\n                    typeof entry.gameId === 'string' &&\n                    typeof entry.roomId === 'string' &&\n                    (entry.role === 'host' || entry.role === 'guest') &&\n                    typeof entry.modeLabel === 'string' &&\n                    typeof entry.mapTitle === 'string' &&\n                    typeof entry.timestamp === 'number' &&\n                    Array.isArray(entry.memberNames)\n                ))\n                .sort((left, right) => right.timestamp - left.timestamp)\n                .slice(0, MAX_RECENT_LAN_PLAYS);\n        }\n        catch (error) {\n            console.warn('[LanSetupScreen] Failed to read recent LAN plays', error);\n            return [];\n        }\n    }\n\n    private saveRecentPlays(records: LanRecentPlayRecord[]): void {\n        this.localPrefs.setItem(\n            StorageKey.LanRecentPlays,\n            JSON.stringify(records.slice(0, MAX_RECENT_LAN_PLAYS))\n        );\n    }\n\n    private recordRecentPlay(descriptor: any): void {\n        const roomSnapshot = this.roomSession.getSnapshot();\n        const recentRecord: LanRecentPlayRecord = {\n            gameId: descriptor.gameId,\n            roomId: descriptor.roomId,\n            role: descriptor.hostPeerId === descriptor.localPeerId ? 'host' : 'guest',\n            modeLabel: this.strings.get(this.gameModes.getById(descriptor.gameOpts.gameMode).label),\n            mapTitle: descriptor.gameOpts.mapTitle,\n            mapOfficial: descriptor.gameOpts.mapOfficial,\n            memberNames: roomSnapshot.members.map((member) => member.name),\n            memberCount: roomSnapshot.members.length || descriptor.humanAssignments.length,\n            timestamp: descriptor.timestamp,\n        };\n        const nextRecentPlays = [\n            recentRecord,\n            ...this.getRecentPlays().filter((entry) => entry.gameId !== recentRecord.gameId),\n        ];\n        this.saveRecentPlays(nextRecentPlays);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lan/component/LanSetup.tsx",
    "content": "import React, { startTransition, useEffect, useMemo, useRef, useState } from 'react';\nimport { ChatHistory } from '@/gui/chat/ChatHistory';\nimport { List, ListItem } from '@/gui/component/List';\nimport { LobbyForm } from '@/gui/screen/mainMenu/lobby/component/LobbyForm';\nimport { LobbyType, PlayerStatus } from '@/gui/screen/mainMenu/lobby/component/viewmodel/lobby';\nimport { LanRecentPlayRecord } from '@/gui/screen/mainMenu/lan/LanRecentPlay';\nimport { RECIPIENT_ALL } from '@/network/gservConfig';\nimport { LanMeshSession, LanMeshSnapshot } from '@/network/lan/LanMeshSession';\nimport { LanRoomSession, LanRoomSnapshot } from '@/network/lan/LanRoomSession';\nimport { PregameController } from '@/gui/screen/mainMenu/lobby/PregameController';\nimport { QrCodeCard } from '@/gui/screen/mainMenu/lan/component/QrCodeCard';\nimport { QrScannerPanel } from '@/gui/screen/mainMenu/lan/component/QrScannerPanel';\n\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\n\ninterface UiChatMessage {\n    from?: string;\n    to?: {\n        type: number;\n        name: string;\n    };\n    text: string;\n    time?: Date;\n}\n\ninterface LanSetupProps {\n    strings: Strings;\n    meshSession: LanMeshSession;\n    roomSession: LanRoomSession;\n    chatHistory: ChatHistory;\n    pregameController: PregameController;\n    resetNonce?: number;\n    inviteNonce?: number;\n    joinNonce?: number;\n    recentSessions: LanRecentPlayRecord[];\n    onStartGame: () => Promise<void>;\n    onLeaveRoom: () => Promise<void>;\n    onChangeMap: () => Promise<void>;\n    onToggleReady: () => Promise<void>;\n    onHostPregameChanged: () => void;\n    onCommitName?: (name: string) => void;\n}\n\nconst MAX_MESSAGES = 180;\nfunction trimMessages(messages: UiChatMessage[]): UiChatMessage[] {\n    if (messages.length <= MAX_MESSAGES) {\n        return messages;\n    }\n    return messages.slice(messages.length - MAX_MESSAGES);\n}\n\nfunction createSystemMessage(text: string): UiChatMessage {\n    return { text };\n}\n\nfunction createChatMessage(from: string, text: string, timestamp: number): UiChatMessage {\n    return {\n        from,\n        to: {\n            type: 0,\n            name: RECIPIENT_ALL,\n        },\n        text,\n        time: new Date(timestamp),\n    };\n}\n\nfunction createInitialMessages(): UiChatMessage[] {\n    return [];\n}\n\nfunction shouldSurfaceSystemLog(text: string): boolean {\n    return /失败|错误|不支持|无法|超时|中断|断开|关闭|拒绝|异常/i.test(text);\n}\n\nfunction describeRoomTone(roomSnapshot: LanRoomSnapshot): 'good' | 'warn' | 'bad' {\n    if (roomSnapshot.canStart) {\n        return 'good';\n    }\n    if (roomSnapshot.isRoomActive || roomSnapshot.mesh.isInRoom) {\n        return 'warn';\n    }\n    return 'bad';\n}\n\nfunction describeCompactRoomState(roomSnapshot: LanRoomSnapshot): string {\n    if (!roomSnapshot.isRoomActive) {\n        return '等待房主同步';\n    }\n    if (roomSnapshot.canStart) {\n        return '连接完成';\n    }\n    if (roomSnapshot.roomState && !roomSnapshot.roomState.gameOpts.mapOfficial) {\n        return '等待地图同步';\n    }\n    return '等待成员互联';\n}\n\nfunction describeMemberRoleTone(member: LanRoomSnapshot['members'][number]): 'good' | 'warn' | 'bad' {\n    if (member.isHost || member.ready) {\n        return 'good';\n    }\n    return member.isConnected ? 'warn' : 'bad';\n}\n\nfunction describeCustomMapTransfer(roomSnapshot: LanRoomSnapshot): { text: string; tone: 'good' | 'warn' | 'bad' } | undefined {\n    if (!roomSnapshot.roomState || roomSnapshot.roomState.gameOpts.mapOfficial) {\n        return undefined;\n    }\n\n    const failedMember = roomSnapshot.members.find((member) => member.mapTransfer.status === 'error');\n    if (failedMember) {\n        return {\n            text: `地图同步失败: ${failedMember.name}`,\n            tone: 'bad',\n        };\n    }\n\n    const completedCount = roomSnapshot.members.filter((member) => member.mapTransfer.status === 'complete').length;\n    if (completedCount >= roomSnapshot.members.length && roomSnapshot.members.length > 0) {\n        return {\n            text: '地图同步完成',\n            tone: 'good',\n        };\n    }\n\n    return {\n        text: `地图同步 ${completedCount}/${roomSnapshot.members.length}`,\n        tone: 'warn',\n    };\n}\n\nfunction formatRecentTimestamp(timestamp: number): string {\n    const date = new Date(timestamp);\n    const month = `${date.getMonth() + 1}`.padStart(2, '0');\n    const day = `${date.getDate()}`.padStart(2, '0');\n    const hours = `${date.getHours()}`.padStart(2, '0');\n    const minutes = `${date.getMinutes()}`.padStart(2, '0');\n    return `${month}/${day} ${hours}:${minutes}`;\n}\n\nfunction describeRecentRole(role: LanRecentPlayRecord['role']): string {\n    return role === 'host' ? '房主' : '成员';\n}\n\nfunction formatMemberSummary(record: LanRecentPlayRecord): string {\n    if (!record.memberNames.length) {\n        return `${record.memberCount} 人房间`;\n    }\n    const visibleMembers = record.memberNames.slice(0, 3).join('、');\n    if (record.memberNames.length > 3) {\n        return `${visibleMembers} 等 ${record.memberCount} 人`;\n    }\n    return `${visibleMembers} · ${record.memberCount} 人`;\n}\n\nexport const LanSetup: React.FC<LanSetupProps> = ({\n    meshSession,\n    roomSession,\n    chatHistory,\n    pregameController,\n    resetNonce = 0,\n    inviteNonce = 0,\n    joinNonce = 0,\n    recentSessions,\n    onHostPregameChanged,\n    onCommitName,\n}) => {\n    const [meshSnapshot, setMeshSnapshot] = useState<LanMeshSnapshot>(meshSession.getSnapshot());\n    const [roomSnapshot, setRoomSnapshot] = useState<LanRoomSnapshot>(roomSession.getSnapshot());\n    const [messages, setMessages] = useState<UiChatMessage[]>(() => {\n        const existingMessages = chatHistory.getAll() as UiChatMessage[];\n        if (existingMessages.length > 0) {\n            return existingMessages;\n        }\n        const initialMessages = createInitialMessages();\n        initialMessages.forEach((message) => chatHistory.addChatMessage(message));\n        return initialMessages;\n    });\n    const [nameInput, setNameInput] = useState(meshSession.getSnapshot().self.name);\n    const [manualPayloadText, setManualPayloadText] = useState('');\n    const [manualResponsePayloadText, setManualResponsePayloadText] = useState('');\n    const [busy, setBusy] = useState(false);\n    const [clipboardHint, setClipboardHint] = useState<string>();\n    const [joinDialogOpen, setJoinDialogOpen] = useState(false);\n    const [inviteDialogOpen, setInviteDialogOpen] = useState(false);\n    const [showAdvancedJoin, setShowAdvancedJoin] = useState(false);\n    const [showAdvancedInvite, setShowAdvancedInvite] = useState(false);\n    const lastResetNonceRef = useRef(resetNonce);\n    const lastInviteNonceRef = useRef(inviteNonce);\n    const lastJoinNonceRef = useRef(joinNonce);\n    const appMessageLogRef = useRef<any[]>([]);\n\n    const supported = typeof RTCPeerConnection !== 'undefined';\n\n    const appendMessage = (message: UiChatMessage) => {\n        chatHistory.addChatMessage(message);\n        startTransition(() => {\n            setMessages((current) => trimMessages([...current, message]));\n        });\n    };\n\n    const replaceMessages = (nextMessages: UiChatMessage[]) => {\n        chatHistory.reset();\n        nextMessages.forEach((message) => chatHistory.addChatMessage(message));\n        startTransition(() => {\n            setMessages(nextMessages);\n        });\n    };\n\n    const appendSystemMessage = (text: string) => {\n        appendMessage(createSystemMessage(text));\n    };\n\n    useEffect(() => {\n        const handleMeshSnapshot = (nextSnapshot: LanMeshSnapshot) => {\n            setMeshSnapshot(nextSnapshot);\n            setNameInput((current) => (current === meshSnapshot.self.name ? nextSnapshot.self.name : current));\n        };\n        const handleRoomSnapshot = (nextSnapshot: LanRoomSnapshot) => {\n            setRoomSnapshot(nextSnapshot);\n        };\n        const handleMeshLog = (entry: { text: string }) => {\n            if (shouldSurfaceSystemLog(entry.text)) {\n                appendSystemMessage(entry.text);\n            }\n        };\n        const handleRoomLog = (entry: { text: string }) => {\n            if (shouldSurfaceSystemLog(entry.text)) {\n                appendSystemMessage(entry.text);\n            }\n        };\n        const handleChat = (entry: { from: { name: string }; text: string; timestamp: number }) => {\n            appendMessage(createChatMessage(entry.from.name, entry.text, entry.timestamp));\n        };\n        const handleAppMessage = (entry: { from: unknown; payload: unknown; timestamp: number }) => {\n            appMessageLogRef.current = [...appMessageLogRef.current.slice(-49), {\n                from: entry.from,\n                payload: entry.payload,\n                timestamp: entry.timestamp,\n            }];\n        };\n\n        meshSession.onSnapshotChange.subscribe(handleMeshSnapshot);\n        roomSession.onSnapshotChange.subscribe(handleRoomSnapshot);\n        meshSession.onLog.subscribe(handleMeshLog);\n        roomSession.onLog.subscribe(handleRoomLog);\n        meshSession.onChat.subscribe(handleChat);\n        meshSession.onAppMessage.subscribe(handleAppMessage);\n\n        return () => {\n            meshSession.onSnapshotChange.unsubscribe(handleMeshSnapshot);\n            roomSession.onSnapshotChange.unsubscribe(handleRoomSnapshot);\n            meshSession.onLog.unsubscribe(handleMeshLog);\n            roomSession.onLog.unsubscribe(handleRoomLog);\n            meshSession.onChat.unsubscribe(handleChat);\n            meshSession.onAppMessage.unsubscribe(handleAppMessage);\n        };\n    }, [chatHistory, meshSession, roomSession, meshSnapshot.self.name]);\n\n    useEffect(() => {\n        if (lastResetNonceRef.current === resetNonce) {\n            return;\n        }\n        lastResetNonceRef.current = resetNonce;\n        setManualPayloadText('');\n        setManualResponsePayloadText('');\n        setClipboardHint(undefined);\n        setJoinDialogOpen(false);\n        setInviteDialogOpen(false);\n        setShowAdvancedJoin(false);\n        setShowAdvancedInvite(false);\n        const nextSnapshot = meshSession.getSnapshot();\n        setMeshSnapshot(nextSnapshot);\n        setRoomSnapshot(roomSession.getSnapshot());\n        setNameInput(nextSnapshot.self.name);\n        replaceMessages(createInitialMessages());\n    }, [meshSession, resetNonce, roomSession]);\n\n    useEffect(() => {\n        if (lastInviteNonceRef.current === inviteNonce) {\n            return;\n        }\n        lastInviteNonceRef.current = inviteNonce;\n        setInviteDialogOpen(true);\n    }, [inviteNonce]);\n\n    useEffect(() => {\n        if (lastJoinNonceRef.current === joinNonce) {\n            return;\n        }\n        lastJoinNonceRef.current = joinNonce;\n        setJoinDialogOpen(true);\n    }, [joinNonce]);\n\n    useEffect(() => {\n        if (inviteDialogOpen && meshSnapshot.isInRoom) {\n            void handleCreateInvite();\n        }\n    }, [inviteDialogOpen, meshSnapshot.isInRoom]);\n\n    useEffect(() => {\n        if (!inviteDialogOpen || roomSnapshot.canInvite) {\n            return;\n        }\n        setInviteDialogOpen(false);\n        setShowAdvancedInvite(false);\n        setClipboardHint(undefined);\n    }, [inviteDialogOpen, roomSnapshot.canInvite]);\n\n    useEffect(() => {\n        if (roomSnapshot.isRoomActive && joinDialogOpen) {\n            setJoinDialogOpen(false);\n            setShowAdvancedJoin(false);\n        }\n    }, [joinDialogOpen, roomSnapshot.isRoomActive]);\n\n    useEffect(() => {\n        const debugRoot = ((window as any).__ra2debug ??= {});\n        debugRoot.lan = {\n            meshSnapshot,\n            roomSnapshot,\n        };\n        debugRoot.lanApi = {\n            sendAppMessage: (payload: unknown) => meshSession.broadcastAppMessage(payload),\n            getAppMessages: () => appMessageLogRef.current.slice(),\n        };\n    }, [meshSnapshot, roomSnapshot]);\n\n    const commitName = () => {\n        meshSession.updateSelfName(nameInput);\n        const nextSelf = meshSession.getSnapshot().self;\n        setMeshSnapshot(meshSession.getSnapshot());\n        setNameInput(nextSelf.name);\n        onCommitName?.(nextSelf.name);\n        if (roomSnapshot.isHost && roomSnapshot.roomState) {\n            pregameController.updateSelfName(nextSelf.name);\n            onHostPregameChanged();\n        }\n    };\n\n    const handleCreateInvite = async () => {\n        if (!supported) {\n            appendSystemMessage('当前浏览器不支持 WebRTC。');\n            return;\n        }\n        if (!roomSession.getSnapshot().canInvite) {\n            appendSystemMessage('当前没有空闲玩家槽位，请先打开一个空位后再邀请。');\n            return;\n        }\n        setBusy(true);\n        try {\n            commitName();\n            await meshSession.createRoomInvite();\n            setMeshSnapshot(meshSession.getSnapshot());\n            setClipboardHint(undefined);\n        }\n        catch (error) {\n            appendSystemMessage((error as Error).message);\n        }\n        finally {\n            setBusy(false);\n        }\n    };\n\n    const handleImportPayload = async (payloadText?: string) => {\n        if (!supported) {\n            appendSystemMessage('当前浏览器不支持 WebRTC。');\n            return;\n        }\n        const nextPayload = (payloadText ?? manualPayloadText).trim();\n        if (!nextPayload) {\n            appendSystemMessage('请先扫码，或者把二维码内容粘贴到文本框里。');\n            return;\n        }\n        setBusy(true);\n        try {\n            commitName();\n            await meshSession.importPayload(nextPayload);\n            setMeshSnapshot(meshSession.getSnapshot());\n            setManualPayloadText(nextPayload);\n            setClipboardHint(undefined);\n        }\n        catch (error) {\n            appendSystemMessage((error as Error).message);\n            throw error;\n        }\n        finally {\n            setBusy(false);\n        }\n    };\n\n    const handleCopyPayload = async () => {\n        if (!meshSnapshot.activeQrPayloadText) {\n            appendSystemMessage('当前没有可复制的二维码内容。');\n            return;\n        }\n        try {\n            await navigator.clipboard.writeText(meshSnapshot.activeQrPayloadText);\n            setClipboardHint('已复制到剪贴板');\n            appendSystemMessage('二维码内容已复制到剪贴板。');\n        }\n        catch {\n            setClipboardHint('复制失败，请手动复制');\n            appendSystemMessage('浏览器不允许写入剪贴板，请手动复制。');\n        }\n    };\n\n    const handlePastePayload = async () => {\n        try {\n            const text = await navigator.clipboard.readText();\n            setManualPayloadText(text);\n            appendSystemMessage('已从剪贴板读取二维码内容。');\n        }\n        catch {\n            appendSystemMessage('浏览器不允许读取剪贴板，请手动粘贴。');\n        }\n    };\n\n    const handleSendMessage = async ({ value }: { value: string }) => {\n        try {\n            await meshSession.sendChat(value);\n        }\n        catch (error) {\n            appendSystemMessage((error as Error).message);\n        }\n    };\n\n    const submitChatMessage = (message: any) => {\n        const value = typeof message === 'string' ? message : message?.value;\n        if (typeof value === 'string' && value.trim()) {\n            void handleSendMessage({ value });\n        }\n    };\n\n    const waitingMode = roomSnapshot.isRoomActive || meshSnapshot.isInRoom;\n    const selfAssignment = roomSnapshot.roomState?.humanAssignments.find((assignment) => assignment.peerId === meshSnapshot.self.id);\n    const activeSlotIndex = selfAssignment?.slotIndex ?? 0;\n    const selfMember = roomSnapshot.members.find((member) => member.isSelf);\n    const customMapTransfer = describeCustomMapTransfer(roomSnapshot);\n    const latestRecentSession = recentSessions[0];\n    const waitingStatusStrip = waitingMode ? (\n        <div className=\"lan-room-status-strip\">\n            <div className=\"lan-status-chip\">\n                房间号 <strong>{meshSnapshot.roomId ?? '--'}</strong>\n            </div>\n            <div className=\"lan-status-chip\">\n                成员 <strong data-lan-stat=\"members\">{roomSnapshot.members.length || meshSnapshot.members.length}</strong>\n                <span className=\"lan-status-divider\">/</span>\n                直连 <strong data-lan-stat=\"direct-peers\">{meshSnapshot.directPeerCount}</strong>\n            </div>\n            <div className={`lan-status-chip tone-${describeRoomTone(roomSnapshot)}`}>\n                {describeCompactRoomState(roomSnapshot)}\n            </div>\n            {selfMember ? (\n                <div className={`lan-status-chip tone-${describeMemberRoleTone(selfMember)}`}>\n                    {selfMember.isHost ? '你是房主' : selfMember.ready ? '已准备' : '未准备'}\n                </div>\n            ) : null}\n            {customMapTransfer ? (\n                <div className={`lan-status-chip tone-${customMapTransfer.tone}`}>\n                    {customMapTransfer.text}\n                </div>\n            ) : null}\n        </div>\n    ) : null;\n\n    const formProps = useMemo(() => {\n        if (!roomSnapshot.roomState) {\n            return undefined;\n        }\n\n        pregameController.hydrate({\n            gameOpts: roomSnapshot.roomState.gameOpts,\n            slotsInfo: roomSnapshot.roomState.slotsInfo,\n            currentMapFile: roomSession.getResolvedCustomMapFile(),\n        });\n\n        const baseProps = pregameController.createLobbyFormProps({\n            lobbyType: roomSnapshot.isHost ? LobbyType.MultiplayerHost : LobbyType.MultiplayerGuest,\n            activeSlotIndex,\n            messages,\n            localUsername: meshSnapshot.self.name,\n            channels: [RECIPIENT_ALL],\n            chatHistory: chatHistory as any,\n            onSendMessage: submitChatMessage,\n            onStateChange: roomSnapshot.isHost ? onHostPregameChanged : undefined,\n            decoratePlayerSlot: (playerSlot: any, _slotInfo: any, slotIndex: number) => {\n                const assignment = roomSnapshot.roomState?.humanAssignments.find((candidate) => candidate.slotIndex === slotIndex);\n                if (!assignment) {\n                    return;\n                }\n                const member = roomSnapshot.members.find((candidate) => candidate.peerId === assignment.peerId);\n                playerSlot.status = member?.isHost\n                    ? PlayerStatus.Host\n                    : member?.ready\n                        ? PlayerStatus.Ready\n                        : PlayerStatus.NotReady;\n            },\n        });\n\n        if (!roomSnapshot.isHost && selfAssignment) {\n            const requestOwnSlotConfig = (updater: (slot: any) => { countryId: number; colorId: number; startPos: number; teamId: number }) => {\n                const slot = baseProps.playerSlots[selfAssignment.slotIndex];\n                const next = updater(slot);\n                void roomSession.requestSlotConfig(selfAssignment.slotIndex, next);\n            };\n            baseProps.onCountrySelect = (country: string) => {\n                requestOwnSlotConfig((slot) => ({\n                    countryId: pregameController.getCountryIdByName(country),\n                    colorId: pregameController.getColorIdByName(slot.color),\n                    startPos: slot.startPos,\n                    teamId: slot.team,\n                }));\n            };\n            baseProps.onColorSelect = (color: string) => {\n                requestOwnSlotConfig((slot) => ({\n                    countryId: pregameController.getCountryIdByName(slot.country),\n                    colorId: pregameController.getColorIdByName(color),\n                    startPos: slot.startPos,\n                    teamId: slot.team,\n                }));\n            };\n            baseProps.onStartPosSelect = (startPos: number) => {\n                requestOwnSlotConfig((slot) => ({\n                    countryId: pregameController.getCountryIdByName(slot.country),\n                    colorId: pregameController.getColorIdByName(slot.color),\n                    startPos,\n                    teamId: slot.team,\n                }));\n            };\n            baseProps.onTeamSelect = (teamId: number) => {\n                requestOwnSlotConfig((slot) => ({\n                    countryId: pregameController.getCountryIdByName(slot.country),\n                    colorId: pregameController.getColorIdByName(slot.color),\n                    startPos: slot.startPos,\n                    teamId,\n                }));\n            };\n        }\n\n        return baseProps;\n    }, [activeSlotIndex, chatHistory, meshSnapshot.self.id, meshSnapshot.self.name, messages, onHostPregameChanged, pregameController, roomSession, roomSnapshot, selfAssignment]);\n\n    return (\n        <div className=\"lobby-form lan-setup-form lan-room-form\" data-lan-view={waitingMode ? 'waiting' : 'entry'}>\n            {!supported ? (\n                <div className=\"lan-panel\">\n                    <h3>环境不支持</h3>\n                    <p>当前浏览器没有可用的 WebRTC 实现，无法在这个页面里建立局域网连接。</p>\n                </div>\n            ) : !waitingMode ? (\n                <div className=\"lan-entry-layout\">\n                    <div className=\"lan-panel lan-entry-panel lan-entry-profile-panel\">\n                        <div className=\"lan-panel-header\">\n                            <h3>玩家信息</h3>\n                            <span>右侧菜单负责创建和加入，这里只保留你的局域网档案。</span>\n                        </div>\n                        <div className=\"lan-entry-profile-grid\">\n                            <div className=\"lan-entry-profile-editor\">\n                                <label className=\"lan-input-label\" htmlFor=\"lan-self-name\">\n                                    玩家名称\n                                </label>\n                                <input\n                                    id=\"lan-self-name\"\n                                    type=\"text\"\n                                    className=\"lan-text-input\"\n                                    maxLength={24}\n                                    value={nameInput}\n                                    data-lan-input=\"self-name\"\n                                    onChange={(event) => setNameInput(event.target.value)}\n                                    onBlur={commitName}\n                                    onKeyDown={(event) => {\n                                        if (event.key === 'Enter') {\n                                            commitName();\n                                        }\n                                    }}\n                                />\n                                <div className=\"lan-entry-field-hint\">\n                                    房间成员列表、聊天和开局后的玩家槽位都会使用这个名字。\n                                </div>\n                            </div>\n\n                            <div className=\"lan-entry-profile-stats\">\n                                <div className=\"lan-entry-stat\">\n                                    <span>当前身份</span>\n                                    <strong>{meshSnapshot.self.name}</strong>\n                                </div>\n                                <div className=\"lan-entry-stat\">\n                                    <span>浏览器支持</span>\n                                    <strong className={supported ? 'tone-good' : 'tone-bad'}>\n                                        {supported ? 'WebRTC 可用' : '不可用'}\n                                    </strong>\n                                </div>\n                                <div className=\"lan-entry-stat\">\n                                    <span>最近房间</span>\n                                    <strong>{latestRecentSession?.roomId ?? '--'}</strong>\n                                </div>\n                                <div className=\"lan-entry-stat\">\n                                    <span>最近模式</span>\n                                    <strong>{latestRecentSession?.modeLabel ?? '暂无记录'}</strong>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n\n                    <div className=\"lan-panel lan-entry-panel lan-entry-recent-panel\">\n                        <div className=\"lan-panel-header\">\n                            <h3>最近参与</h3>\n                            <span>{recentSessions.length ? `本机保留最近 ${recentSessions.length} 次开局记录。` : '完成一次开局后会自动记录在这里。'}</span>\n                        </div>\n                        {recentSessions.length ? (\n                            <List className=\"lan-entry-recent-list\">\n                                {recentSessions.map((record) => (\n                                    <ListItem className=\"lan-entry-recent-item\" key={record.gameId}>\n                                        <div className=\"lan-entry-recent-item-top\">\n                                            <strong>{record.mapTitle}</strong>\n                                            <span>{formatRecentTimestamp(record.timestamp)}</span>\n                                        </div>\n                                        <div className=\"lan-entry-recent-item-meta\">\n                                            <span className=\"lan-entry-recent-chip\">{describeRecentRole(record.role)}</span>\n                                            <span>{record.modeLabel}</span>\n                                            <span>房间 {record.roomId}</span>\n                                            <span>{record.mapOfficial ? '官方地图' : '自定义地图'}</span>\n                                        </div>\n                                        <div className=\"lan-entry-recent-item-members\">\n                                            {formatMemberSummary(record)}\n                                        </div>\n                                    </ListItem>\n                                ))}\n                            </List>\n                        ) : (\n                            <div className=\"lan-entry-empty-state\">\n                                右侧可以直接创建房间或加入房间。完成一次联机开局后，最近参与记录会显示在这里。\n                            </div>\n                        )}\n                    </div>\n                </div>\n            ) : (\n                <div className=\"lan-waiting-main\">\n                    {formProps ? (\n                        <div className=\"lan-room-form-shell lan-room-form-shell-compact\">\n                            <LobbyForm {...formProps} beforeChatContent={waitingStatusStrip} />\n                        </div>\n                    ) : (\n                        <div className=\"lan-panel lan-room-loading-panel lan-room-loading-panel-compact\">\n                            正在接收房间配置...\n                        </div>\n                    )}\n                </div>\n            )}\n\n            {inviteDialogOpen ? (\n                <div className=\"lan-dialog-overlay\" onClick={() => setInviteDialogOpen(false)}>\n                    <div className=\"lan-dialog\" onClick={(event) => event.stopPropagation()}>\n                        <div className=\"lan-dialog-header\">\n                            <h3>邀请其他玩家</h3>\n                            <button type=\"button\" className=\"lan-dialog-close\" onClick={() => setInviteDialogOpen(false)}>\n                                ×\n                            </button>\n                        </div>\n                            <div className=\"lan-dialog-body\">\n                                <div className=\"lan-dialog-grid\">\n                                    <div className=\"lan-panel\">\n                                        <div className=\"lan-panel-header\">\n                                            <h3>邀请二维码</h3>\n                                            <span>新玩家先扫这张码。</span>\n                                        </div>\n                                        <QrCodeCard\n                                            title={meshSnapshot.activeQrPayloadTitle ?? '邀请二维码'}\n                                            description={meshSnapshot.activeQrPayloadDescription ?? '等待生成二维码。'}\n                                            payloadText={meshSnapshot.activeQrPayloadText}\n                                        />\n                                        <textarea\n                                            className=\"lan-sdp-textarea\"\n                                            readOnly={true}\n                                            value={meshSnapshot.activeQrPayloadText}\n                                            data-lan-output=\"active-payload\"\n                                            placeholder=\"二维码原始内容。\"\n                                        />\n                                        <div className=\"lan-actions\">\n                                            <button\n                                                type=\"button\"\n                                                className=\"dialog-button\"\n                                                data-lan-action=\"create-or-invite\"\n                                                disabled={busy}\n                                                onClick={() => {\n                                                    void handleCreateInvite();\n                                                }}\n                                            >\n                                                重新生成邀请二维码\n                                            </button>\n                                            <button\n                                                type=\"button\"\n                                                className=\"dialog-button\"\n                                                disabled={!meshSnapshot.activeQrPayloadText}\n                                                data-lan-action=\"copy-payload\"\n                                                onClick={() => {\n                                                    void handleCopyPayload();\n                                                }}\n                                            >\n                                                复制二维码内容\n                                            </button>\n                                            {clipboardHint ? <span className=\"lan-hint\">{clipboardHint}</span> : null}\n                                        </div>\n                                    </div>\n\n                                    <div className=\"lan-panel\">\n                                        <div className=\"lan-panel-header\">\n                                            <h3>接收加入响应</h3>\n                                            <span>新玩家扫完邀请后，把响应码扫回这里。</span>\n                                        </div>\n                                        <QrScannerPanel\n                                            onDetected={async (payloadText) => {\n                                                await handleImportPayload(payloadText);\n                                            }}\n                                        />\n                                        <div className=\"lan-actions\">\n                                            <button\n                                                type=\"button\"\n                                                className=\"dialog-button\"\n                                                data-lan-action=\"toggle-invite-manual\"\n                                                onClick={() => setShowAdvancedInvite((current) => !current)}\n                                            >\n                                                {showAdvancedInvite ? '隐藏高级方式' : '显示高级方式'}\n                                            </button>\n                                        </div>\n                                        {showAdvancedInvite ? (\n                                            <>\n                                                <textarea\n                                                    className=\"lan-sdp-textarea\"\n                                                    value={manualResponsePayloadText}\n                                                    data-lan-input=\"invite-response-payload\"\n                                                    onChange={(event) => setManualResponsePayloadText(event.target.value)}\n                                                    placeholder=\"把加入响应二维码内容粘贴到这里，然后点击导入。\"\n                                                />\n                                                <div className=\"lan-actions\">\n                                                    <button\n                                                        type=\"button\"\n                                                        className=\"dialog-button\"\n                                                        data-lan-action=\"import-invite-response\"\n                                                        disabled={busy}\n                                                        onClick={() => {\n                                                            void handleImportPayload(manualResponsePayloadText).catch(() => undefined);\n                                                        }}\n                                                    >\n                                                        导入加入响应\n                                                    </button>\n                                                    <button\n                                                        type=\"button\"\n                                                        className=\"dialog-button\"\n                                                        disabled={busy}\n                                                        onClick={async () => {\n                                                            try {\n                                                                const text = await navigator.clipboard.readText();\n                                                                setManualResponsePayloadText(text);\n                                                                appendSystemMessage('已从剪贴板读取加入响应。');\n                                                            }\n                                                            catch {\n                                                                appendSystemMessage('浏览器不允许读取剪贴板，请手动粘贴。');\n                                                            }\n                                                        }}\n                                                    >\n                                                        从剪贴板粘贴\n                                                    </button>\n                                                    <button\n                                                        type=\"button\"\n                                                        className=\"dialog-button\"\n                                                        disabled={!manualResponsePayloadText}\n                                                        onClick={() => setManualResponsePayloadText('')}\n                                                    >\n                                                        清空\n                                                    </button>\n                                                </div>\n                                            </>\n                                        ) : (\n                                            <p className=\"lan-join-hint\">优先扫码，只有需要排障时再展开高级方式。</p>\n                                        )}\n                                    </div>\n                                </div>\n                        </div>\n                    </div>\n                </div>\n            ) : null}\n\n            {joinDialogOpen ? (\n                <div className=\"lan-dialog-overlay\" onClick={() => setJoinDialogOpen(false)}>\n                    <div className=\"lan-dialog lan-dialog-wide\" onClick={(event) => event.stopPropagation()}>\n                        <div className=\"lan-dialog-header\">\n                            <h3>加入房间</h3>\n                            <button type=\"button\" className=\"lan-dialog-close\" onClick={() => setJoinDialogOpen(false)}>\n                                ×\n                            </button>\n                        </div>\n                        <div className=\"lan-dialog-body\">\n                            {meshSnapshot.activeQrPayloadKind === 'join-response' ? (\n                                <div className=\"lan-panel\">\n                                    <div className=\"lan-panel-header\">\n                                        <h3>加入响应二维码</h3>\n                                        <span>把这张码给房主扫描即可。</span>\n                                    </div>\n                                    <QrCodeCard\n                                        title={meshSnapshot.activeQrPayloadTitle ?? '加入响应二维码'}\n                                        description={meshSnapshot.activeQrPayloadDescription ?? '等待房主扫描这张二维码。'}\n                                        payloadText={meshSnapshot.activeQrPayloadText}\n                                    />\n                                    <textarea\n                                        className=\"lan-sdp-textarea\"\n                                        readOnly={true}\n                                        value={meshSnapshot.activeQrPayloadText}\n                                        data-lan-output=\"active-payload\"\n                                        placeholder=\"响应二维码原始内容。\"\n                                    />\n                                    <div className=\"lan-actions\">\n                                        <button\n                                            type=\"button\"\n                                            className=\"dialog-button\"\n                                            disabled={!meshSnapshot.activeQrPayloadText}\n                                            onClick={() => {\n                                                void handleCopyPayload();\n                                            }}\n                                        >\n                                            复制响应内容\n                                        </button>\n                                        {clipboardHint ? <span className=\"lan-hint\">{clipboardHint}</span> : null}\n                                    </div>\n                                </div>\n                            ) : null}\n\n                            <div className=\"lan-dialog-grid\">\n                                <QrScannerPanel\n                                    onDetected={async (payloadText) => {\n                                        await handleImportPayload(payloadText);\n                                    }}\n                                />\n\n                                <div className=\"lan-panel\">\n                                    <div className=\"lan-panel-header\">\n                                        <h3>回退方式</h3>\n                                        <span>无法扫码时改为粘贴文本。</span>\n                                    </div>\n                                    <div className=\"lan-actions\">\n                                        <button\n                                            type=\"button\"\n                                            className=\"dialog-button\"\n                                            data-lan-action=\"toggle-manual\"\n                                            onClick={() => setShowAdvancedJoin((current) => !current)}\n                                        >\n                                            {showAdvancedJoin ? '隐藏高级方式' : '显示高级方式'}\n                                        </button>\n                                    </div>\n                                    {showAdvancedJoin ? (\n                                        <>\n                                            <textarea\n                                                className=\"lan-sdp-textarea\"\n                                                value={manualPayloadText}\n                                                data-lan-input=\"manual-payload\"\n                                                onChange={(event) => setManualPayloadText(event.target.value)}\n                                                placeholder=\"把二维码内容粘贴到这里，然后点击导入。\"\n                                            />\n                                            <div className=\"lan-actions\">\n                                                <button\n                                                    type=\"button\"\n                                                    className=\"dialog-button\"\n                                                    data-lan-action=\"import-payload\"\n                                                    disabled={busy}\n                                                    onClick={() => {\n                                                        void handleImportPayload().catch(() => undefined);\n                                                    }}\n                                                >\n                                                    导入二维码内容\n                                                </button>\n                                                <button\n                                                    type=\"button\"\n                                                    className=\"dialog-button\"\n                                                    disabled={busy}\n                                                    onClick={() => {\n                                                        void handlePastePayload();\n                                                    }}\n                                                >\n                                                    从剪贴板粘贴\n                                                </button>\n                                                <button\n                                                    type=\"button\"\n                                                    className=\"dialog-button\"\n                                                    disabled={!manualPayloadText}\n                                                    onClick={() => setManualPayloadText('')}\n                                                >\n                                                    清空\n                                                </button>\n                                            </div>\n                                        </>\n                                    ) : (\n                                        <p className=\"lan-join-hint\">默认扫码即可，高级方式只作兜底。</p>\n                                    )}\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            ) : null}\n        </div>\n    );\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lan/component/QrCodeCard.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport QRCode from 'qrcode';\n\ninterface QrCodeCardProps {\n    title: string;\n    description?: string;\n    payloadText: string;\n}\n\nexport const QrCodeCard: React.FC<QrCodeCardProps> = ({ title, description, payloadText }) => {\n    const [dataUrl, setDataUrl] = useState<string>();\n    const [errorText, setErrorText] = useState<string>();\n\n    useEffect(() => {\n        let cancelled = false;\n\n        if (!payloadText) {\n            setDataUrl(undefined);\n            setErrorText(undefined);\n            return;\n        }\n\n        QRCode.toDataURL(payloadText, {\n            errorCorrectionLevel: 'M',\n            margin: 1,\n            width: 280,\n            color: {\n                dark: '#ffffff',\n                light: '#000000',\n            },\n        })\n            .then((nextDataUrl) => {\n                if (!cancelled) {\n                    setDataUrl(nextDataUrl);\n                    setErrorText(undefined);\n                }\n            })\n            .catch((error) => {\n                if (!cancelled) {\n                    setDataUrl(undefined);\n                    setErrorText((error as Error).message);\n                }\n            });\n\n        return () => {\n            cancelled = true;\n        };\n    }, [payloadText]);\n\n    return (\n        <div className=\"lan-qr-card\" data-lan-card=\"qr\">\n            <div className=\"lan-panel-header\">\n                <h3>{title}</h3>\n                {description ? <span>{description}</span> : null}\n            </div>\n            {dataUrl ? (\n                <div className=\"lan-qr-artwork\">\n                    <img src={dataUrl} alt={title} />\n                </div>\n            ) : (\n                <div className=\"lan-qr-placeholder\">\n                    {errorText ?? '当前还没有可展示的二维码内容。'}\n                </div>\n            )}\n        </div>\n    );\n};\n\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lan/component/QrScannerPanel.tsx",
    "content": "import React, { useEffect, useEffectEvent, useRef, useState } from 'react';\nimport jsQR from 'jsqr';\n\ninterface QrScannerPanelProps {\n    onDetected: (payloadText: string) => Promise<void>;\n}\n\nasync function decodeQrFromFile(file: File): Promise<string | undefined> {\n    const bitmap = await createImageBitmap(file);\n    const canvas = document.createElement('canvas');\n    canvas.width = bitmap.width;\n    canvas.height = bitmap.height;\n\n    const context = canvas.getContext('2d', {\n        willReadFrequently: true,\n    });\n    if (!context) {\n        throw new Error('无法创建二维码识别画布。');\n    }\n\n    context.drawImage(bitmap, 0, 0);\n    const imageData = context.getImageData(0, 0, canvas.width, canvas.height);\n    const result = jsQR(imageData.data, imageData.width, imageData.height, {\n        inversionAttempts: 'attemptBoth',\n    });\n    return result?.data;\n}\n\nexport const QrScannerPanel: React.FC<QrScannerPanelProps> = ({ onDetected }) => {\n    const videoRef = useRef<HTMLVideoElement | null>(null);\n    const fileInputRef = useRef<HTMLInputElement | null>(null);\n    const frameRequestRef = useRef<number | undefined>(undefined);\n    const streamRef = useRef<MediaStream | undefined>(undefined);\n\n    const [active, setActive] = useState(false);\n    const [busy, setBusy] = useState(false);\n    const [errorText, setErrorText] = useState<string>();\n    const [lastDetectedText, setLastDetectedText] = useState<string>();\n    const handleDetected = useEffectEvent(onDetected);\n\n    const stopScanner = () => {\n        if (frameRequestRef.current) {\n            cancelAnimationFrame(frameRequestRef.current);\n            frameRequestRef.current = undefined;\n        }\n        streamRef.current?.getTracks().forEach((track) => track.stop());\n        streamRef.current = undefined;\n        if (videoRef.current) {\n            videoRef.current.srcObject = null;\n        }\n        setActive(false);\n    };\n\n    useEffect(() => stopScanner, []);\n\n    useEffect(() => {\n        if (!active) {\n            return;\n        }\n\n        let cancelled = false;\n        const canvas = document.createElement('canvas');\n        const context = canvas.getContext('2d', {\n            willReadFrequently: true,\n        });\n        if (!context) {\n            setErrorText('无法创建二维码识别画布。');\n            setActive(false);\n            return;\n        }\n\n        const scanLoop = async () => {\n            if (cancelled) {\n                return;\n            }\n\n            const video = videoRef.current;\n            if (video && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {\n                const width = video.videoWidth || 0;\n                const height = video.videoHeight || 0;\n                if (width > 0 && height > 0) {\n                    canvas.width = width;\n                    canvas.height = height;\n                    context.drawImage(video, 0, 0, width, height);\n                    const imageData = context.getImageData(0, 0, width, height);\n                    const result = jsQR(imageData.data, imageData.width, imageData.height, {\n                        inversionAttempts: 'attemptBoth',\n                    });\n                    if (result?.data && result.data !== lastDetectedText && !busy) {\n                        setBusy(true);\n                        setLastDetectedText(result.data);\n                        try {\n                            await handleDetected(result.data);\n                            stopScanner();\n                            setErrorText(undefined);\n                            setBusy(false);\n                            return;\n                        }\n                        catch (error) {\n                            setErrorText((error as Error).message);\n                        }\n                        finally {\n                            setBusy(false);\n                        }\n                    }\n                }\n            }\n\n            frameRequestRef.current = requestAnimationFrame(() => {\n                scanLoop().catch((error) => {\n                    setErrorText((error as Error).message);\n                    stopScanner();\n                });\n            });\n        };\n\n        navigator.mediaDevices\n            ?.getUserMedia({\n                video: {\n                    facingMode: 'environment',\n                },\n                audio: false,\n            })\n            .then((stream) => {\n                if (cancelled) {\n                    stream.getTracks().forEach((track) => track.stop());\n                    return;\n                }\n\n                streamRef.current = stream;\n                const video = videoRef.current;\n                if (!video) {\n                    stopScanner();\n                    return;\n                }\n\n                video.srcObject = stream;\n                video.setAttribute('playsinline', 'true');\n                video.play().catch((error) => {\n                    setErrorText((error as Error).message);\n                    stopScanner();\n                });\n                scanLoop().catch((error) => {\n                    setErrorText((error as Error).message);\n                    stopScanner();\n                });\n            })\n            .catch((error) => {\n                setErrorText((error as Error).message);\n                stopScanner();\n            });\n\n        return () => {\n            cancelled = true;\n            stopScanner();\n        };\n    }, [active, busy, lastDetectedText]);\n\n    const handleImportImage = async (file: File | undefined) => {\n        if (!file) {\n            return;\n        }\n        setBusy(true);\n        try {\n            const decoded = await decodeQrFromFile(file);\n            if (!decoded) {\n                throw new Error('没有在图片里识别到二维码。');\n            }\n            setLastDetectedText(decoded);\n            await handleDetected(decoded);\n            setErrorText(undefined);\n        }\n        catch (error) {\n            setErrorText((error as Error).message);\n        }\n        finally {\n            setBusy(false);\n            if (fileInputRef.current) {\n                fileInputRef.current.value = '';\n            }\n        }\n    };\n\n    return (\n        <div className=\"lan-panel lan-scanner-panel\" data-lan-card=\"scanner\">\n            <div className=\"lan-panel-header\">\n                <h3>扫码加入</h3>\n                <span>支持摄像头和图片导入。</span>\n            </div>\n\n            <div className=\"lan-scanner-preview\">\n                {active ? (\n                    <video ref={videoRef} muted={true} autoPlay={true} />\n                ) : (\n                    <div className=\"lan-qr-placeholder\">\n                        摄像头未开启，也可以直接导入二维码图片。\n                    </div>\n                )}\n            </div>\n\n            <div className=\"lan-actions\">\n                <button\n                    type=\"button\"\n                    className=\"dialog-button\"\n                    disabled={busy}\n                    data-lan-action={active ? 'stop-scanner' : 'start-scanner'}\n                    onClick={() => {\n                        if (active) {\n                            stopScanner();\n                            return;\n                        }\n                        setErrorText(undefined);\n                        setActive(true);\n                    }}\n                >\n                    {active ? '停止扫码' : '开启摄像头扫码'}\n                </button>\n                <button\n                    type=\"button\"\n                    className=\"dialog-button\"\n                    disabled={busy}\n                    onClick={() => fileInputRef.current?.click()}\n                >\n                    导入二维码图片\n                </button>\n                <input\n                    ref={fileInputRef}\n                    type=\"file\"\n                    accept=\"image/*\"\n                    className=\"lan-hidden-input\"\n                    onChange={(event) => {\n                        handleImportImage(event.target.files?.[0]).catch((error) => {\n                            setErrorText((error as Error).message);\n                        });\n                    }}\n                />\n            </div>\n\n            {errorText ? (\n                <div className=\"lan-error-text\">{errorText}</div>\n            ) : null}\n        </div>\n    );\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lobby/LobbyScreen.ts",
    "content": "import { Task } from \"@puzzl/core/lib/async/Task\";\nimport { WolConnection } from \"@/network/WolConnection\";\nimport { WolError } from \"@/network/WolError\";\nimport { SlotType, SlotInfo } from \"@/network/gameopt/SlotInfo\";\nimport { GameOpts } from \"@/game/gameopts/GameOpts\";\nimport { RANDOM_COUNTRY_ID, RANDOM_COLOR_ID, RANDOM_START_POS, NO_TEAM_ID, OBS_COUNTRY_ID, OBS_COLOR_ID, RANDOM_COUNTRY_NAME, OBS_COUNTRY_NAME, RANDOM_COLOR_NAME, RANDOM_COUNTRY_UI_NAME, OBS_COUNTRY_UI_NAME, RANDOM_COUNTRY_UI_TOOLTIP, OBS_COUNTRY_UI_TOOLTIP, aiUiNames } from \"@/game/gameopts/constants\";\nimport { LobbyForm } from \"@/gui/screen/mainMenu/lobby/component/LobbyForm\";\nimport { LobbyType, SlotOccupation, SlotType as ViewModelSlotType, PlayerStatus } from \"@/gui/screen/mainMenu/lobby/component/viewmodel/lobby\";\nimport { PasswordBox } from \"@/gui/screen/mainMenu/lobby/component/PasswordBox\";\nimport { CreateGameBox } from \"@/gui/screen/mainMenu/lobby/component/CreateGameBox\";\nimport { ScreenType } from \"@/gui/screen/mainMenu/ScreenType\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { DownloadError } from \"@/engine/ResourceLoader\";\nimport { CancellationTokenSource, OperationCanceledError } from \"@puzzl/core/lib/async/cancellation\";\nimport { MapPreviewRenderer } from \"@/gui/screen/mainMenu/lobby/MapPreviewRenderer\";\nimport { findIndexReverse } from \"@/util/array\";\nimport { SoundKey } from \"@/engine/sound/SoundKey\";\nimport { ChannelType } from \"@/engine/sound/ChannelType\";\nimport { StorageKey } from \"LocalPrefs\";\nimport { PreferredHostOpts } from \"@/gui/screen/mainMenu/lobby/PreferredHostOpts\";\nimport { isNotNullOrUndefined } from \"@/util/typeGuard\";\nimport { GameOptSanitizer } from \"@/game/gameopts/GameOptSanitizer\";\nimport { MainMenuScreen } from \"@/gui/screen/mainMenu/MainMenuScreen\";\nimport { MapFile } from \"data/MapFile\";\nimport { MapDigest } from \"@/engine/MapDigest\";\nimport { MAX_MAP_TRANSFER_BYTES } from \"@/network/gservConfig\";\nimport { WolHasMapStatus } from \"@/network/WolConfig\";\nimport { MainMenuRoute } from \"@/gui/screen/mainMenu/MainMenuRoute\";\nimport { sleep } from \"@puzzl/core/lib/async/sleep\";\nimport { ChatRecipientType } from \"@/network/chat/ChatMessage\";\nimport { ChatHistory } from \"@/gui/chat/ChatHistory\";\nimport { Throttle } from \"@/util/time\";\ninterface GameMode {\n    id: number;\n    label: string;\n    mpDialogSettings: any;\n}\ninterface GameModes {\n    getById(id: number): GameMode;\n}\ninterface MapListEntry {\n    fileName: string;\n    maxSlots: number;\n    official: boolean;\n    getFullMapTitle(strings: any): string;\n}\ninterface MapList {\n    getByName(name: string): MapListEntry;\n}\ninterface MapFileLoader {\n    load(mapName: string): Promise<any>;\n}\ninterface WolService {\n    getConfig(): any;\n    onWolConnectionLost: {\n        subscribe(handler: (error: any) => void): void;\n        unsubscribe(handler: (error: any) => void): void;\n    };\n}\ninterface WladderService {\n    getUrl(): string;\n    listSearch(playerNames: string[], cancellationToken: any): Promise<any[]>;\n}\ninterface MapTransferService {\n    getUrl(): string;\n}\ninterface GservConnection {\n    connect(url: string, options: any): Promise<void>;\n    ping(timeout: number): Promise<number>;\n    close(): void;\n}\ninterface RootController {\n    createGame(gameId: string, timestamp: number, gservUrl: string, username: string, gameOpts: GameOpts, singlePlayer: boolean, tournament: boolean, mapTransfer: boolean, privateGame: boolean, fallbackRoute: MainMenuRoute): void;\n    joinGame(gameId: string, timestamp: number, gservUrl: string, username: string, tournament: boolean, mapTransfer: boolean, fallbackRoute: MainMenuRoute): void;\n}\ninterface ErrorHandler {\n    handle(error: any, message: string, onClose: () => void): void;\n}\ninterface MessageBoxApi {\n    show(message: string, buttonText?: string, onClose?: () => void): void;\n    confirm(message: string, confirmText: string, cancelText: string): Promise<boolean>;\n}\ninterface Sound {\n    play(key: SoundKey, channel: ChannelType): void;\n}\ninterface LocalPrefs {\n    getItem(key: string): string | undefined;\n    setItem(key: string, value: string): void;\n    removeItem(key: string): void;\n}\ninterface Rules {\n    getMultiplayerCountries(): any[];\n    getMultiplayerColors(): Map<number, any>;\n    mpDialogSettings: any;\n    general: any;\n}\ninterface LobbyScreenParams {\n    create?: boolean;\n    game?: any;\n    observe?: boolean;\n}\ninterface CreateGameOptions {\n    roomDesc: string;\n    observe: boolean;\n    pass?: string;\n    tournament: boolean;\n}\ninterface PlayerPing {\n    playerName: string;\n    ping: number;\n}\nexport class LobbyScreen extends MainMenuScreen {\n    private botsEnabled: boolean;\n    private engineModHash: string;\n    private activeModMeta?: any;\n    private rootController: RootController;\n    private errorHandler: ErrorHandler;\n    private messageBoxApi: MessageBoxApi;\n    private strings: any;\n    private uiScene: any;\n    private wolCon: WolConnection;\n    private wolService: WolService;\n    private wladderService: WladderService;\n    private mapTransferService: MapTransferService;\n    private gservCon: GservConnection;\n    private rules: Rules;\n    private gameOptParser: any;\n    private gameOptSerializer: any;\n    private jsxRenderer: any;\n    private mapFileLoader: MapFileLoader;\n    private mapList: MapList;\n    private gameModes: GameModes;\n    private sound: Sound;\n    private localPrefs: LocalPrefs;\n    private messages: any[] = [];\n    private playerReadyStatus: Map<string, boolean> = new Map();\n    private playerHasMapStatus: Map<string, WolHasMapStatus> = new Map();\n    private playerProfiles: Map<string, any> = new Map();\n    private disposables: CompositeDisposable = new CompositeDisposable();\n    private playerPings: PlayerPing[] = [];\n    private gameChannelName?: string;\n    private hostMode: boolean = false;\n    private hostPlayerName?: string;\n    private hostIsFreshAccount?: boolean;\n    private hostRoomDesc: string = \"\";\n    private isTournament: boolean = false;\n    private hostPrivateGame: boolean = false;\n    private observerSlotIndex: number = 8;\n    private gameOpts?: GameOpts;\n    private frozenGameOpts?: GameOpts;\n    private slotsInfo?: SlotInfo[];\n    private currentMapFile?: any;\n    private preferredHostOpts?: PreferredHostOpts;\n    private formModel?: any;\n    private lobbyForm?: any;\n    private passBox?: any;\n    private createGameBox?: any;\n    private currentGameServer?: any;\n    private pingsUpdateTask?: Task<void>;\n    private ranksUpdateTask?: Task<void>;\n    private gservPingUpdateTask?: Task<void>;\n    private mapLoadTask?: Task<void>;\n    private hostOptsIntervalId?: number;\n    private gservPingIntervalId?: number;\n    constructor(botsEnabled: boolean, engineModHash: string, activeModMeta: any, rootController: RootController, errorHandler: ErrorHandler, messageBoxApi: MessageBoxApi, strings: any, uiScene: any, wolCon: WolConnection, wolService: WolService, wladderService: WladderService, mapTransferService: MapTransferService, gservCon: GservConnection, rules: Rules, gameOptParser: any, gameOptSerializer: any, jsxRenderer: any, mapFileLoader: MapFileLoader, mapList: MapList, gameModes: GameModes, sound: Sound, localPrefs: LocalPrefs) {\n        super();\n        this.botsEnabled = botsEnabled;\n        this.engineModHash = engineModHash;\n        this.activeModMeta = activeModMeta;\n        this.rootController = rootController;\n        this.errorHandler = errorHandler;\n        this.messageBoxApi = messageBoxApi;\n        this.strings = strings;\n        this.uiScene = uiScene;\n        this.wolCon = wolCon;\n        this.wolService = wolService;\n        this.wladderService = wladderService;\n        this.mapTransferService = mapTransferService;\n        this.gservCon = gservCon;\n        this.rules = rules;\n        this.gameOptParser = gameOptParser;\n        this.gameOptSerializer = gameOptSerializer;\n        this.jsxRenderer = jsxRenderer;\n        this.mapFileLoader = mapFileLoader;\n        this.mapList = mapList;\n        this.gameModes = gameModes;\n        this.sound = sound;\n        this.localPrefs = localPrefs;\n        this.updatePings = () => {\n            if (this.wolCon.isOpen()) {\n                if (this.gameChannelName && this.wolCon.isInChannel(this.gameChannelName)) {\n                    this.pingsUpdateTask?.cancel();\n                    const task = (this.pingsUpdateTask = new Task(async (cancellationToken) => {\n                        const users = await this.wolCon.listUsers(this.gameChannelName!);\n                        if (!cancellationToken.isCancelled()) {\n                            for (const user of users) {\n                                this.updatePlayerPing(user.name, user.ping);\n                            }\n                            this.sendPingData();\n                        }\n                    }));\n                    task.start().catch((error) => {\n                        if (!(error instanceof OperationCanceledError)) {\n                            console.error(error);\n                        }\n                    });\n                }\n            }\n            else {\n                this.onWolClose();\n            }\n        };\n        this.updateRanks = () => {\n            if (this.wladderService.getUrl() && this.slotsInfo) {\n                this.ranksUpdateTask?.cancel();\n                const task = (this.ranksUpdateTask = new Task(async (cancellationToken) => {\n                    await sleep(5000, cancellationToken);\n                    const playerNames = this.slotsInfo!\n                        .map((slot) => (slot.type === SlotType.Player ? slot.name : undefined))\n                        .filter(isNotNullOrUndefined);\n                    const profiles = await this.wladderService.listSearch(playerNames, cancellationToken);\n                    if (!cancellationToken.isCancelled()) {\n                        for (const profile of profiles) {\n                            this.playerProfiles.set(profile.name, profile);\n                        }\n                        this.updateFormModel();\n                    }\n                }));\n                task.start().catch((error) => {\n                    if (!(error instanceof OperationCanceledError)) {\n                        console.error(error);\n                    }\n                });\n            }\n        };\n        this.onChannelLeave = (event: any) => {\n            if (event.channel === this.gameChannelName) {\n                if (this.hostMode) {\n                    if (event.user.name !== this.wolCon.getCurrentUser()) {\n                        this.handlePlayerJoinLeave(event);\n                    }\n                }\n                else {\n                    if (event.user.name !== this.hostPlayerName && event.user.name !== this.wolCon.getCurrentUser()) {\n                        return;\n                    }\n                    this.controller?.goToScreen(ScreenType.CustomGame, {});\n                }\n            }\n        };\n        this.onChannelJoin = (event: any) => {\n            if (this.wolCon.isOpen() &&\n                this.gameChannelName &&\n                event.user.name !== this.wolCon.getCurrentUser()) {\n                this.sound.play(event.type === \"join\" ? SoundKey.PlayerJoined : SoundKey.PlayerLeft, ChannelType.Ui);\n                if (event.type === \"join\") {\n                    this.updatePlayerPing(event.user.name, event.user.ping);\n                }\n                if (this.hostMode) {\n                    this.handlePlayerJoinLeave(event);\n                }\n                else {\n                    this.wolCon.sendPlayerReady(false);\n                }\n                if (!this.playerProfiles.has(event.user.name)) {\n                    this.updateRanks();\n                }\n            }\n        };\n        this.onChannelMessage = (message: any) => {\n            if (this.lobbyForm) {\n                if (message.to.type === ChatRecipientType.Page ||\n                    message.to.type === ChatRecipientType.Whisper) {\n                    this.sound.play(SoundKey.IncomingMessage, ChannelType.Ui);\n                }\n                this.messages.push(message);\n                this.lobbyForm.refresh();\n            }\n            if (message.to.type === ChatRecipientType.Whisper &&\n                message.to.name !== this.wolCon.getServerName() &&\n                message.from !== this.wolCon.getCurrentUser()) {\n                this.chatHistory.lastWhisperFrom.value = message.from;\n            }\n        };\n        this.handleGameStart = (event: any) => {\n            const username = this.wolCon.getCurrentUser();\n            const fallbackRoute = new MainMenuRoute(ScreenType.Login, {\n                afterLogin: (messages: any[]) => new MainMenuRoute(ScreenType.CustomGame, { messages }),\n            });\n            if (this.hostMode) {\n                const gameOpts = this.frozenGameOpts ?? this.gameOpts;\n                const mapTransfer = [...this.playerHasMapStatus.values()].includes(WolHasMapStatus.MapTransfer);\n                this.rootController.createGame(event.gameId, event.timestamp, event.gservUrl, username!, gameOpts!, false, this.isTournament, mapTransfer, this.hostPrivateGame, fallbackRoute);\n            }\n            else {\n                const mapTransfer = this.playerHasMapStatus.get(username!) === WolHasMapStatus.MapTransfer;\n                this.rootController.joinGame(event.gameId, event.timestamp, event.gservUrl, username!, this.isTournament, mapTransfer, fallbackRoute);\n            }\n        };\n        this.handleGameServer = (event: any) => {\n            if (this.currentGameServer?.id !== event.id) {\n                this.currentGameServer = event;\n                this.formModel.selectedGameServer = event.id;\n                this.playerPings.length = 0;\n                this.lobbyForm?.refresh();\n                if (this.hostMode) {\n                    this.updatePings();\n                }\n                this.updateGservPing();\n            }\n        };\n        this.handleGameOpt = (event: any) => {\n            const opt = event.opt;\n            const optType = opt[0];\n            if (this.hostMode) {\n                if (event.user !== this.hostPlayerName) {\n                    if (optType === \"A\") {\n                        this.handleGameOptReady(event.user, opt[1]);\n                    }\n                    else if (optType === \"R\") {\n                        this.handlePlayerOptsChange(event.user, opt);\n                    }\n                    else if (optType === \"K\") {\n                        this.handleGameOptHasMap(event.user, opt[1]);\n                    }\n                    else {\n                        throw new Error(\"Unknown GAMEOPT string \" + opt);\n                    }\n                }\n            }\n            else {\n                if (optType === \"L\") {\n                    this.handleGameOptSlots(opt);\n                    if (this.slotsInfo!.some((slot) => slot.type === SlotType.Player &&\n                        !this.playerProfiles.has(slot.name!))) {\n                        this.updateRanks();\n                    }\n                }\n                else if (optType === \"P\") {\n                    this.handleGameOptPing(opt.slice(1));\n                }\n                else if (optType === \"O\") {\n                    this.handleGameOptObserver(opt[1]);\n                }\n                else if (optType === \"A\") {\n                    this.handleGameOptReady(event.user, opt[1]);\n                }\n                else if (optType === \"K\") {\n                    this.handleGameOptHasMap(event.user, opt[1]);\n                }\n                else if (optType === \"R\") {\n                    return;\n                }\n                else if (optType === \"G\") {\n                    if (!this.playerReadyStatus.get(this.wolCon.getCurrentUser())) {\n                        this.addSystemMessage(this.strings.get(\"GUI:HostGameStartJoiner\"));\n                    }\n                    return;\n                }\n                else if (!optType.match(/^-|\\d+/)) {\n                    throw new Error(\"Unknown GAMEOPT string \" + opt);\n                }\n                else {\n                    this.handleGameOptOptions(opt);\n                }\n            }\n            this.updateFormModel();\n        };\n        this.onWolClose = () => {\n            if (this.hostOptsIntervalId) {\n                clearInterval(this.hostOptsIntervalId);\n            }\n            if (this.gservPingIntervalId) {\n                clearInterval(this.gservPingIntervalId);\n            }\n        };\n        this.onWolConLost = (error: any) => {\n            this.errorHandler.handle(error, this.strings.get(\"TXT_YOURE_DISCON\"), () => {\n                this.controller?.goToScreen(ScreenType.Home);\n            });\n        };\n    }\n    private updatePings: () => void;\n    private updateRanks: () => void;\n    private onChannelLeave: (event: any) => void;\n    private onChannelJoin: (event: any) => void;\n    private onChannelMessage: (message: any) => void;\n    private handleGameStart: (event: any) => void;\n    private handleGameServer: (event: any) => void;\n    private handleGameOpt: (event: any) => void;\n    private onWolClose: () => void;\n    private onWolConLost: (error: any) => void;\n    private chatHistory!: ChatHistory;\n    async onEnter(params: LobbyScreenParams): Promise<void> {\n        if (!this.wolCon.getCurrentUser()) {\n            this.messageBoxApi.show(this.strings.get(\"TXT_YOURE_DISCON\"), this.strings.get(\"GUI:Ok\"), () => {\n                this.controller?.goToScreen(ScreenType.Home);\n            });\n            return;\n        }\n        const cancellationSource = new CancellationTokenSource();\n        this.disposables.add(() => cancellationSource.cancel());\n        const cancellationToken = cancellationSource.token;\n        this.gameChannelName = undefined;\n        this.lobbyForm = undefined;\n        this.chatHistory = new ChatHistory();\n        this.playerPings = [];\n        this.initFormModel();\n        this.wolCon.onGameOpt.subscribe(this.handleGameOpt);\n        this.wolCon.onGameStart.subscribe(this.handleGameStart);\n        this.wolCon.onGameServer.subscribe(this.handleGameServer);\n        this.wolCon.onLeaveChannel.subscribe(this.onChannelLeave);\n        this.wolCon.onJoinChannel.subscribe(this.onChannelJoin);\n        this.wolCon.onChatMessage.subscribe(this.onChannelMessage);\n        this.wolCon.onClose.subscribe(this.onWolClose);\n        this.wolService.onWolConnectionLost.subscribe(this.onWolConLost);\n        this.hostMode = !!params.create;\n        if (this.hostMode) {\n            this.title = this.strings.get(\"GUI:HostScreen\");\n            this.createGame(cancellationToken);\n        }\n        else {\n            this.title = this.strings.get(\"GUI:JoinScreen\");\n            const { game, observe } = params;\n            this.joinGame(game!, !!observe, undefined, cancellationToken);\n        }\n    }\n    private async joinGame(game: any, observe: boolean, password?: string, cancellationToken?: any): Promise<void> {\n        if (password || !game.passLocked) {\n            const channelName = game.name;\n            try {\n                const hostPlayerPromise = this.waitForHostPlayer(channelName, cancellationToken).catch((error) => {\n                    if (!(error instanceof OperationCanceledError))\n                        throw error;\n                });\n                await this.wolCon.joinGame(channelName, password, observe);\n                if (cancellationToken?.isCancelled())\n                    return;\n                this.gameChannelName = channelName;\n                const hostPlayer = await hostPlayerPromise;\n                if (cancellationToken?.isCancelled())\n                    return;\n                this.hostPlayerName = hostPlayer.name;\n                this.hostIsFreshAccount = hostPlayer.fresh;\n                this.isTournament = game.tournament;\n                this.formModel.channels = [this.gameChannelName];\n                if (observe) {\n                    this.sendPlayerInfo(OBS_COUNTRY_ID, RANDOM_COLOR_ID, RANDOM_START_POS, NO_TEAM_ID);\n                }\n                else {\n                    const savedCountry = this.localPrefs.getItem(StorageKey.LastPlayerCountry);\n                    const savedColor = this.localPrefs.getItem(StorageKey.LastPlayerColor);\n                    const countryId = savedCountry !== undefined &&\n                        Number(savedCountry) < this.getAvailablePlayerCountries().length\n                        ? Number(savedCountry)\n                        : RANDOM_COUNTRY_ID;\n                    const colorId = savedColor !== undefined &&\n                        Number(savedColor) < this.getAvailablePlayerColors().length &&\n                        this.getSelectablePlayerColors().includes(this.getColorNameById(Number(savedColor)))\n                        ? Number(savedColor)\n                        : RANDOM_COLOR_ID;\n                    if (!(countryId === RANDOM_COUNTRY_ID && colorId === RANDOM_COLOR_ID)) {\n                        this.sendPlayerInfo(countryId, colorId, RANDOM_START_POS, NO_TEAM_ID);\n                    }\n                }\n                this.observerSlotIndex = 8;\n            }\n            catch (error) {\n                if (error instanceof WolError) {\n                    const errorMessages = new Map<string, string>()\n                        .set(WolError.Code.BadChannelPass, \"TXT_BADPASS\")\n                        .set(WolError.Code.GameHasClosed, \"TXT_GAME_CLOSED\")\n                        .set(WolError.Code.ChannelFull, \"TXT_CHANNEL_FULL\")\n                        .set(WolError.Code.BannedFromChannel, \"TXT_JOINBAN\");\n                    const messageKey = errorMessages.get(error.code);\n                    if (messageKey) {\n                        this.messageBoxApi.show(this.strings.get(messageKey), this.strings.get(\"GUI:Ok\"), () => {\n                            this.controller?.goToScreen(ScreenType.CustomGame, {});\n                        });\n                        return;\n                    }\n                }\n                else if (error instanceof OperationCanceledError) {\n                    return;\n                }\n                this.handleError(error, this.strings.get(\"WOL:MatchBadParameters\"));\n                return;\n            }\n            this.controller.toggleSidebarPreview(true);\n            this.initView();\n        }\n        else {\n            this.showPasswordBox((password: string) => {\n                this.joinGame(game, observe, password, cancellationToken);\n            }, () => {\n                this.controller?.goToScreen(ScreenType.CustomGame, {});\n            });\n        }\n    }\n    private waitForHostPlayer(channelName: string, cancellationToken: any): Promise<any> {\n        return new Promise((resolve, reject) => {\n            const handler = (event: any) => {\n                if (event.channelName === channelName) {\n                    this.wolCon.onChannelUsers.unsubscribe(handler);\n                    const hostPlayer = event.users.find((user: any) => user.operator);\n                    if (hostPlayer) {\n                        resolve(hostPlayer);\n                    }\n                    else {\n                        reject(new Error(\"Host player not found\"));\n                    }\n                }\n            };\n            this.wolCon.onChannelUsers.subscribe(handler);\n            cancellationToken?.register(() => {\n                this.wolCon.onChannelUsers.unsubscribe(handler);\n                reject(new OperationCanceledError(cancellationToken));\n            });\n        });\n    }\n    private async createGame(cancellationToken: any, options?: CreateGameOptions): Promise<void> {\n        if (options) {\n            try {\n                const { roomDesc, tournament, observe, pass } = options;\n                const channelName = this.wolCon.makeGameChannelName();\n                const hostPlayerPromise = this.waitForHostPlayer(channelName, cancellationToken).catch((error) => {\n                    if (!(error instanceof OperationCanceledError))\n                        throw error;\n                });\n                await this.wolCon.createGame(channelName, 1, 9, this.wolService.getConfig().getClientChannelType(), tournament, pass);\n                if (cancellationToken?.isCancelled())\n                    return;\n                this.gameChannelName = channelName;\n                const hostPlayer = await hostPlayerPromise;\n                if (cancellationToken?.isCancelled())\n                    return;\n                this.hostPlayerName = this.wolCon.getCurrentUser();\n                this.hostIsFreshAccount = hostPlayer.fresh;\n                this.hostRoomDesc = roomDesc;\n                this.isTournament = tournament;\n                this.hostPrivateGame = !!pass;\n                this.observerSlotIndex = observe ? 0 : 8;\n                this.formModel.lobbyType = LobbyType.MultiplayerHost;\n                this.formModel.activeSlotIndex = observe ? -1 : 0;\n                this.formModel.channels = [this.gameChannelName];\n                await this.initHostOptions(observe, cancellationToken);\n                if (cancellationToken?.isCancelled())\n                    return;\n                this.updateMapPreview(this.currentMapFile);\n                this.updateFormModel();\n                this.updatePings();\n                this.updateRanks();\n                this.sendGameOpts();\n                this.sendModeMaxSlots();\n                this.hostOptsIntervalId = window.setInterval(() => {\n                    if (this.wolCon.isOpen() && this.gameChannelName) {\n                        this.sendGameOpts();\n                        this.updatePings();\n                    }\n                }, 5000);\n            }\n            catch (error) {\n                this.handleError(error, error instanceof DownloadError\n                    ? this.strings.get(\"TXT_DOWNLOAD_FAILED\")\n                    : this.strings.get(\"WOL:MatchBadParameters\"));\n                return;\n            }\n            this.controller.toggleSidebarPreview(true);\n            this.initView();\n        }\n        else {\n            this.showCreateGameBox((roomDesc: string, pass: string, observe: boolean) => {\n                this.createGame(cancellationToken, {\n                    roomDesc,\n                    observe,\n                    pass,\n                    tournament: false,\n                });\n            }, () => {\n                this.controller?.goToScreen(ScreenType.CustomGame, {});\n            });\n        }\n    }\n    private getAvailablePlayerCountryRules(): any[] {\n        return this.rules.getMultiplayerCountries();\n    }\n    private getAvailablePlayerCountries(): string[] {\n        return this.getAvailablePlayerCountryRules().map((country) => country.name);\n    }\n    private getAvailablePlayerColors(): string[] {\n        return [...this.rules.getMultiplayerColors().values()].map((color) => color.asHexString());\n    }\n    private getSelectablePlayerColors(): string[] {\n        const usedColors: string[] = [];\n        if (this.formModel?.playerSlots) {\n            this.formModel.playerSlots.forEach((slot: any) => {\n                if (slot)\n                    usedColors.push(slot.color);\n            });\n        }\n        const availableColors = this.getAvailablePlayerColors();\n        return [RANDOM_COLOR_NAME].concat(availableColors.filter((color) => color && !usedColors.includes(color)));\n    }\n    private getColorNameById(colorId: number): string {\n        return colorId === RANDOM_COLOR_ID\n            ? RANDOM_COLOR_NAME\n            : this.getAvailablePlayerColors()[colorId];\n    }\n    private getColorIdByName(colorName: string): number {\n        if (colorName === RANDOM_COLOR_NAME)\n            return RANDOM_COLOR_ID;\n        const availableColors = this.getAvailablePlayerColors();\n        const colorId = availableColors.indexOf(colorName);\n        if (colorId === -1) {\n            throw new Error(`Color ${colorName} not found in available player colors`);\n        }\n        return colorId;\n    }\n    private initFormModel(): void {\n    }\n    private updateFormModel(): void {\n    }\n    private initView(): void {\n        this.initLobbyForm();\n        this.refreshSidebarButtons();\n        this.refreshSidebarMpText();\n        this.controller.showSidebarButtons();\n    }\n    private initLobbyForm(): void {\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            innerRef: (ref: any) => (this.lobbyForm = ref),\n            component: LobbyForm,\n            props: this.formModel,\n        }));\n        this.controller.setMainComponent(component);\n    }\n    private refreshSidebarButtons(): void {\n    }\n    private refreshSidebarMpText(): void {\n    }\n    private sendPlayerInfo(countryId: number, colorId: number, startPos: number, teamId: number, slotIndex?: number): void {\n    }\n    private updatePlayerPing(playerName: string, ping: number): void {\n        const existingPing = this.playerPings.find((p) => p.playerName === playerName);\n        if (existingPing) {\n            existingPing.ping = ping;\n        }\n        else {\n            this.playerPings.push({ ping, playerName });\n        }\n    }\n    @Throttle(350)\n    private sendGameOpts(): void {\n    }\n    @Throttle(350)\n    private sendGameSlotInfo(): void {\n    }\n    private sendPingData(): void {\n    }\n    private sendModeMaxSlots(): void {\n    }\n    private handlePlayerJoinLeave(event: any): void {\n    }\n    private handleGameOptReady(playerName: string, ready: string): void {\n    }\n    private handleGameOptHasMap(playerName: string, hasMap: string): void {\n    }\n    private handleGameOptSlots(opt: string): void {\n    }\n    private handleGameOptPing(opt: string): void {\n    }\n    private handleGameOptObserver(opt: string): void {\n    }\n    private handleGameOptOptions(opt: string): void {\n    }\n    private handlePlayerOptsChange(playerName: string, opt: string[]): void {\n    }\n    private addSystemMessage(text: string): void {\n        if (this.lobbyForm) {\n            this.messages.push({ text });\n            this.lobbyForm.refresh();\n        }\n    }\n    private showPasswordBox(onSubmit: (password: string) => void, onDismiss: () => void): void {\n    }\n    private showCreateGameBox(onSubmit: (roomDesc: string, pass: string, observe: boolean) => void, onDismiss: () => void): void {\n    }\n    private async initHostOptions(observe: boolean, cancellationToken: any): Promise<void> {\n    }\n    @Throttle(5000)\n    private updateGservPing(): void {\n    }\n    private updateMapPreview(mapFile?: any): void {\n        try {\n            if (mapFile) {\n                const preview = new MapPreviewRenderer(this.strings).render(new MapFile(mapFile), this.hostMode ? LobbyType.MultiplayerHost : LobbyType.MultiplayerGuest, this.controller.getSidebarPreviewSize());\n                this.controller.setSidebarPreview(preview);\n            }\n        }\n        catch (error) {\n            console.error(\"Failed to render map preview\");\n            console.error(error);\n            this.controller.setSidebarPreview();\n        }\n    }\n    private handleError(error: any, message: string): void {\n        this.errorHandler.handle(error, message, () => {\n            this.controller?.goToScreen(ScreenType.CustomGame, {});\n        });\n        if (this.hostOptsIntervalId) {\n            clearInterval(this.hostOptsIntervalId);\n        }\n        if (this.gservPingIntervalId) {\n            clearInterval(this.gservPingIntervalId);\n        }\n    }\n    async onLeave(): Promise<void> {\n        if (this.wolCon.isOpen() && this.wolCon.getCurrentUser() && this.gameChannelName) {\n            this.wolCon.leaveChannel(this.gameChannelName);\n        }\n        this.disposables.dispose();\n        this.mapLoadTask?.cancel();\n        this.mapLoadTask = undefined;\n        this.ranksUpdateTask?.cancel();\n        this.ranksUpdateTask = undefined;\n        this.pingsUpdateTask?.cancel();\n        this.pingsUpdateTask = undefined;\n        this.gservPingUpdateTask?.cancel();\n        this.gservPingUpdateTask = undefined;\n        this.currentMapFile = undefined;\n        this.gameChannelName = undefined;\n        this.hostPlayerName = undefined;\n        this.hostIsFreshAccount = undefined;\n        this.hostRoomDesc = \"\";\n        this.gameOpts = undefined;\n        this.frozenGameOpts = undefined;\n        this.preferredHostOpts = undefined;\n        this.slotsInfo = undefined;\n        this.playerProfiles.clear();\n        this.currentGameServer = undefined;\n        if (this.hostOptsIntervalId) {\n            clearInterval(this.hostOptsIntervalId);\n        }\n        if (this.gservPingIntervalId) {\n            clearInterval(this.gservPingIntervalId);\n        }\n        this.wolCon.onGameOpt.unsubscribe(this.handleGameOpt);\n        this.wolCon.onGameStart.unsubscribe(this.handleGameStart);\n        this.wolCon.onGameServer.unsubscribe(this.handleGameServer);\n        this.wolCon.onLeaveChannel.unsubscribe(this.onChannelLeave);\n        this.wolCon.onJoinChannel.unsubscribe(this.onChannelJoin);\n        this.wolCon.onChatMessage.unsubscribe(this.onChannelMessage);\n        this.wolCon.onClose.unsubscribe(this.onWolClose);\n        this.wolService.onWolConnectionLost.unsubscribe(this.onWolConLost);\n        this.controller.toggleSidebarPreview(false);\n        await this.unrender();\n    }\n    private async unrender(): Promise<void> {\n        await this.controller.hideSidebarButtons();\n        this.lobbyForm = undefined;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lobby/MapPreviewRenderer.ts",
    "content": "import { CanvasUtils } from \"@/engine/gfx/CanvasUtils\";\nimport { HtmlContainer } from \"@/gui/HtmlContainer\";\nimport { UiObject } from \"@/gui/UiObject\";\nimport { LobbyType } from \"@/gui/screen/mainMenu/lobby/component/viewmodel/lobby\";\nimport { Coords } from \"@/game/Coords\";\nimport { IsoCoords } from \"@/engine/IsoCoords\";\nimport { THREE } from \"@/setupThreeGlobal\";\ninterface MapFile {\n    decodePreviewImage(): {\n        data: Uint8Array;\n        width: number;\n        height: number;\n    };\n    fullSize: {\n        width: number;\n        height: number;\n    };\n    localSize: {\n        width: number;\n        height: number;\n        x: number;\n        y: number;\n    };\n    startingLocations: {\n        x: number;\n        y: number;\n    }[];\n}\ninterface Size {\n    width: number;\n    height: number;\n}\nconst tooltipMap = new Map<LobbyType, string>([\n    [LobbyType.Singleplayer, \"STT:SkirmishMapThumbnail\"],\n    [LobbyType.MultiplayerHost, \"STT:HostMapThumbnail\"],\n    [LobbyType.MultiplayerGuest, \"STT:GuestMapThumbnail\"],\n]);\nexport class MapPreviewRenderer {\n    private strings: any;\n    constructor(strings: any) {\n        this.strings = strings;\n    }\n    render(mapFile: MapFile, lobbyType: LobbyType, containerSize: Size): UiObject | undefined {\n        let previewImage;\n        try {\n            previewImage = mapFile.decodePreviewImage();\n        }\n        catch (error) {\n            console.error(\"Failed to decode map preview data\", error);\n        }\n        if (previewImage) {\n            const { data, width, height } = previewImage;\n            let canvas = CanvasUtils.canvasFromRgbImageData(data, width, height);\n            let scale = 1;\n            const scaleFactor = canvas.width < containerSize.width / 2 || canvas.height < containerSize.height / 2 ? 4 : 2;\n            const scaledCanvas = document.createElement(\"canvas\");\n            scaledCanvas.width = scaleFactor * canvas.width;\n            scaledCanvas.height = scaleFactor * canvas.height;\n            const ctx = scaledCanvas.getContext(\"2d\");\n            if (ctx) {\n                scale = scaleFactor;\n                ctx.scale(scale, scale);\n                ctx.drawImage(canvas, 0, 0);\n                canvas = scaledCanvas;\n            }\n            this.drawStartLocations(canvas, mapFile, containerSize, scale);\n            const container = new HtmlContainer();\n            const uiObject = new UiObject(new THREE.Object3D(), container);\n            container.setSize(\"100%\", \"100%\");\n            container.render();\n            canvas.style.objectFit = \"contain\";\n            canvas.style.width = \"100%\";\n            canvas.style.height = \"100%\";\n            canvas.setAttribute(\"data-r-tooltip\", this.strings.get(tooltipMap.get(lobbyType)));\n            container.getElement().appendChild(canvas);\n            return uiObject;\n        }\n        return undefined;\n    }\n    private drawStartLocations(canvas: HTMLCanvasElement, mapFile: MapFile, containerSize: Size, scale: number): void {\n        const ctx = canvas.getContext(\"2d\");\n        if (!ctx)\n            return;\n        IsoCoords.init({\n            x: 0,\n            y: (mapFile.fullSize.width * Coords.getWorldTileSize()) / 2,\n        });\n        const worldOrigin = IsoCoords.worldToScreen(0, 0);\n        const screenTileOrigin = IsoCoords.screenToScreenTile(worldOrigin.x, worldOrigin.y);\n        const canvasScale = canvas.width > canvas.height\n            ? canvas.width / containerSize.width / scale\n            : canvas.height / containerSize.height / scale;\n        const fontSize = 13 * canvasScale;\n        const outlineWidth = 2 * canvasScale;\n        for (const [index, location] of mapFile.startingLocations.entries()) {\n            const screenPos = IsoCoords.tileToScreen(location.x, location.y);\n            const screenTilePos = IsoCoords.screenToScreenTile(screenPos.x, screenPos.y);\n            screenTilePos.x += screenTileOrigin.x;\n            screenTilePos.y += screenTileOrigin.y;\n            const canvasPos = this.dxyToCanvas(screenTilePos.x, screenTilePos.y, canvas, mapFile.localSize);\n            canvasPos.x /= scale;\n            canvasPos.y /= scale;\n            CanvasUtils.drawText(ctx, String(index + 1), canvasPos.x - fontSize / 4, canvasPos.y - fontSize / 2, {\n                fontSize: fontSize,\n                color: \"yellow\",\n                outlineColor: \"black\",\n                outlineWidth: outlineWidth,\n            });\n        }\n    }\n    private dxyToCanvas(x: number, y: number, canvas: HTMLCanvasElement, localSize: {\n        width: number;\n        height: number;\n        x: number;\n        y: number;\n    }): {\n        x: number;\n        y: number;\n    } {\n        const scaleX = canvas.width / (2 * localSize.width);\n        const scaleY = canvas.height / localSize.height / 2;\n        return {\n            x: (x - 2 * localSize.x) * scaleX,\n            y: (y - 2 * localSize.y) * scaleY,\n        };\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lobby/PreferredHostOpts.ts",
    "content": "import { GameOpts } from '@/game/gameopts/GameOpts';\nexport class PreferredHostOpts {\n    gameSpeed: number = 6;\n    credits: number = 10000;\n    unitCount: number = 10;\n    shortGame: boolean = true;\n    superWeapons: boolean = false;\n    buildOffAlly: boolean = true;\n    mcvRepacks: boolean = true;\n    cratesAppear: boolean = false;\n    hostTeams: boolean = false;\n    destroyableBridges: boolean = true;\n    multiEngineer: boolean = false;\n    noDogEngiKills: boolean = false;\n    slotsClosed: Set<number> = new Set();\n    serialize(): string {\n        return [\n            this.gameSpeed,\n            this.credits,\n            this.unitCount,\n            Number(this.shortGame),\n            Number(this.superWeapons),\n            Number(this.buildOffAlly),\n            Number(this.mcvRepacks),\n            Number(this.cratesAppear),\n            [...this.slotsClosed].join(','),\n            Number(this.hostTeams),\n            Number(this.destroyableBridges),\n            Number(this.multiEngineer),\n            Number(this.noDogEngiKills),\n        ].join(';');\n    }\n    unserialize(data: string): this {\n        const [gameSpeed, credits, unitCount, shortGame, superWeapons, buildOffAlly, mcvRepacks, cratesAppear, slotsClosed, hostTeams = '0', destroyableBridges = '1', multiEngineer, noDogEngiKills] = data.split(';');\n        this.gameSpeed = Number(gameSpeed);\n        this.credits = Number(credits);\n        this.unitCount = Number(unitCount);\n        this.shortGame = Boolean(Number(shortGame));\n        this.superWeapons = Boolean(Number(superWeapons));\n        this.buildOffAlly = Boolean(Number(buildOffAlly));\n        this.mcvRepacks = Boolean(Number(mcvRepacks));\n        this.cratesAppear = Boolean(Number(cratesAppear));\n        this.hostTeams = Boolean(Number(hostTeams));\n        this.destroyableBridges = Boolean(Number(destroyableBridges));\n        this.multiEngineer = Boolean(Number(multiEngineer));\n        this.noDogEngiKills = Boolean(Number(noDogEngiKills));\n        this.slotsClosed = new Set(slotsClosed && slotsClosed.length > 0\n            ? slotsClosed.split(',').map(Number)\n            : []);\n        return this;\n    }\n    applyMpDialogSettings(mpDialogSettings: any): this {\n        this.gameSpeed = mpDialogSettings.gameSpeed !== undefined ? 6 - mpDialogSettings.gameSpeed : this.gameSpeed;\n        this.credits = mpDialogSettings.money ?? this.credits;\n        this.unitCount = mpDialogSettings.unitCount ?? this.unitCount;\n        this.shortGame = mpDialogSettings.shortGame ?? this.shortGame;\n        this.superWeapons = mpDialogSettings.superWeapons ?? this.superWeapons;\n        this.buildOffAlly = mpDialogSettings.buildOffAlly ?? this.buildOffAlly;\n        this.mcvRepacks = mpDialogSettings.mcvRedeploys ?? this.mcvRepacks;\n        this.cratesAppear = mpDialogSettings.crates ?? this.cratesAppear;\n        this.destroyableBridges = mpDialogSettings.bridgeDestruction ?? this.destroyableBridges;\n        this.multiEngineer = mpDialogSettings.multiEngineer ?? this.multiEngineer;\n        this.noDogEngiKills = mpDialogSettings.noDogEngiKills ?? this.noDogEngiKills;\n        return this;\n    }\n    applyGameOpts(gameOpts: GameOpts): this {\n        this.gameSpeed = gameOpts.gameSpeed;\n        this.credits = gameOpts.credits;\n        this.unitCount = gameOpts.unitCount;\n        this.shortGame = gameOpts.shortGame;\n        this.superWeapons = gameOpts.superWeapons;\n        this.buildOffAlly = gameOpts.buildOffAlly;\n        this.mcvRepacks = gameOpts.mcvRepacks;\n        this.cratesAppear = gameOpts.cratesAppear;\n        this.hostTeams = gameOpts.hostTeams ?? false;\n        this.destroyableBridges = gameOpts.destroyableBridges;\n        this.multiEngineer = gameOpts.multiEngineer;\n        this.noDogEngiKills = gameOpts.noDogEngiKills;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lobby/PregameController.ts",
    "content": "import { StorageKey } from '@/LocalPrefs';\nimport { GameOpts, AiDifficulty } from '@/game/gameopts/GameOpts';\nimport {\n    RANDOM_COUNTRY_ID,\n    RANDOM_COLOR_ID,\n    RANDOM_START_POS,\n    NO_TEAM_ID,\n    OBS_TEAM_ID,\n    OBS_COUNTRY_ID,\n    OBS_COUNTRY_NAME,\n    RANDOM_COUNTRY_NAME,\n    RANDOM_COUNTRY_UI_NAME,\n    RANDOM_COUNTRY_UI_TOOLTIP,\n    OBS_COUNTRY_UI_NAME,\n    OBS_COUNTRY_UI_TOOLTIP,\n    RANDOM_COLOR_NAME,\n    aiUiNames,\n} from '@/game/gameopts/constants';\nimport { LobbyType, SlotOccupation, PlayerStatus, SlotType as UiSlotType } from '@/gui/screen/mainMenu/lobby/component/viewmodel/lobby';\nimport { SlotType as NetSlotType, SlotInfo } from '@/network/gameopt/SlotInfo';\nimport { MapDigest } from '@/engine/MapDigest';\nimport { findIndexReverse } from '@/util/array';\nimport { PreferredHostOpts } from '@/gui/screen/mainMenu/lobby/PreferredHostOpts';\nimport { Parser } from '@/network/gameopt/Parser';\nimport { Serializer } from '@/network/gameopt/Serializer';\nimport { BotRegistry } from '@/game/ai/thirdpartbot/BotRegistry';\n\ninterface Rules {\n    getMultiplayerCountries(): any[];\n    getMultiplayerColors(): Map<number, any>;\n    mpDialogSettings: any;\n    general?: any;\n}\n\ninterface GameMode {\n    id: number;\n    label: string;\n    mpDialogSettings: any;\n}\n\ninterface GameModes {\n    getAll(): GameMode[];\n    getById(id: number): GameMode;\n}\n\ninterface MapListEntry {\n    fileName: string;\n    maxSlots: number;\n    official?: boolean;\n    getFullMapTitle(strings: any): string;\n}\n\ninterface MapList {\n    getAll(): MapListEntry[];\n    getByName(name: string): MapListEntry | undefined;\n}\n\ninterface MapFileLoader {\n    load(mapName: string): Promise<any>;\n}\n\ninterface LocalPrefs {\n    getItem(key: string): string | undefined;\n    setItem(key: string, value: string): void;\n    removeItem(key: string): void;\n}\n\nexport interface PregameMapSelectionResult {\n    gameMode: GameMode;\n    mapName: string;\n    changedMapFile?: any;\n}\n\nexport interface PregameSnapshot {\n    gameOpts: GameOpts;\n    slotsInfo: SlotInfo[];\n    currentMapFile?: any;\n}\n\nexport interface PregameLobbyFormOptions {\n    lobbyType: LobbyType;\n    activeSlotIndex: number;\n    selectedGameServer?: string;\n    messages?: any[];\n    localUsername?: string;\n    channels?: any[];\n    chatHistory?: any;\n    onSendMessage?: (message: string) => void;\n    onStateChange?: () => void;\n    decoratePlayerSlot?: (playerSlot: any, slotInfo: SlotInfo | undefined, slotIndex: number) => void;\n}\n\nfunction cloneAiPlayer(ai: any) {\n    return ai\n        ? {\n            difficulty: ai.difficulty,\n            customBotId: ai.customBotId,\n            countryId: ai.countryId,\n            colorId: ai.colorId,\n            startPos: ai.startPos,\n            teamId: ai.teamId,\n        }\n        : undefined;\n}\n\nfunction cloneHumanPlayer(player: any) {\n    return {\n        name: player.name,\n        countryId: player.countryId,\n        colorId: player.colorId,\n        startPos: player.startPos,\n        teamId: player.teamId,\n    };\n}\n\nfunction cloneGameOpts(gameOpts: GameOpts): GameOpts {\n    return {\n        gameMode: gameOpts.gameMode,\n        gameSpeed: gameOpts.gameSpeed,\n        credits: gameOpts.credits,\n        unitCount: gameOpts.unitCount,\n        shortGame: gameOpts.shortGame,\n        superWeapons: gameOpts.superWeapons,\n        buildOffAlly: gameOpts.buildOffAlly,\n        mcvRepacks: gameOpts.mcvRepacks,\n        cratesAppear: gameOpts.cratesAppear,\n        hostTeams: gameOpts.hostTeams,\n        destroyableBridges: gameOpts.destroyableBridges,\n        multiEngineer: gameOpts.multiEngineer,\n        noDogEngiKills: gameOpts.noDogEngiKills,\n        mapName: gameOpts.mapName,\n        mapTitle: gameOpts.mapTitle,\n        mapDigest: gameOpts.mapDigest,\n        mapSizeBytes: gameOpts.mapSizeBytes,\n        maxSlots: gameOpts.maxSlots,\n        mapOfficial: gameOpts.mapOfficial,\n        humanPlayers: gameOpts.humanPlayers.map(cloneHumanPlayer),\n        aiPlayers: gameOpts.aiPlayers.map(cloneAiPlayer),\n        unknown: gameOpts.unknown,\n    };\n}\n\nfunction cloneSlotsInfo(slotsInfo: SlotInfo[]): SlotInfo[] {\n    return slotsInfo.map((slot) => ({\n        type: slot.type,\n        name: slot.name,\n        difficulty: slot.difficulty,\n        customBotId: slot.customBotId,\n    }));\n}\n\nexport class PregameController {\n    private gameOpts?: GameOpts;\n    private slotsInfo?: SlotInfo[];\n    private currentMapFile?: any;\n    private preferredHostOpts?: PreferredHostOpts;\n\n    constructor(\n        private readonly strings: any,\n        private readonly rules: Rules,\n        private readonly mapFileLoader: MapFileLoader,\n        private readonly mapList: MapList,\n        private readonly gameModes: GameModes,\n        private readonly localPrefs: LocalPrefs,\n        private readonly playerName: string\n    ) {\n    }\n\n    async initialize(): Promise<void> {\n        await this.initOptions();\n    }\n\n    isInitialized(): boolean {\n        return Boolean(this.gameOpts && this.slotsInfo);\n    }\n\n    private mergeDefinedSettings(...sources: any[]): any {\n        const merged: Record<string, any> = {};\n        for (const source of sources) {\n            if (!source) {\n                continue;\n            }\n            for (const key in source) {\n                if (!Object.prototype.hasOwnProperty.call(source, key) || source[key] === undefined) {\n                    continue;\n                }\n                merged[key] = source[key];\n            }\n        }\n        return merged;\n    }\n\n    private getEffectiveMpDialogSettings(gameModeId: number): any {\n        return this.mergeDefinedSettings(this.rules.mpDialogSettings, this.gameModes.getById(gameModeId).mpDialogSettings);\n    }\n\n    getSnapshot(): PregameSnapshot {\n        return {\n            gameOpts: cloneGameOpts(this.requireGameOpts()),\n            slotsInfo: cloneSlotsInfo(this.requireSlotsInfo()),\n            currentMapFile: this.currentMapFile,\n        };\n    }\n\n    hydrate(snapshot: PregameSnapshot): void {\n        this.gameOpts = cloneGameOpts(snapshot.gameOpts);\n        this.slotsInfo = cloneSlotsInfo(snapshot.slotsInfo);\n        this.currentMapFile = snapshot.currentMapFile;\n    }\n\n    getGameOpts(): GameOpts {\n        return this.requireGameOpts();\n    }\n\n    getSlotsInfo(): SlotInfo[] {\n        return this.requireSlotsInfo();\n    }\n\n    getCurrentMapFile(): any {\n        return this.currentMapFile;\n    }\n\n    getUsedSlots(): number {\n        return 1 + findIndexReverse(this.requireSlotsInfo(), (slot) => slot.type === NetSlotType.Ai || slot.type === NetSlotType.Player);\n    }\n\n    isHumanObserver(): boolean {\n        return this.requireGameOpts().humanPlayers[0]?.countryId === OBS_COUNTRY_ID;\n    }\n\n    meetsMinimumTeams(): boolean {\n        const gameOpts = this.requireGameOpts();\n        const allPlayers = [\n            ...gameOpts.humanPlayers,\n            ...gameOpts.aiPlayers,\n        ]\n            .filter(Boolean)\n            .filter((player: any) => player.countryId !== OBS_COUNTRY_ID);\n\n        if (!allPlayers.length) {\n            return false;\n        }\n\n        const firstTeamId = allPlayers[0].teamId;\n        return firstTeamId === NO_TEAM_ID || allPlayers.some((player: any) => player.teamId !== firstTeamId);\n    }\n\n    updateSelfName(playerName: string): void {\n        const gameOpts = this.requireGameOpts();\n        const slotsInfo = this.requireSlotsInfo();\n        const currentHuman = gameOpts.humanPlayers[0];\n        if (!currentHuman || currentHuman.name === playerName) {\n            return;\n        }\n        currentHuman.name = playerName;\n        const hostSlot = slotsInfo.find((slot) => slot.type === NetSlotType.Player && slot.name === this.playerName) ?? slotsInfo[0];\n        if (hostSlot) {\n            hostSlot.name = playerName;\n        }\n    }\n\n    applyMapSelection(params: PregameMapSelectionResult): void {\n        const gameOpts = this.requireGameOpts();\n        const slotsInfo = this.requireSlotsInfo();\n        const modeChanged = params.gameMode.id !== gameOpts.gameMode;\n        gameOpts.gameMode = params.gameMode.id;\n        const mapEntry = this.mapList.getByName(params.mapName);\n        if (!mapEntry) {\n            throw new Error(`Map ${params.mapName} not found`);\n        }\n        const mapFile = params.changedMapFile ?? this.currentMapFile;\n        this.currentMapFile = mapFile;\n        const lastUsedSlotIndex = findIndexReverse(slotsInfo, (slot) => slot.type === NetSlotType.Ai ||\n            slot.type === NetSlotType.Player ||\n            slot.type === NetSlotType.Open);\n        const observerBonus = this.isHumanObserver() ? 1 : 0;\n        const slotsToClose = Math.max(0, lastUsedSlotIndex + 1 - (mapEntry.maxSlots + observerBonus));\n        for (let index = 0; index < slotsToClose; index += 1) {\n            slotsInfo[lastUsedSlotIndex - index].type = NetSlotType.Closed;\n            gameOpts.aiPlayers[lastUsedSlotIndex - index] = undefined;\n        }\n        const mpDialogSettings = this.getEffectiveMpDialogSettings(gameOpts.gameMode);\n        [...gameOpts.humanPlayers, ...gameOpts.aiPlayers].forEach((player: any) => {\n            if (!player) {\n                return;\n            }\n            if (player.startPos > mapEntry.maxSlots - 1) {\n                player.startPos = RANDOM_START_POS;\n            }\n            if (modeChanged) {\n                player.teamId = mpDialogSettings.alliesAllowed && mpDialogSettings.mustAlly ? 0 : NO_TEAM_ID;\n            }\n        });\n        this.applyGameOption((opts) => {\n            opts.mapName = mapEntry.fileName;\n            opts.mapDigest = MapDigest.compute(mapFile);\n            opts.mapSizeBytes = mapFile.getSize();\n            opts.mapTitle = mapEntry.getFullMapTitle(this.strings);\n            opts.maxSlots = mapEntry.maxSlots;\n            opts.mapOfficial = mapEntry.official ?? false;\n        });\n        this.localPrefs.setItem(StorageKey.LastMap, mapEntry.fileName);\n        this.localPrefs.setItem(StorageKey.LastMode, String(params.gameMode.id));\n        this.saveBotSettings();\n    }\n\n    private buildAvailableAiNames(): Map<string, string> {\n        const names = new Map<string, string>();\n        names.set('Easy', aiUiNames.get(AiDifficulty.Easy)!);\n        names.set('Normal', aiUiNames.get(AiDifficulty.Normal)!);\n        const uploadedBots = BotRegistry.getInstance().getUploadedBots();\n        if (uploadedBots.length > 0) {\n            for (const bot of uploadedBots) {\n                names.set(`Custom:${bot.id}`, bot.displayName);\n            }\n        } else {\n            names.set('Custom', aiUiNames.get(AiDifficulty.Custom)!);\n        }\n        return names;\n    }\n\n    createLobbyFormProps(options: PregameLobbyFormOptions): any {\n        const gameOpts = this.requireGameOpts();\n        const slotsInfo = this.requireSlotsInfo();\n        const mpDialogSettings = this.getEffectiveMpDialogSettings(gameOpts.gameMode);\n        const onStateChange = () => options.onStateChange?.();\n        const playerSlots = this.buildPlayerSlots(options.decoratePlayerSlot);\n\n        return {\n            strings: this.strings,\n            countryUiNames: new Map<string, string>([\n                [RANDOM_COUNTRY_NAME, RANDOM_COUNTRY_UI_NAME],\n                [OBS_COUNTRY_NAME, OBS_COUNTRY_UI_NAME],\n                ...this.getAvailablePlayerCountryRules().map((country: any) => [country.name, country.uiName] as [string, string]),\n            ]),\n            countryUiTooltips: new Map<string, string>([\n                [RANDOM_COUNTRY_NAME, RANDOM_COUNTRY_UI_TOOLTIP],\n                [OBS_COUNTRY_NAME, OBS_COUNTRY_UI_TOOLTIP],\n                ...this.getAvailablePlayerCountryRules()\n                    .filter((country: any) => country.uiTooltip)\n                    .map((country: any) => [country.name, country.uiTooltip] as [string, string]),\n            ]),\n            availablePlayerCountries: [RANDOM_COUNTRY_NAME, OBS_COUNTRY_NAME].concat(this.getAvailablePlayerCountries()),\n            availablePlayerColors: this.getSelectablePlayerColors(playerSlots),\n            availableAiNames: this.buildAvailableAiNames(),\n            availableStartPositions: this.getSelectableStartPositions(playerSlots, gameOpts.maxSlots),\n            maxTeams: 4,\n            lobbyType: options.lobbyType,\n            mpDialogSettings,\n            selectedGameServer: options.selectedGameServer,\n            activeSlotIndex: options.activeSlotIndex,\n            teamsAllowed: mpDialogSettings.alliesAllowed,\n            teamsRequired: mpDialogSettings.mustAlly,\n            playerSlots,\n            shortGame: gameOpts.shortGame,\n            mcvRepacks: gameOpts.mcvRepacks,\n            cratesAppear: gameOpts.cratesAppear,\n            superWeapons: gameOpts.superWeapons,\n            buildOffAlly: gameOpts.buildOffAlly,\n            hostTeams: gameOpts.hostTeams ?? false,\n            destroyableBridges: gameOpts.destroyableBridges,\n            multiEngineer: gameOpts.multiEngineer,\n            multiEngineerCount: Math.ceil((1 - ((this.rules as any).general?.engineerCaptureLevel || 0.5)) /\n                ((this.rules as any).general?.engineerDamage || 0.25)) + 1,\n            noDogEngiKills: gameOpts.noDogEngiKills,\n            gameSpeed: gameOpts.gameSpeed,\n            credits: gameOpts.credits,\n            unitCount: gameOpts.unitCount,\n            messages: options.messages,\n            localUsername: options.localUsername,\n            channels: options.channels,\n            chatHistory: options.chatHistory,\n            onSendMessage: options.onSendMessage,\n            onCountrySelect: (country: string, slotIndex: number) => {\n                this.handleCountrySelect(country, slotIndex);\n                onStateChange();\n            },\n            onColorSelect: (color: string, slotIndex: number) => {\n                this.handleColorSelect(color, slotIndex);\n                onStateChange();\n            },\n            onStartPosSelect: (startPos: number, slotIndex: number) => {\n                this.handleStartPosSelect(startPos, slotIndex);\n                onStateChange();\n            },\n            onTeamSelect: (team: number, slotIndex: number) => {\n                this.handleTeamSelect(team, slotIndex);\n                onStateChange();\n            },\n            onSlotChange: (occupation: SlotOccupation, slotIndex: number, aiDifficulty?: AiDifficulty, customBotId?: string) => {\n                this.handleSlotChange(occupation, slotIndex, aiDifficulty, customBotId);\n                onStateChange();\n            },\n            onToggleShortGame: (value: boolean) => {\n                this.applyGameOption((opts) => (opts.shortGame = value));\n                onStateChange();\n            },\n            onToggleMcvRepacks: (value: boolean) => {\n                this.applyGameOption((opts) => (opts.mcvRepacks = value));\n                onStateChange();\n            },\n            onToggleCratesAppear: (value: boolean) => {\n                this.applyGameOption((opts) => (opts.cratesAppear = value));\n                onStateChange();\n            },\n            onToggleSuperWeapons: (value: boolean) => {\n                this.applyGameOption((opts) => (opts.superWeapons = value));\n                onStateChange();\n            },\n            onToggleBuildOffAlly: (value: boolean) => {\n                this.applyGameOption((opts) => (opts.buildOffAlly = value));\n                onStateChange();\n            },\n            onToggleHostTeams: (value: boolean) => {\n                this.applyGameOption((opts) => (opts.hostTeams = value));\n                onStateChange();\n            },\n            onToggleDestroyableBridges: (value: boolean) => {\n                this.applyGameOption((opts) => (opts.destroyableBridges = value));\n                onStateChange();\n            },\n            onToggleMultiEngineer: (value: boolean) => {\n                this.applyGameOption((opts) => (opts.multiEngineer = value));\n                onStateChange();\n            },\n            onToggleNoDogEngiKills: (value: boolean) => {\n                this.applyGameOption((opts) => (opts.noDogEngiKills = value));\n                onStateChange();\n            },\n            onChangeGameSpeed: (value: number) => {\n                this.applyGameOption((opts) => (opts.gameSpeed = value));\n                onStateChange();\n            },\n            onChangeCredits: (value: number) => {\n                this.applyGameOption((opts) => (opts.credits = value));\n                onStateChange();\n            },\n            onChangeUnitCount: (value: number) => {\n                this.applyGameOption((opts) => (opts.unitCount = value));\n                onStateChange();\n            },\n        };\n    }\n\n    getCountryNameById(countryId: number): string {\n        if (countryId === RANDOM_COUNTRY_ID) {\n            return RANDOM_COUNTRY_NAME;\n        }\n        if (countryId === OBS_COUNTRY_ID) {\n            return OBS_COUNTRY_NAME;\n        }\n        return this.getAvailablePlayerCountries()[countryId];\n    }\n\n    getCountryIdByName(name: string): number {\n        if (name === RANDOM_COUNTRY_NAME) {\n            return RANDOM_COUNTRY_ID;\n        }\n        if (name === OBS_COUNTRY_NAME) {\n            return OBS_COUNTRY_ID;\n        }\n        return this.getAvailablePlayerCountries().indexOf(name);\n    }\n\n    getColorNameById(colorId: number): string {\n        return colorId === RANDOM_COLOR_ID ? RANDOM_COLOR_NAME : this.getAvailablePlayerColors()[colorId];\n    }\n\n    getColorIdByName(name: string): number {\n        if (name === RANDOM_COLOR_NAME) {\n            return RANDOM_COLOR_ID;\n        }\n        const index = this.getAvailablePlayerColors().indexOf(name);\n        if (index === -1) {\n            throw new Error(`Color ${name} not found in available player colors`);\n        }\n        return index;\n    }\n\n    private requireGameOpts(): GameOpts {\n        if (!this.gameOpts) {\n            throw new Error('Pregame options are not initialized');\n        }\n        return this.gameOpts;\n    }\n\n    private requireSlotsInfo(): SlotInfo[] {\n        if (!this.slotsInfo) {\n            throw new Error('Pregame slots are not initialized');\n        }\n        return this.slotsInfo;\n    }\n\n    private async initOptions(): Promise<void> {\n        const savedOpts = this.localPrefs.getItem(StorageKey.PreferredGameOpts);\n        const savedCountry = this.localPrefs.getItem(StorageKey.LastPlayerCountry);\n        const savedColor = this.localPrefs.getItem(StorageKey.LastPlayerColor);\n        const savedStartPos = this.localPrefs.getItem(StorageKey.LastPlayerStartPos);\n        const savedTeam = this.localPrefs.getItem(StorageKey.LastPlayerTeam);\n        const savedMap = this.localPrefs.getItem(StorageKey.LastMap);\n        const savedMode = this.localPrefs.getItem(StorageKey.LastMode);\n        const savedBots = this.localPrefs.getItem(StorageKey.LastBots);\n\n        let selectedMap = savedMap ? this.mapList.getByName(savedMap) : undefined;\n        let selectedModeId = selectedMap && savedMode && this.gameModes.getAll().find((mode) => mode.id === Number(savedMode))\n            ? Number(savedMode)\n            : 1;\n        let selectedMode = this.gameModes.getById(selectedModeId);\n\n        if (!selectedMap || !(selectedMap as any)?.gameModes?.find((mode: any) => mode.mapFilter === (selectedMode as any).mapFilter)) {\n            selectedModeId = 1;\n            selectedMode = this.gameModes.getById(selectedModeId);\n            selectedMap = this.mapList\n                .getAll()\n                .find((map) => (map as any).gameModes?.find((mode: any) => (selectedMode as any).mapFilter === mode.mapFilter));\n        }\n\n        if (!selectedMap) {\n            throw new Error('Unable to resolve an initial map for pregame setup');\n        }\n\n        this.currentMapFile = await this.mapFileLoader.load(selectedMap.fileName);\n        const preferredOpts = (this.preferredHostOpts = new PreferredHostOpts());\n        const mpDialogSettings = this.getEffectiveMpDialogSettings(selectedModeId);\n        if (savedOpts) {\n            preferredOpts.unserialize(savedOpts);\n        }\n        else {\n            preferredOpts.applyMpDialogSettings(mpDialogSettings);\n        }\n\n        const lastBots = savedBots ? this.parseSavedBots(savedBots) : undefined;\n        const defaultAiDifficulty = AiDifficulty.Easy;\n        const humanCountryId = savedCountry !== undefined &&\n            Number(savedCountry) < this.getAvailablePlayerCountries().length\n            ? Number(savedCountry)\n            : RANDOM_COUNTRY_ID;\n        const humanIsObserver = humanCountryId === OBS_COUNTRY_ID;\n        const effectiveMaxSlots = humanIsObserver ? selectedMap.maxSlots + 1 : selectedMap.maxSlots;\n\n        if (lastBots) {\n            this.sanitizeLastBotSettings(lastBots, savedColor, savedStartPos, selectedMap.maxSlots, mpDialogSettings, humanIsObserver);\n        }\n\n        this.gameOpts = {\n            gameMode: selectedModeId,\n            shortGame: preferredOpts.shortGame,\n            mcvRepacks: preferredOpts.mcvRepacks,\n            cratesAppear: preferredOpts.cratesAppear,\n            superWeapons: preferredOpts.superWeapons,\n            gameSpeed: preferredOpts.gameSpeed,\n            credits: preferredOpts.credits,\n            unitCount: preferredOpts.unitCount,\n            buildOffAlly: preferredOpts.buildOffAlly,\n            hostTeams: false,\n            destroyableBridges: preferredOpts.destroyableBridges,\n            multiEngineer: preferredOpts.multiEngineer,\n            noDogEngiKills: preferredOpts.noDogEngiKills,\n            humanPlayers: [\n                {\n                    name: this.playerName,\n                    countryId: humanCountryId,\n                    colorId: savedColor !== undefined &&\n                        Number(savedColor) < this.getAvailablePlayerColors().length\n                        ? Number(savedColor)\n                        : RANDOM_COLOR_ID,\n                    startPos: savedStartPos !== undefined &&\n                        Number(savedStartPos) < this.getAvailableStartPositionsForMax(selectedMap.maxSlots).length\n                        ? Number(savedStartPos)\n                        : RANDOM_START_POS,\n                    teamId: savedTeam !== undefined && mpDialogSettings.alliesAllowed && Number(savedTeam) < 4\n                        ? Number(savedTeam)\n                        : mpDialogSettings.mustAlly ? 0 : NO_TEAM_ID,\n                },\n            ],\n            aiPlayers: new Array(8).fill(undefined).map((_, index) => {\n                if (index && !(index > effectiveMaxSlots - 1)) {\n                    const difficulty = index > 1 || lastBots ? lastBots?.[index]?.difficulty : defaultAiDifficulty;\n                    if (difficulty !== undefined) {\n                        return {\n                            difficulty,\n                            customBotId: lastBots?.[index]?.customBotId,\n                            countryId: lastBots?.[index]?.countryId ?? RANDOM_COUNTRY_ID,\n                            colorId: lastBots?.[index]?.colorId ?? RANDOM_COLOR_ID,\n                            startPos: lastBots?.[index]?.startPos ?? RANDOM_START_POS,\n                            teamId: lastBots?.[index]?.teamId ?? (mpDialogSettings.mustAlly ? 3 : NO_TEAM_ID),\n                        } as any;\n                    }\n                }\n                return undefined;\n            }),\n            mapName: selectedMap.fileName,\n            mapDigest: MapDigest.compute(this.currentMapFile),\n            mapSizeBytes: this.currentMapFile.getSize(),\n            mapTitle: selectedMap.getFullMapTitle(this.strings),\n            maxSlots: selectedMap.maxSlots,\n            mapOfficial: selectedMap.official ?? false,\n        };\n\n        this.slotsInfo = [{ type: NetSlotType.Player, name: this.playerName }];\n        for (let index = 1; index < 8; index += 1) {\n            if (index < effectiveMaxSlots && this.gameOpts.aiPlayers[index]) {\n                const ai = this.gameOpts.aiPlayers[index]!;\n                this.slotsInfo.push({ type: NetSlotType.Ai, difficulty: ai.difficulty, customBotId: ai.customBotId });\n            }\n            else {\n                const type = index < effectiveMaxSlots\n                    ? (preferredOpts.slotsClosed.has(index) ? NetSlotType.Closed : NetSlotType.Open)\n                    : NetSlotType.Closed;\n                this.slotsInfo.push({ type });\n            }\n        }\n    }\n\n    private sanitizeLastBotSettings(aiPlayers: (any | undefined)[], savedColor: string | undefined, savedStartPos: string | undefined, maxSlots: number, mpDialogSettings: any, humanIsObserver: boolean = false): void {\n        const maxAi = humanIsObserver ? maxSlots : maxSlots - 1;\n        let aiCount = 0;\n        for (let index = 0; index < aiPlayers.length; index += 1) {\n            if (aiPlayers[index]) {\n                aiCount += 1;\n                if (aiCount > maxAi) {\n                    aiPlayers[index] = undefined;\n                }\n            }\n        }\n\n        const usedColors = savedColor !== undefined ? [Number(savedColor)] : [];\n        const usedStartPositions = savedStartPos !== undefined ? [Number(savedStartPos)] : [];\n\n        for (const ai of aiPlayers) {\n            if (!ai) {\n                continue;\n            }\n            if (ai.difficulty !== AiDifficulty.Easy && ai.difficulty !== AiDifficulty.Normal && ai.difficulty !== AiDifficulty.Custom) {\n                ai.difficulty = AiDifficulty.Easy;\n            }\n            if (ai.countryId !== undefined && ai.countryId >= this.getAvailablePlayerCountries().length) {\n                ai.countryId = RANDOM_COUNTRY_ID;\n            }\n            if (ai.colorId !== undefined && ai.colorId !== RANDOM_COLOR_ID) {\n                if (ai.colorId >= this.getAvailablePlayerColors().length || usedColors.includes(ai.colorId)) {\n                    ai.colorId = RANDOM_COLOR_ID;\n                }\n                else {\n                    usedColors.push(ai.colorId);\n                }\n            }\n            if (ai.startPos !== undefined && ai.startPos !== RANDOM_START_POS) {\n                if (ai.startPos >= this.getAvailableStartPositionsForMax(maxSlots).length || usedStartPositions.includes(ai.startPos)) {\n                    ai.startPos = RANDOM_START_POS;\n                }\n                else {\n                    usedStartPositions.push(ai.startPos);\n                }\n            }\n            if (ai.teamId !== NO_TEAM_ID) {\n                if (ai.teamId >= 4 || !mpDialogSettings.alliesAllowed) {\n                    ai.teamId = mpDialogSettings.mustAlly ? 3 : NO_TEAM_ID;\n                }\n            }\n            else if (mpDialogSettings.mustAlly) {\n                ai.teamId = 3;\n            }\n        }\n    }\n\n    private getAvailablePlayerCountries(): string[] {\n        return this.rules.getMultiplayerCountries().map((country: any) => country.name);\n    }\n\n    private getAvailablePlayerCountryRules(): any[] {\n        return this.rules.getMultiplayerCountries();\n    }\n\n    private getAvailablePlayerColors(): string[] {\n        return [...this.rules.getMultiplayerColors().values()].map((color: any) => color.asHexString());\n    }\n\n    private getAvailableStartPositionsForMax(maxSlots: number): number[] {\n        return new Array(maxSlots).fill(0).map((_, index) => index);\n    }\n\n    private applyGameOption(modifier: (opts: GameOpts) => void): void {\n        modifier(this.requireGameOpts());\n        this.savePreferences();\n    }\n\n    private handleCountrySelect(countryName: string, slotIndex: number): void {\n        const playerSlots = this.buildPlayerSlots();\n        const wasObserver = this.isHumanObserver();\n        this.updatePlayerInfo(this.getCountryIdByName(countryName), this.getColorIdByName(playerSlots[slotIndex].color), playerSlots[slotIndex].startPos, playerSlots[slotIndex].team, slotIndex);\n        const isNowObserver = this.isHumanObserver();\n        const slotsInfo = this.requireSlotsInfo();\n        const gameOpts = this.requireGameOpts();\n        if (!wasObserver && isNowObserver) {\n            const extraSlotIndex = gameOpts.maxSlots;\n            if (extraSlotIndex < 8 && slotsInfo[extraSlotIndex]?.type === NetSlotType.Closed) {\n                slotsInfo[extraSlotIndex].type = NetSlotType.Open;\n            }\n        }\n        else if (wasObserver && !isNowObserver) {\n            const extraSlotIndex = gameOpts.maxSlots;\n            if (extraSlotIndex < 8) {\n                slotsInfo[extraSlotIndex].type = NetSlotType.Closed;\n                gameOpts.aiPlayers[extraSlotIndex] = undefined as any;\n            }\n        }\n    }\n\n    private handleColorSelect(colorName: string, slotIndex: number): void {\n        const playerSlots = this.buildPlayerSlots();\n        this.updatePlayerInfo(this.getCountryIdByName(playerSlots[slotIndex].country), this.getColorIdByName(colorName), playerSlots[slotIndex].startPos, playerSlots[slotIndex].team, slotIndex);\n    }\n\n    private handleStartPosSelect(startPos: number, slotIndex: number): void {\n        const playerSlots = this.buildPlayerSlots();\n        this.updatePlayerInfo(this.getCountryIdByName(playerSlots[slotIndex].country), this.getColorIdByName(playerSlots[slotIndex].color), startPos, playerSlots[slotIndex].team, slotIndex);\n    }\n\n    private handleTeamSelect(teamId: number, slotIndex: number): void {\n        const playerSlots = this.buildPlayerSlots();\n        if (teamId === OBS_TEAM_ID) {\n            if (slotIndex === 0 && !this.isHumanObserver()) {\n                this.handleCountrySelect(OBS_COUNTRY_NAME, slotIndex);\n            }\n            return;\n        }\n        if (slotIndex === 0 && this.isHumanObserver()) {\n            this.updatePlayerInfo(RANDOM_COUNTRY_ID, this.getColorIdByName(playerSlots[slotIndex].color), playerSlots[slotIndex].startPos, teamId, slotIndex);\n            const extraSlotIndex = this.requireGameOpts().maxSlots;\n            if (extraSlotIndex < 8) {\n                this.requireSlotsInfo()[extraSlotIndex].type = NetSlotType.Closed;\n                this.requireGameOpts().aiPlayers[extraSlotIndex] = undefined as any;\n            }\n            return;\n        }\n        this.updatePlayerInfo(this.getCountryIdByName(playerSlots[slotIndex].country), this.getColorIdByName(playerSlots[slotIndex].color), playerSlots[slotIndex].startPos, teamId, slotIndex);\n    }\n\n    private handleSlotChange(occupation: SlotOccupation, slotIndex: number, aiDifficulty?: AiDifficulty, customBotId?: string): void {\n        this.changeSlotType(occupation, slotIndex, aiDifficulty, customBotId);\n        this.saveBotSettings();\n    }\n\n    private changeSlotType(occupation: SlotOccupation, slotIndex: number, aiDifficulty?: AiDifficulty, customBotId?: string): void {\n        if (slotIndex === 0) {\n            throw new Error('Change slot type of host');\n        }\n\n        const slotsInfo = this.requireSlotsInfo();\n        const gameOpts = this.requireGameOpts();\n        if (occupation === SlotOccupation.Occupied && aiDifficulty !== undefined) {\n            const mpDialogSettings = this.getEffectiveMpDialogSettings(gameOpts.gameMode);\n            const slot = slotsInfo[slotIndex];\n            slot.type = NetSlotType.Ai;\n            slot.difficulty = aiDifficulty;\n            slot.customBotId = customBotId;\n            if (!gameOpts.aiPlayers[slotIndex]) {\n                gameOpts.aiPlayers[slotIndex] = {\n                    difficulty: aiDifficulty,\n                    customBotId,\n                    countryId: RANDOM_COUNTRY_ID,\n                    colorId: RANDOM_COLOR_ID,\n                    startPos: RANDOM_START_POS,\n                    teamId: mpDialogSettings.mustAlly ? 3 : NO_TEAM_ID,\n                } as any;\n            }\n            gameOpts.aiPlayers[slotIndex]!.difficulty = aiDifficulty;\n            gameOpts.aiPlayers[slotIndex]!.customBotId = customBotId;\n            return;\n        }\n\n        if (occupation === SlotOccupation.Closed) {\n            slotsInfo[slotIndex].type = NetSlotType.Closed;\n            gameOpts.aiPlayers[slotIndex] = undefined as any;\n            return;\n        }\n\n        slotsInfo[slotIndex].type = NetSlotType.Open;\n        gameOpts.aiPlayers[slotIndex] = undefined as any;\n    }\n\n    private updatePlayerInfo(countryId: number, colorId: number, startPos: number, teamId: number, slotIndex: number): void {\n        const slot = this.requireSlotsInfo()[slotIndex];\n        if (slot.type === NetSlotType.Ai) {\n            const ai = this.requireGameOpts().aiPlayers[slotIndex];\n            if (!ai) {\n                throw new Error(`No AI found on slot ${slotIndex}`);\n            }\n            ai.countryId = countryId;\n            ai.colorId = colorId;\n            ai.startPos = startPos;\n            ai.teamId = teamId;\n            this.saveBotSettings();\n            return;\n        }\n\n        if (slot.type === NetSlotType.Player) {\n            const human = this.requireGameOpts().humanPlayers.find((player) => player.name === slot.name);\n            if (!human) {\n                throw new Error(`No player found on slot ${slotIndex}`);\n            }\n            human.countryId = countryId;\n            human.colorId = colorId;\n            human.startPos = startPos;\n            human.teamId = teamId;\n            if (countryId !== RANDOM_COUNTRY_ID) {\n                this.localPrefs.setItem(StorageKey.LastPlayerCountry, String(countryId));\n            }\n            else {\n                this.localPrefs.removeItem(StorageKey.LastPlayerCountry);\n            }\n            if (colorId !== RANDOM_COLOR_ID) {\n                this.localPrefs.setItem(StorageKey.LastPlayerColor, String(colorId));\n            }\n            else {\n                this.localPrefs.removeItem(StorageKey.LastPlayerColor);\n            }\n            if (startPos !== RANDOM_START_POS) {\n                this.localPrefs.setItem(StorageKey.LastPlayerStartPos, String(startPos));\n            }\n            else {\n                this.localPrefs.removeItem(StorageKey.LastPlayerStartPos);\n            }\n            if (teamId !== NO_TEAM_ID) {\n                this.localPrefs.setItem(StorageKey.LastPlayerTeam, String(teamId));\n            }\n            else {\n                this.localPrefs.removeItem(StorageKey.LastPlayerTeam);\n            }\n            return;\n        }\n\n        throw new Error(`Unexpected slot type ${slot.type}`);\n    }\n\n    private buildPlayerSlots(decoratePlayerSlot?: (playerSlot: any, slotInfo: SlotInfo | undefined, slotIndex: number) => void): any[] {\n        const gameOpts = this.requireGameOpts();\n        const slotsInfo = this.requireSlotsInfo();\n        const observerActive = this.isHumanObserver();\n        const playerSlots = new Array(8).fill(undefined);\n        let remaining = observerActive ? gameOpts.maxSlots + 1 : gameOpts.maxSlots;\n\n        slotsInfo.forEach((_, slotIndex) => {\n            if (remaining) {\n                remaining -= 1;\n                playerSlots[slotIndex] = {\n                    country: RANDOM_COUNTRY_NAME,\n                    color: RANDOM_COLOR_NAME,\n                    startPos: RANDOM_START_POS,\n                    team: NO_TEAM_ID,\n                };\n            }\n        });\n\n        slotsInfo.forEach((slot, slotIndex) => {\n            if (!playerSlots[slotIndex]) {\n                return;\n            }\n            const playerSlot = playerSlots[slotIndex];\n            if (slot.type === NetSlotType.Closed) {\n                playerSlot.occupation = SlotOccupation.Closed;\n            }\n            else if (slot.type === NetSlotType.Open || slot.type === NetSlotType.OpenObserver) {\n                playerSlot.occupation = SlotOccupation.Open;\n            }\n            else {\n                playerSlot.occupation = SlotOccupation.Occupied;\n            }\n\n            if (slot.type === NetSlotType.Ai) {\n                playerSlot.aiDifficulty = slot.difficulty;\n                playerSlot.customBotId = slot.customBotId;\n                playerSlot.type = UiSlotType.Ai;\n            }\n            else if (slot.type === NetSlotType.Player) {\n                playerSlot.name = slot.name;\n                playerSlot.type = UiSlotType.Player;\n            }\n\n            playerSlot.status = PlayerStatus.NotReady;\n        });\n\n        const mpDialogSettings = this.getEffectiveMpDialogSettings(gameOpts.gameMode);\n        playerSlots.forEach((playerSlot: any, slotIndex: number) => {\n            if (!playerSlot) {\n                return;\n            }\n\n            if (playerSlot.occupation === SlotOccupation.Occupied) {\n                const human = gameOpts.humanPlayers.find((player) => player.name === playerSlot.name);\n                if (human) {\n                    playerSlot.country = this.getCountryNameById(human.countryId);\n                    playerSlot.color = this.getColorNameById(human.colorId);\n                    playerSlot.startPos = human.startPos;\n                    playerSlot.team = human.teamId;\n                }\n                else {\n                    const ai = gameOpts.aiPlayers[slotIndex];\n                    if (ai) {\n                        playerSlot.country = this.getCountryNameById(ai.countryId);\n                        playerSlot.color = this.getColorNameById(ai.colorId);\n                        playerSlot.startPos = ai.startPos;\n                        playerSlot.team = ai.teamId;\n                    }\n                }\n            }\n            else {\n                playerSlot.country = RANDOM_COUNTRY_NAME;\n                playerSlot.team = mpDialogSettings.mustAlly ? 0 : NO_TEAM_ID;\n            }\n\n            decoratePlayerSlot?.(playerSlot, slotsInfo[slotIndex], slotIndex);\n        });\n\n        return playerSlots;\n    }\n\n    private getSelectablePlayerColors(playerSlots: any[]): string[] {\n        const usedColors: string[] = [];\n        playerSlots.forEach((slot) => {\n            if (slot) {\n                usedColors.push(slot.color);\n            }\n        });\n        const availableColors = this.getAvailablePlayerColors();\n        return [RANDOM_COLOR_NAME].concat(availableColors.filter((color) => color && !usedColors.includes(color)));\n    }\n\n    private getSelectableStartPositions(playerSlots: any[], maxSlots: number): number[] {\n        const usedPositions: number[] = [];\n        playerSlots.forEach((slot) => {\n            if (slot) {\n                usedPositions.push(slot.startPos);\n            }\n        });\n        const positions = this.getAvailableStartPositionsForMax(maxSlots);\n        return [RANDOM_START_POS].concat(positions.filter((position) => !usedPositions.includes(position)));\n    }\n\n    private saveBotSettings(): void {\n        const aiPlayers = this.requireGameOpts().aiPlayers;\n        const hasCustomBotId = aiPlayers.some(ai => ai?.customBotId);\n        if (hasCustomBotId) {\n            this.localPrefs.setItem(StorageKey.LastBots, JSON.stringify(\n                aiPlayers.map(ai => ai\n                    ? { d: ai.difficulty, b: ai.customBotId, c: ai.countryId, co: ai.colorId, s: ai.startPos, t: ai.teamId }\n                    : null\n                )\n            ));\n        } else {\n            this.localPrefs.setItem(StorageKey.LastBots, new Serializer().serializeAiOpts(aiPlayers));\n        }\n    }\n\n    private parseSavedBots(data: string): (any | undefined)[] {\n        if (data.startsWith('[')) {\n            try {\n                const parsed = JSON.parse(data);\n                return parsed.map((item: any) =>\n                    item ? { difficulty: item.d, customBotId: item.b, countryId: item.c, colorId: item.co, startPos: item.s, teamId: item.t } : undefined\n                );\n            } catch {\n                return new Parser().parseAiOpts(data);\n            }\n        }\n        return new Parser().parseAiOpts(data);\n    }\n\n    private savePreferences(): void {\n        if (!this.preferredHostOpts) {\n            return;\n        }\n        this.localPrefs.setItem(StorageKey.PreferredGameOpts, this.preferredHostOpts.applyGameOpts(this.requireGameOpts()).serialize());\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lobby/SelectMapParams.ts",
    "content": "export {};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lobby/SkirmishScreen.ts",
    "content": "import { LobbyForm } from '@/gui/screen/mainMenu/lobby/component/LobbyForm';\nimport { LobbyType } from '@/gui/screen/mainMenu/lobby/component/viewmodel/lobby';\nimport { MainMenuScreenType } from '../../ScreenType';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { jsx } from '@/gui/jsx/jsx';\nimport { HtmlView } from '@/gui/jsx/HtmlView';\nimport { MapPreviewRenderer } from '@/gui/screen/mainMenu/lobby/MapPreviewRenderer';\nimport { StorageKey } from '@/LocalPrefs';\nimport { isNotNullOrUndefined } from '@/util/typeGuard';\nimport { MainMenuScreen } from '@/gui/screen/mainMenu/MainMenuScreen';\nimport { MainMenuRoute } from '@/gui/screen/mainMenu/MainMenuRoute';\nimport { MusicType } from '@/engine/sound/Music';\nimport { OBS_COUNTRY_ID } from '@/game/gameopts/constants';\nimport { MapFile } from '@/data/MapFile';\nimport { PregameController, PregameMapSelectionResult } from '@/gui/screen/mainMenu/lobby/PregameController';\nimport { BotRegistry } from '@/game/ai/thirdpartbot/BotRegistry';\n\ninterface GameMode {\n    id: number;\n    label: string;\n    mpDialogSettings: any;\n}\n\ninterface GameModes {\n    getAll(): GameMode[];\n    getById(id: number): GameMode;\n}\n\ninterface MapListEntry {\n    fileName: string;\n    maxSlots: number;\n    getFullMapTitle(strings: any): string;\n}\n\ninterface MapList {\n    getAll(): MapListEntry[];\n    getByName(name: string): MapListEntry;\n}\n\ninterface MapFileLoader {\n    load(mapName: string): Promise<any>;\n}\n\ninterface RootController {\n    createGame(gameId: string, timestamp: number, gservUrl: string, username: string, gameOpts: any, singlePlayer: boolean, tournament: boolean, mapTransfer: boolean, privateGame: boolean, fallbackRoute: MainMenuRoute): void;\n}\n\ninterface ErrorHandler {\n    handle(error: any, message: string, onClose: () => void): void;\n}\n\ninterface MessageBoxApi {\n    show(message: string, buttonText?: string, onClose?: () => void): void;\n    confirm(message: string, confirmText: string, cancelText: string): Promise<boolean>;\n}\n\ninterface LocalPrefs {\n    getItem(key: string): string | undefined;\n    setItem(key: string, value: string): void;\n    removeItem(key: string): void;\n}\n\ninterface Rules {\n    getMultiplayerCountries(): any[];\n    getMultiplayerColors(): Map<number, any>;\n    mpDialogSettings: any;\n    general?: any;\n}\n\ninterface SkirmishUnstackParams extends PregameMapSelectionResult {\n}\n\nexport class SkirmishScreen extends MainMenuScreen {\n    declare public musicType: MusicType;\n\n    private playerName: string = 'Player 1';\n    private disposables: CompositeDisposable = new CompositeDisposable();\n    private pregameController?: PregameController;\n    private lobbyForm?: any;\n\n    constructor(\n        private readonly rootController: RootController,\n        private readonly errorHandler: ErrorHandler,\n        private readonly messageBoxApi: MessageBoxApi,\n        private readonly strings: any,\n        private readonly rules: Rules,\n        private readonly jsxRenderer: any,\n        private readonly mapFileLoader: MapFileLoader,\n        private readonly mapList: MapList,\n        private readonly gameModes: GameModes,\n        private readonly localPrefs: LocalPrefs\n    ) {\n        super();\n        this.title = this.strings.get('GUI:SkirmishGame');\n        this.musicType = MusicType.Intro;\n    }\n\n    onEnter(): void {\n        this.controller.toggleMainVideo(false);\n        this.lobbyForm = undefined;\n        this.loadPersistedBots();\n        this.pregameController = new PregameController(\n            this.strings,\n            this.rules,\n            this.mapFileLoader,\n            this.mapList,\n            this.gameModes,\n            this.localPrefs,\n            this.playerName\n        );\n        void this.createGame();\n    }\n\n    onViewportChange(): void {\n    }\n\n    async onStack(): Promise<void> {\n        await this.unrender();\n    }\n\n    onUnstack(params?: SkirmishUnstackParams): void {\n        if (params) {\n            this.pregameController?.applyMapSelection(params);\n        }\n        this.updateMapPreview();\n        this.initView();\n    }\n\n    async onLeave(): Promise<void> {\n        this.disposables.dispose();\n        this.pregameController = undefined;\n        const debugRoot = (window as any).__ra2debug;\n        if (debugRoot) {\n            delete debugRoot.skirmishLobby;\n        }\n        this.controller.toggleSidebarPreview(false);\n        await this.unrender();\n    }\n\n    private async createGame(): Promise<void> {\n        const controller = this.pregameController;\n        if (!controller) {\n            return;\n        }\n\n        try {\n            await controller.initialize();\n        }\n        catch (error) {\n            this.handleError(\n                error,\n                (error as any)?.name === 'DownloadError'\n                    ? this.strings.get('TXT_DOWNLOAD_FAILED')\n                    : this.strings.get('WOL:MatchErrorCreatingGame')\n            );\n            return;\n        }\n\n        // Bail out if the screen was left or re-entered during initialization\n        if (this.pregameController !== controller) {\n            return;\n        }\n\n        this.updateMapPreview();\n        this.initView();\n    }\n\n    private initView(): void {\n        this.initLobbyForm();\n        this.refreshSidebarButtons();\n        this.refreshSidebarMpText();\n        this.controller.showSidebarButtons();\n    }\n\n    private buildFormProps(): any {\n        const pregameController = this.requirePregameController();\n        return pregameController.createLobbyFormProps({\n            lobbyType: LobbyType.Singleplayer,\n            activeSlotIndex: 0,\n            onStateChange: () => {\n                this.updateMapPreview();\n                this.refreshSidebarMpText();\n                this.refreshLobbyForm();\n            },\n        });\n    }\n\n    private initLobbyForm(): void {\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            innerRef: (ref: any) => (this.lobbyForm = ref),\n            component: LobbyForm,\n            props: this.buildFormProps(),\n        }));\n        this.controller.setMainComponent(component);\n        this.syncDebugState();\n    }\n\n    private refreshLobbyForm(): void {\n        const formProps = this.buildFormProps();\n        if (this.lobbyForm) {\n            this.lobbyForm.applyOptions((options: any) => {\n                Object.assign(options, formProps);\n            });\n        }\n        this.syncDebugState();\n    }\n\n    private refreshSidebarButtons(): void {\n        this.controller.setSidebarButtons([\n            {\n                label: this.strings.get('GUI:StartGame'),\n                tooltip: this.strings.get('STT:SkirmishButtonStartGame'),\n                onClick: () => this.handleStartGame(),\n            },\n            {\n                label: this.strings.get('GUI:ChooseMap'),\n                tooltip: this.strings.get('STT:SkirmishButtonChooseMap'),\n                onClick: () => {\n                    const pregameController = this.requirePregameController();\n                    this.controller?.pushScreen(MainMenuScreenType.MapSelection, {\n                        lobbyType: LobbyType.Singleplayer,\n                        gameOpts: pregameController.getGameOpts(),\n                        usedSlots: () => pregameController.getUsedSlots(),\n                    });\n                },\n            },\n            {\n                label: this.strings.get('GUI:BotUpload') || 'Upload AI Bot',\n                tooltip: this.strings.get('STT:SkirmishButtonUploadBot') || 'Upload a custom AI bot script package',\n                onClick: () => this.showBotUploadDialog(),\n            },\n            {\n                label: this.strings.get('GUI:Back'),\n                tooltip: this.strings.get('STT:SkirmishButtonBack'),\n                isBottom: true,\n                onClick: () => this.controller?.goToScreen(MainMenuScreenType.Home),\n            },\n        ], true);\n    }\n\n    private refreshSidebarMpText(): void {\n        const pregameController = this.pregameController;\n        if (!pregameController) {\n            this.controller.setSidebarMpContent({ text: '' });\n            return;\n        }\n\n        const gameOpts = pregameController.getGameOpts();\n        this.controller.setSidebarMpContent({\n            text: this.strings.get(this.gameModes.getById(gameOpts.gameMode).label) + '\\n\\n' + gameOpts.mapTitle,\n            icon: gameOpts.mapOfficial ? 'gt18.pcx' : 'settings.png',\n            tooltip: gameOpts.mapOfficial\n                ? this.strings.get('STT:VerifiedMap')\n                : this.strings.get('STT:UnverifiedMap'),\n        });\n    }\n\n    private updateMapPreview(): void {\n        try {\n            const currentMapFile = this.requirePregameController().getCurrentMapFile();\n            const preview = new MapPreviewRenderer(this.strings).render(\n                new MapFile(currentMapFile),\n                LobbyType.Singleplayer,\n                this.controller.getSidebarPreviewSize()\n            );\n            this.controller.toggleSidebarPreview(true);\n            this.controller.setSidebarPreview(preview);\n        }\n        catch (error) {\n            console.error('Failed to render map preview');\n            console.error(error);\n            this.controller.setSidebarPreview();\n        }\n    }\n\n    private handleStartGame(): void {\n        const pregameController = this.requirePregameController();\n        const gameOpts = pregameController.getGameOpts();\n        const aiCount = gameOpts.aiPlayers.filter(isNotNullOrUndefined).length;\n        const humanIsObserver = gameOpts.humanPlayers.length > 0 &&\n            gameOpts.humanPlayers[0].countryId === OBS_COUNTRY_ID;\n        const minAiRequired = humanIsObserver ? 2 : 1;\n\n        if (aiCount < minAiRequired) {\n            this.messageBoxApi.show(this.strings.get('TXT_NEED_AT_LEAST_TWO_PLAYERS'), this.strings.get('GUI:Ok'));\n            return;\n        }\n\n        if (!pregameController.meetsMinimumTeams()) {\n            this.messageBoxApi.show(this.strings.get('TXT_CANNOT_ALLY'), this.strings.get('GUI:Ok'));\n            return;\n        }\n\n        const gameId = '0';\n        const timestamp = Date.now();\n        const fallbackRoute = new MainMenuRoute(MainMenuScreenType.Skirmish, {});\n        this.rootController.createGame(gameId, timestamp, '', this.playerName, gameOpts, true, false, false, false, fallbackRoute);\n    }\n\n    private syncDebugState(): void {\n        const debugRoot = ((window as any).__ra2debug ??= {});\n        const snapshot = this.pregameController?.getSnapshot();\n        const formProps = this.pregameController ? this.buildFormProps() : undefined;\n        debugRoot.skirmishLobby = {\n            gameOpts: snapshot?.gameOpts,\n            slotsInfo: snapshot?.slotsInfo,\n            formModel: formProps\n                ? {\n                    playerSlots: JSON.parse(JSON.stringify(formProps.playerSlots ?? [])),\n                    availablePlayerCountries: [...(formProps.availablePlayerCountries ?? [])],\n                    availablePlayerColors: [...(formProps.availablePlayerColors ?? [])],\n                    availableStartPositions: [...(formProps.availableStartPositions ?? [])],\n                    teamsAllowed: formProps.teamsAllowed,\n                    teamsRequired: formProps.teamsRequired,\n                    gameSpeed: formProps.gameSpeed,\n                    credits: formProps.credits,\n                    unitCount: formProps.unitCount,\n                }\n                : undefined,\n            startGame: () => this.handleStartGame(),\n        };\n    }\n\n    private handleError(error: any, message: string): void {\n        this.errorHandler.handle(error, message, () => {\n            this.controller?.goToScreen(MainMenuScreenType.Home);\n        });\n    }\n\n    private requirePregameController(): PregameController {\n        if (!this.pregameController) {\n            throw new Error('Pregame controller is not initialized');\n        }\n        return this.pregameController;\n    }\n\n    private showBotUploadDialog(): void {\n        const overlay = document.createElement('div');\n        overlay.className = 'bot-upload-dialog-overlay';\n        overlay.innerHTML = `\n            <div class=\"bot-upload-dialog\" onclick=\"event.stopPropagation()\">\n                <div class=\"bot-upload-header\">\n                    <h3>${this.strings.get('GUI:BotUpload:Title') || 'Upload AI Bot Script'}</h3>\n                    <button class=\"bot-upload-close\" id=\"bot-upload-close-btn\">×</button>\n                </div>\n                <div class=\"bot-upload-body\">\n                    <div class=\"bot-upload-section\">\n                        <label class=\"bot-upload-label\">${this.strings.get('GUI:BotUpload:Select') || 'Select Bot Zip File'}</label>\n                        <input type=\"file\" accept=\".zip\" class=\"bot-upload-input\" id=\"bot-upload-file\" />\n                        <div class=\"bot-upload-hint\">${this.strings.get('GUI:BotUpload:Hint') || 'Upload a .zip file containing bot.ts or index.ts'}</div>\n                    </div>\n                    <div id=\"bot-upload-message\"></div>\n                    <div class=\"bot-upload-section\">\n                        <h4>${this.strings.get('GUI:BotUpload:Manage') || 'Manage Bots'}</h4>\n                        <div id=\"bot-upload-list\"></div>\n                    </div>\n                </div>\n                <div class=\"bot-upload-footer\">\n                    <button class=\"dialog-button\" id=\"bot-upload-ok-btn\">${this.strings.get('GUI:Ok') || 'OK'}</button>\n                </div>\n            </div>\n        `;\n\n        const closeDialog = () => {\n            overlay.remove();\n        };\n\n        overlay.addEventListener('click', (event) => {\n            if (event.target === overlay) {\n                closeDialog();\n            }\n        });\n\n        document.getElementById('ra2web-root')?.appendChild(overlay);\n        document.getElementById('bot-upload-close-btn')?.addEventListener('click', closeDialog);\n        document.getElementById('bot-upload-ok-btn')?.addEventListener('click', closeDialog);\n\n        const fileInput = document.getElementById('bot-upload-file') as HTMLInputElement;\n        fileInput?.addEventListener('change', async () => {\n            const file = fileInput.files?.[0];\n            if (!file) {\n                return;\n            }\n\n            const messageDiv = document.getElementById('bot-upload-message');\n            if (messageDiv) {\n                messageDiv.innerHTML = '<div class=\"bot-upload-status\">Loading...</div>';\n            }\n\n            try {\n                const { BotUploader } = await import('@/game/ai/thirdpartbot/BotUploader');\n                const result = await BotUploader.processUpload(file);\n\n                if (result.success && result.meta && messageDiv) {\n                    messageDiv.innerHTML = `<div class=\"bot-upload-message bot-upload-message-success\">${this.strings.get('GUI:BotUpload:Success') || 'Bot uploaded successfully!'}</div>`;\n                    this.persistBots();\n                    this.refreshBotList();\n                    this.refreshLobbyForm();\n                }\n                else if (messageDiv) {\n                    messageDiv.innerHTML = `<div class=\"bot-upload-message bot-upload-message-error\">${(result.errors || ['Upload failed']).join('\\n')}</div>`;\n                }\n            }\n            catch (error) {\n                if (messageDiv) {\n                    messageDiv.innerHTML = `<div class=\"bot-upload-message bot-upload-message-error\">Error: ${(error as Error).message}</div>`;\n                }\n            }\n\n            fileInput.value = '';\n        });\n\n        this.refreshBotList();\n    }\n\n    private refreshBotList(): void {\n        const listDiv = document.getElementById('bot-upload-list');\n        if (!listDiv) {\n            return;\n        }\n\n        import('@/game/ai/thirdpartbot/BotRegistry').then(({ BotRegistry }) => {\n            const bots = BotRegistry.getInstance().getUploadedBots();\n            if (bots.length === 0) {\n                listDiv.innerHTML = `<div class=\"bot-upload-empty\">${this.strings.get('GUI:BotUpload:NoBot') || 'No custom bots uploaded'}</div>`;\n                return;\n            }\n\n            listDiv.innerHTML = bots.map((bot) => `\n                <div class=\"bot-upload-item\">\n                    <div class=\"bot-upload-item-info\">\n                        <span class=\"bot-upload-item-name\">${bot.displayName}</span>\n                        <span class=\"bot-upload-item-version\">v${bot.version}</span>\n                        <span class=\"bot-upload-item-author\">by ${bot.author}</span>\n                    </div>\n                    <button class=\"bot-upload-item-remove\" data-bot-id=\"${bot.id}\">${this.strings.get('GUI:BotUpload:Remove') || 'Remove'}</button>\n                </div>\n            `).join('');\n\n            listDiv.querySelectorAll('.bot-upload-item-remove').forEach((button) => {\n                button.addEventListener('click', () => {\n                    const botId = (button as HTMLElement).dataset.botId;\n                    if (botId) {\n                        BotRegistry.getInstance().unregister(botId);\n                        this.persistBots();\n                        this.refreshBotList();\n                        this.refreshLobbyForm();\n                    }\n                });\n            });\n        });\n    }\n\n    private loadPersistedBots(): void {\n        const data = this.localPrefs.getItem(StorageKey.UploadedBots);\n        if (data) {\n            BotRegistry.getInstance().loadPersistedBots(data);\n        }\n    }\n\n    private persistBots(): void {\n        this.localPrefs.setItem(StorageKey.UploadedBots, BotRegistry.getInstance().serializeUploadedBots());\n    }\n\n    private async unrender(): Promise<void> {\n        await this.controller.hideSidebarButtons();\n        this.lobbyForm = undefined;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lobby/component/CreateGameBox.tsx",
    "content": "import React, { useState, useRef } from 'react';\nimport { Dialog } from '@/gui/component/Dialog';\nimport { WolConnection } from '@/network/WolConnection';\ninterface CreateGameBoxProps {\n    strings: {\n        get: (key: string) => string;\n    };\n    viewport: any;\n    onSubmit: (roomName: string, password: string, observe: boolean) => void;\n    onDismiss?: () => void;\n}\nexport const CreateGameBox: React.FC<CreateGameBoxProps> = ({ strings, viewport, onSubmit, onDismiss }) => {\n    const [hidden, setHidden] = useState(false);\n    const [roomName, setRoomName] = useState('');\n    const passwordRef = useRef<HTMLInputElement>(null);\n    const submitRef = useRef<HTMLButtonElement>(null);\n    const [enablePassword, setEnablePassword] = useState(false);\n    const observeRef = useRef<HTMLInputElement>(null);\n    return (<Dialog className=\"login-box create-game-box\" hidden={hidden} viewport={viewport} buttons={[\n            {\n                label: strings.get(\"GUI:Ok\"),\n                onClick: () => submitRef.current?.click(),\n            },\n            {\n                label: strings.get(\"GUI:Cancel\"),\n                onClick: () => {\n                    setHidden(true);\n                    onDismiss?.();\n                },\n            },\n        ]} zIndex={100}>\n      <form onSubmit={(e) => {\n            e.preventDefault();\n            setHidden(true);\n            onSubmit(roomName, enablePassword ? passwordRef.current?.value || '' : '', observeRef.current?.checked || false);\n        }} autoComplete=\"off\">\n        <div className=\"field\">\n          <label>{strings.get(\"GUI:RoomDesc\")}</label>\n          <input name=\"roomname\" type=\"text\" value={roomName} maxLength={WolConnection.MAX_ROOM_DESC_LEN} onChange={(e) => setRoomName(e.target.value)}/>\n        </div>\n        <div className=\"field\">\n          <label>{strings.get(\"GUI:Password\")}</label>\n          <input name=\"enablepass\" type=\"checkbox\" checked={enablePassword} onChange={() => setEnablePassword(!enablePassword)}/>\n          <input name=\"lobbypass\" type=\"password\" autoComplete=\"off\" data-lpignore=\"true\" ref={passwordRef} disabled={!enablePassword} required={enablePassword}/>\n        </div>\n        <div className=\"field\">\n          <label>{strings.get(\"GUI:Observe\")}</label>\n          <input type=\"checkbox\" name=\"test\" ref={observeRef}/>\n        </div>\n        <button type=\"submit\" ref={submitRef} style={{ visibility: \"hidden\" }}/>\n      </form>\n    </Dialog>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lobby/component/LobbyForm.tsx",
    "content": "import React from \"react\";\nimport classnames from \"classnames\";\nimport { LobbyType, SlotOccupation, SlotType, PlayerStatus, } from \"@/gui/screen/mainMenu/lobby/component/viewmodel/lobby\";\nimport { Slider } from \"@/gui/component/Slider\";\nimport { Chat } from \"@/gui/component/Chat\";\nimport { CountrySelect } from \"@/gui/component/CountrySelect\";\nimport { ColorSelect } from \"@/gui/component/ColorSelect\";\nimport { PingIndicator } from \"@/gui/component/PingIndicator\";\nimport { AiDifficulty } from \"@/game/gameopts/GameOpts\";\nimport { Image } from \"@/gui/component/Image\";\nimport { StartPosSelect } from \"@/gui/component/StartPosSelect\";\nimport { TeamSelect } from \"@/gui/component/TeamSelect\";\nimport { NO_TEAM_ID, OBS_TEAM_ID, OBS_COUNTRY_NAME, aiUiTooltips } from \"@/game/gameopts/constants\";\nimport { Select } from \"@/gui/component/Select\";\nimport { Option } from \"@/gui/component/Option\";\nimport { isNotNullOrUndefined } from \"@/util/typeGuard\";\nimport { RankIndicator } from \"@/gui/screen/mainMenu/lobby/component/RankIndicator\";\ninterface LobbyFormProps {\n    strings: any;\n    lobbyType: LobbyType;\n    mpDialogSettings: any;\n    selectedGameServer?: string;\n    playerSlots: any[];\n    shortGame: boolean;\n    mcvRepacks: boolean;\n    cratesAppear: boolean;\n    superWeapons: boolean;\n    hostTeams?: boolean;\n    destroyableBridges: boolean;\n    multiEngineer: boolean;\n    multiEngineerCount?: number;\n    noDogEngiKills: boolean;\n    gameSpeed: number;\n    credits: number;\n    unitCount: number;\n    buildOffAlly: boolean;\n    messages?: any[];\n    localUsername?: string;\n    channels?: any[];\n    chatHistory?: any;\n    activeSlotIndex: number;\n    countryUiNames: any;\n    countryUiTooltips: any;\n    availablePlayerCountries: any;\n    availablePlayerColors: any;\n    availableStartPositions: any;\n    teamsAllowed: boolean;\n    teamsRequired: boolean;\n    maxTeams: number;\n    availableAiNames: any;\n    onSlotChange: (occupation: number, slotIndex: number, aiDifficulty?: any, customBotId?: string) => void;\n    onToggleShortGame: (checked: boolean) => void;\n    onToggleMcvRepacks: (checked: boolean) => void;\n    onToggleCratesAppear: (checked: boolean) => void;\n    onToggleSuperWeapons: (checked: boolean) => void;\n    onToggleHostTeams?: (checked: boolean) => void;\n    onToggleDestroyableBridges?: (checked: boolean) => void;\n    onToggleMultiEngineer?: (checked: boolean) => void;\n    onToggleNoDogEngiKills?: (checked: boolean) => void;\n    onChangeGameSpeed: (value: number) => void;\n    onChangeCredits: (value: number) => void;\n    onChangeUnitCount: (value: number) => void;\n    onToggleBuildOffAlly: (checked: boolean) => void;\n    onSendMessage?: (message: string) => void;\n    onCountrySelect: (country: any, slotIndex: number) => void;\n    onColorSelect: (color: any, slotIndex: number) => void;\n    onStartPosSelect: (pos: any, slotIndex: number) => void;\n    onTeamSelect: (team: any, slotIndex: number) => void;\n    beforeChatContent?: React.ReactNode;\n}\nexport class LobbyForm extends React.Component<LobbyFormProps> {\n    private getFirstAvailableAiDifficulty(): AiDifficulty {\n        const iterator = this.props.availableAiNames.keys();\n        const first = iterator.next();\n        if (first.done) return AiDifficulty.Easy;\n        const key = first.value as string;\n        if (key.startsWith('Custom:')) return AiDifficulty.Custom;\n        return AiDifficulty[key as keyof typeof AiDifficulty] ?? AiDifficulty.Easy;\n    }\n    onPlayerSelect = (value: string, slotIndex: number) => {\n        let occupation: number;\n        let aiDifficulty: any;\n        let customBotId: string | undefined;\n        if (value.match(/^\\d+$/)) {\n            occupation = Number(value);\n        }\n        else {\n            occupation = SlotOccupation.Occupied;\n            if (value.startsWith('Custom:')) {\n                aiDifficulty = AiDifficulty.Custom;\n                customBotId = value.slice('Custom:'.length);\n            } else {\n                aiDifficulty = AiDifficulty[value as keyof typeof AiDifficulty];\n                if (aiDifficulty === undefined ||\n                    !this.props.availableAiNames.has(value)) {\n                    const firstKey = this.props.availableAiNames.keys().next().value;\n                    if (firstKey?.startsWith('Custom:')) {\n                        aiDifficulty = AiDifficulty.Custom;\n                        customBotId = firstKey.slice('Custom:'.length);\n                    } else {\n                        aiDifficulty = this.getFirstAvailableAiDifficulty();\n                    }\n                }\n            }\n        }\n        this.props.onSlotChange(occupation, slotIndex, aiDifficulty, customBotId);\n    };\n    render() {\n        const { strings, lobbyType, mpDialogSettings } = this.props;\n        const isHost = lobbyType === LobbyType.Singleplayer ||\n            lobbyType === LobbyType.MultiplayerHost;\n        const isSingleplayer = lobbyType === LobbyType.Singleplayer;\n        const props = this.props;\n        return (<div className={classnames(\"lobby-form\", {\n                \"lobby-form-sp\": isSingleplayer,\n                \"lobby-form-server-sel\": props.selectedGameServer,\n            })}>\n        {props.selectedGameServer && (<div className=\"game-server\">\n            <span className=\"label\">{strings.get(\"TS:ServerLabel\")}</span>\n            <Select initialValue={props.selectedGameServer} disabled={true}>\n              <Option label={props.selectedGameServer} value={props.selectedGameServer}/>\n            </Select>\n          </div>)}\n        \n        <div className=\"player-slots\">\n          <div className=\"player-slot player-slot-header\">\n            <div className=\"player-header-players\">\n              {strings.get(\"GUI:Players\")}\n            </div>\n            <div className=\"player-header-side\">\n              {strings.get(\"GUI:Side\")}\n            </div>\n            <div className=\"player-header-color\">\n              {strings.get(\"GUI:Color\")}\n            </div>\n            <div className=\"player-header-position\">\n              {strings.get(\"GUI:StartPosition\")}\n            </div>\n            <div className=\"player-header-team\">\n              {strings.get(\"GUI:Team\")}\n            </div>\n          </div>\n          {props.playerSlots.map((slot, index) => this.renderPlayerSlot(props, slot, index))}\n        </div>\n\n        <div className=\"game-options\">\n          <div className=\"game-options-left\">\n            <div data-r-tooltip={strings.get(\"STT:HostCBoxShortGame\")}>\n              <label>\n                <input type=\"checkbox\" name=\"shortGame\" checked={props.shortGame} onChange={(e) => this.props.onToggleShortGame(e.target.checked)} disabled={!isHost}/>{\" \"}\n                <span>{strings.get(\"GUI:ShortGame\")}</span>\n              </label>\n            </div>\n            <div data-r-tooltip={strings.get(\"STT:HostCBoxRedeploys\")}>\n              <label>\n                <input type=\"checkbox\" name=\"mcvRepacks\" checked={props.mcvRepacks} onChange={(e) => this.props.onToggleMcvRepacks(e.target.checked)} disabled={!isHost}/>{\" \"}\n                <span>{strings.get(\"GUI:MCVRepacks\")}</span>\n              </label>\n            </div>\n            <div data-r-tooltip={strings.get(\"STT:HostCBoxCrates\")}>\n              <label>\n                <input type=\"checkbox\" name=\"cratesAppear\" checked={props.cratesAppear} onChange={(e) => this.props.onToggleCratesAppear(e.target.checked)} disabled={!isHost}/>{\" \"}\n                <span>{strings.get(\"GUI:CratesAppear\")}</span>\n              </label>\n            </div>\n            <div data-r-tooltip={strings.get(\"STT:HostCBoxSWAllowed\")}>\n              <label>\n                <input type=\"checkbox\" name=\"superWeapons\" checked={props.superWeapons} onChange={(e) => this.props.onToggleSuperWeapons(e.target.checked)} disabled={!isHost}/>{\" \"}\n                <span>{strings.get(\"GUI:SuperWeaponsAllowed\")}</span>\n              </label>\n            </div>\n            {props.lobbyType !== LobbyType.Singleplayer && props.hostTeams !== undefined && (<div data-r-tooltip={strings.get(\"STT:HostCBoxHostTeams\")}>\n                <label>\n                  <input type=\"checkbox\" name=\"hostTeams\" checked={props.hostTeams} onChange={(e) => this.props.onToggleHostTeams?.(e.target.checked)} disabled={!isHost}/>{\" \"}\n                  <span>{strings.get(\"GUI:HostTeams\")}</span>\n                </label>\n              </div>)}\n            <div data-r-tooltip={strings.get(\"STT:DestroyableBridges\")}>\n              <label>\n                <input type=\"checkbox\" name=\"destBridges\" checked={props.destroyableBridges} onChange={(e) => this.props.onToggleDestroyableBridges?.(e.target.checked)} disabled={!isHost}/>{\" \"}\n                <span>{strings.get(\"GUI:DestroyableBridges\")}</span>\n              </label>\n            </div>\n            <div data-r-tooltip={strings.get(\"STT:MultiEngineer\", props.multiEngineerCount)}>\n              <label>\n                <input type=\"checkbox\" name=\"multiEngineer\" checked={props.multiEngineer} onChange={(e) => this.props.onToggleMultiEngineer?.(e.target.checked)} disabled={!isHost}/>{\" \"}\n                <span>{strings.get(\"GUI:MultiEngineer\")}</span>\n              </label>\n            </div>\n            <div data-r-tooltip={strings.get(\"STT:NoDogEngiKills\")}>\n              <label>\n                <input type=\"checkbox\" name=\"noDogEngiKills\" checked={props.noDogEngiKills} onChange={(e) => this.props.onToggleNoDogEngiKills?.(e.target.checked)} disabled={!isHost}/>{\" \"}\n                <span>{strings.get(\"GUI:NoDogEngiKills\")}</span>\n              </label>\n            </div>\n          </div>\n          \n          <div className={\"game-options-right\" + (isHost ? \"\" : \" all-disabled\")}>\n            <div className=\"slider-item\">\n              <span className=\"label\">{strings.get(\"GUI:GameSpeed\")}</span>\n              <Slider name=\"gameSpeed\" min={0} max={6} value={\"\" + props.gameSpeed} disabled={!isHost} data-r-tooltip={strings.get(\"STT:HostSliderSpeed\")} onChange={(e) => this.props.onChangeGameSpeed(Number(e.target.value))}/>\n            </div>\n            <div className=\"slider-item\">\n              <span className=\"label\">{strings.get(\"GUI:Credits\")}</span>\n              <Slider name=\"credits\" min={mpDialogSettings.minMoney} max={mpDialogSettings.maxMoney} step={mpDialogSettings.moneyIncrement} value={\"\" + props.credits} data-r-tooltip={strings.get(\"STT:HostSliderCredits\")} onChange={(e) => this.props.onChangeCredits(Number(e.target.value))} disabled={!isHost}/>\n            </div>\n            <div className=\"slider-item\">\n              <span className=\"label\">{strings.get(\"GUI:UnitCount\")}</span>\n              <Slider name=\"unitCount\" min={mpDialogSettings.minUnitCount} max={mpDialogSettings.maxUnitCount} value={\"\" + props.unitCount} data-r-tooltip={strings.get(\"STT:HostSliderUnit\")} onChange={(e) => this.props.onChangeUnitCount(Number(e.target.value))} disabled={!isHost}/>\n            </div>\n            <div className=\"checkbox-item\" data-r-tooltip={strings.get(\"STT:HostCBoxBuildOffAlly\")}>\n              <label>\n                <input type=\"checkbox\" name=\"buildOffAlly\" checked={props.buildOffAlly} disabled={!isHost} onChange={(e) => this.props.onToggleBuildOffAlly(e.target.checked)}/>{\" \"}\n                <span>{strings.get(\"GUI:BuildOffAlly\")}</span>\n              </label>\n            </div>\n          </div>\n        </div>\n\n        {this.props.beforeChatContent !== undefined ? (<div className=\"lobby-form-before-chat\">{this.props.beforeChatContent}</div>) : null}\n\n        {this.props.messages !== undefined &&\n                this.props.localUsername !== undefined &&\n                this.props.onSendMessage && (<Chat messages={this.props.messages} localUsername={this.props.localUsername} channels={this.props.channels ?? []} chatHistory={this.props.chatHistory} onSendMessage={this.props.onSendMessage} onCancelMessage={() => { }} tooltips={{\n                    button: strings.get(\"STT:EmoteButton\"),\n                    input: isHost\n                        ? strings.get(\"STT:HostEditInput\")\n                        : strings.get(\"STT:GuestEditInput\"),\n                    output: isHost\n                        ? strings.get(\"STT:HostEditOutput\")\n                        : strings.get(\"STT:GuestEditOutput\"),\n                }} strings={strings}/>)}\n      </div>);\n    }\n    renderPlayerSlot(props: LobbyFormProps, slot: any, index: number) {\n        const strings = props.strings;\n        const isHost = props.lobbyType === LobbyType.Singleplayer ||\n            props.lobbyType === LobbyType.MultiplayerHost;\n        return slot ? (<div className=\"player-slot\" key={\"playerslot\" + index}>\n        {slot.type === SlotType.Player ? (<RankIndicator playerProfile={slot.playerProfile} strings={strings}/>) : (<RankIndicator playerProfile={undefined} strings={strings}/>)}\n        <PingIndicator ping={slot.type === SlotType.Player ? slot.ping : undefined} strings={strings}/>\n        <div className=\"player-status\" data-r-tooltip={strings.get(\"STT:HostPictureAcceptance\")}>\n          {this.renderPlayerStatus(slot.status)}\n        </div>\n        {this.renderPlayerSelect(slot, index, props.lobbyType)}\n        <CountrySelect countryUiNames={props.countryUiNames} countryUiTooltips={props.countryUiTooltips} country={slot.country} availableCountries={props.availablePlayerCountries} disabled={(index !== props.activeSlotIndex &&\n                (!isHost || slot.type !== SlotType.Ai)) ||\n                slot.type === SlotType.Observer ||\n                (slot.status === PlayerStatus.Ready &&\n                    slot.type !== SlotType.Ai)} strings={props.strings} onSelect={(country) => this.props.onCountrySelect(country, index)}/>\n        {slot.type !== SlotType.Observer ? (<>\n            <ColorSelect color={slot.color} availableColors={props.availablePlayerColors} disabled={(index !== props.activeSlotIndex &&\n                    (!isHost || slot.type !== SlotType.Ai)) ||\n                    (slot.status === PlayerStatus.Ready &&\n                        slot.type !== SlotType.Ai)} strings={props.strings} onSelect={(color) => this.props.onColorSelect(color, index)}/>\n            <StartPosSelect disabled={props.hostTeams\n                    ? !isHost || slot.occupation !== SlotOccupation.Occupied\n                    : (index !== props.activeSlotIndex &&\n                        (!isHost || slot.type !== SlotType.Ai)) ||\n                        (slot.status === PlayerStatus.Ready &&\n                            slot.type !== SlotType.Ai)} startPos={slot.startPos} availableStartPositions={props.availableStartPositions} onSelect={(pos) => this.props.onStartPosSelect(pos, index)} strings={props.strings}/>\n          </>) : null}\n            <TeamSelect disabled={!props.teamsAllowed ||\n                    (props.hostTeams\n                        ? !isHost || slot.occupation !== SlotOccupation.Occupied\n                        : (index !== props.activeSlotIndex &&\n                            (!isHost || slot.type !== SlotType.Ai)) ||\n                            (slot.status === PlayerStatus.Ready &&\n                                slot.type !== SlotType.Ai))} teamId={slot.type === SlotType.Observer || slot.country === OBS_COUNTRY_NAME ? OBS_TEAM_ID : (props.teamsAllowed ? slot.team : NO_TEAM_ID)} required={props.teamsRequired} maxTeams={props.maxTeams} showObserver={index === props.activeSlotIndex} onSelect={(team) => this.props.onTeamSelect(team, index)} strings={props.strings}/>\n      </div>) : (<div className=\"player-slot\" key={\"playerslot\" + index}/>);\n    }\n    renderPlayerStatus(status: any) {\n        return status === PlayerStatus.Host ? (<Image src=\"wolhost.pcx\"/>) : status === PlayerStatus.Ready ? (<Image src=\"wolacpt.pcx\"/>) : null;\n    }\n    renderPlayerSelect(slot: any, index: number, lobbyType: LobbyType) {\n        const isSingleplayer = lobbyType === LobbyType.Singleplayer;\n        const isHost = isSingleplayer || lobbyType === LobbyType.MultiplayerHost;\n        if (index === this.props.activeSlotIndex || (isHost && index === 0)) {\n            return (<input type=\"text\" className=\"player-name\" value={slot.name} readOnly={true}/>);\n        }\n        const strings = this.props.strings;\n        const displayOccupation = isSingleplayer && slot.occupation === SlotOccupation.Open\n            ? SlotOccupation.Closed\n            : slot.occupation;\n        const optionsMap = new Map()\n            .set(SlotOccupation.Occupied, slot.name || \"\")\n            .set(SlotOccupation.Open, strings.get(slot.type === SlotType.Observer\n            ? \"GUI:OpenObserver\"\n            : \"GUI:Open\"))\n            .set(SlotOccupation.Closed, isSingleplayer ? strings.get(\"GUI:None\") : strings.get(\"GUI:Closed\"));\n        let selectedValue = displayOccupation;\n        if (displayOccupation === SlotOccupation.Occupied &&\n            slot.type === SlotType.Ai) {\n            optionsMap.delete(SlotOccupation.Occupied);\n            if (slot.customBotId && this.props.availableAiNames.has(`Custom:${slot.customBotId}`)) {\n                selectedValue = `Custom:${slot.customBotId}`;\n            } else {\n                const diffKey = AiDifficulty[slot.aiDifficulty];\n                selectedValue = this.props.availableAiNames.has(diffKey)\n                    ? diffKey\n                    : [...this.props.availableAiNames.keys()][0] ?? 'Easy';\n            }\n        }\n        if (slot.type !== SlotType.Observer) {\n            this.props.availableAiNames.forEach((name: string, key: string) => {\n                const resolvedName = key.startsWith('Custom:') ? name : strings.get(name);\n                optionsMap.set(key, resolvedName);\n            });\n        }\n        return (<Select initialValue={\"\" + selectedValue} disabled={!isHost} onSelect={(value) => this.onPlayerSelect(value, index)} className=\"player-name\" tooltip={isSingleplayer\n                ? strings.get(\"STT:SkirmishComboAiPlayer\")\n                : strings.get(\"STT:HostComboPlayer\")}>\n        {[...optionsMap]\n                .map(([value, label]) => (value === SlotOccupation.Occupied &&\n                slot.occupation !== SlotOccupation.Occupied) ||\n                (value === SlotOccupation.Open && isSingleplayer)\n                ? null\n                : (<Option key={value} value={\"\" + value} label={label} tooltip={isSingleplayer\n                        ? (() => {\n                            let tooltipKey: string | undefined;\n                            if (value === SlotOccupation.Closed) {\n                                tooltipKey = \"STT:PlayerNone\";\n                            }\n                            else if (typeof value === 'string' && value.startsWith('Custom:')) {\n                                tooltipKey = undefined;\n                            }\n                            else {\n                                tooltipKey = aiUiTooltips.get(AiDifficulty[value as keyof typeof AiDifficulty] as AiDifficulty);\n                            }\n                            return tooltipKey ? strings.get(tooltipKey) : undefined;\n                        })()\n                        : undefined}/>))\n                .filter(isNotNullOrUndefined)}\n      </Select>);\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lobby/component/PasswordBox.tsx",
    "content": "import React, { useRef, useState, useEffect } from 'react';\nimport { Dialog } from '@/gui/component/Dialog';\ninterface PasswordBoxProps {\n    strings: {\n        get: (key: string) => string;\n    };\n    viewport: any;\n    onSubmit: (password: string) => void;\n    onDismiss?: () => void;\n}\nexport const PasswordBox: React.FC<PasswordBoxProps> = ({ strings, viewport, onSubmit, onDismiss }) => {\n    const inputRef = useRef<HTMLInputElement>(null);\n    const [hidden, setHidden] = useState(false);\n    useEffect(() => {\n        setTimeout(() => {\n            inputRef.current?.focus();\n        }, 50);\n    }, []);\n    const handleSubmit = (e?: React.FormEvent) => {\n        e?.preventDefault();\n        setHidden(true);\n        onSubmit(inputRef.current?.value || '');\n    };\n    return (<Dialog className=\"login-box password-box\" hidden={hidden} viewport={viewport} zIndex={100} buttons={[\n            { label: strings.get(\"GUI:Ok\"), onClick: handleSubmit },\n            {\n                label: strings.get(\"GUI:Cancel\"),\n                onClick: () => {\n                    setHidden(true);\n                    onDismiss?.();\n                },\n            },\n        ]}>\n      <form onSubmit={handleSubmit} autoComplete=\"off\">\n        <div className=\"field\">\n          <label>{strings.get(\"GUI:Password\")}</label>\n          <input name=\"lobbypass\" type=\"password\" autoComplete=\"off\" data-lpignore=\"true\" ref={inputRef}/>\n        </div>\n        <button type=\"submit\" style={{ visibility: \"hidden\" }}/>\n      </form>\n    </Dialog>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lobby/component/RankIndicator.tsx",
    "content": "import React from 'react';\nimport { Image } from '@/gui/component/Image';\nimport { PlayerRankType } from '@/network/ladder/PlayerRankType';\nconst RANK_ICONS = new Map<PlayerRankType, string>()\n    .set(PlayerRankType.Private, \"private\")\n    .set(PlayerRankType.Corporal, \"corporal\")\n    .set(PlayerRankType.Sergeant, \"sergeant\")\n    .set(PlayerRankType.Lieutenant, \"lieutena\")\n    .set(PlayerRankType.Major, \"major\")\n    .set(PlayerRankType.Colonel, \"colonel\")\n    .set(PlayerRankType.BrigGeneral, \"briggenr\")\n    .set(PlayerRankType.General, \"general\")\n    .set(PlayerRankType.FiveStarGeneral, \"stargen\")\n    .set(PlayerRankType.CommanderInChief, \"comchief\");\nconst RANK_LABELS = new Map<PlayerRankType, string>()\n    .set(PlayerRankType.Private, \"GUI:RankPrivate\")\n    .set(PlayerRankType.Corporal, \"GUI:RankCorporal\")\n    .set(PlayerRankType.Sergeant, \"GUI:RankSergeant\")\n    .set(PlayerRankType.Lieutenant, \"GUI:RankLieutenant\")\n    .set(PlayerRankType.Major, \"GUI:RankMajor\")\n    .set(PlayerRankType.Colonel, \"GUI:RankColonel\")\n    .set(PlayerRankType.BrigGeneral, \"GUI:RankBrigGeneral\")\n    .set(PlayerRankType.General, \"GUI:RankGeneral\")\n    .set(PlayerRankType.FiveStarGeneral, \"GUI:RankFiveStar\")\n    .set(PlayerRankType.CommanderInChief, \"GUI:RankCmdInChief\");\ninterface RankIndicatorProps {\n    playerProfile?: {\n        name: string;\n        rankType: PlayerRankType;\n    };\n    strings: {\n        get: (key: string) => string;\n    };\n}\nexport const RankIndicator: React.FC<RankIndicatorProps> = ({ playerProfile, strings }) => {\n    const rankType = playerProfile?.rankType ?? PlayerRankType.None;\n    const tooltip = playerProfile\n        ? rankType !== PlayerRankType.None\n            ? `${playerProfile.name} : ${strings.get(RANK_LABELS.get(rankType)!)}`\n            : `${playerProfile.name} : ${strings.get(\"TXT_UNRANKED\")}`\n        : undefined;\n    return (<div className=\"rank-indicator\" data-r-tooltip={tooltip}>\n      {rankType !== PlayerRankType.None && (<Image src={`${RANK_ICONS.get(rankType)}.pcx`}/>)}\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/lobby/component/viewmodel/lobby.ts",
    "content": "export enum LobbyType {\n    Singleplayer = 0,\n    MultiplayerHost = 1,\n    MultiplayerGuest = 2\n}\nexport enum SlotType {\n    Player = 1,\n    Ai = 2,\n    Observer = 3\n}\nexport enum SlotOccupation {\n    Open = 1,\n    Closed = 2,\n    Occupied = 3\n}\nexport enum PlayerStatus {\n    NotReady = 1,\n    Ready = 2,\n    Host = 3\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/login/LoginBox.tsx",
    "content": "import React, { useRef, useState, useEffect, useImperativeHandle, forwardRef } from \"react\";\nimport { ServerList } from \"@/gui/screen/mainMenu/login/ServerList\";\nimport { Task } from \"@puzzl/core/lib/async/Task\";\nimport { HttpRequest } from \"@/network/HttpRequest\";\nimport { MIN_USERNAME_LEN, MAX_USERNAME_LEN, MAX_PASS_LEN } from \"@/network/WolConfig\";\ninterface LoginBoxProps {\n    regions: any[];\n    selectedRegion: any;\n    selectedUser: string;\n    pings: any[];\n    breakingNewsUrl?: string;\n    strings: any;\n    onRegionChange: (region: any) => void;\n    onRequestRegionRefresh: () => void;\n    onSubmit: (username: string, password: string) => void;\n}\ninterface LoginBoxRef {\n    submit(): void;\n}\nexport const LoginBox = forwardRef<LoginBoxRef, LoginBoxProps>(({ regions, selectedRegion, selectedUser, pings, breakingNewsUrl, strings, onRegionChange, onRequestRegionRefresh, onSubmit, }, ref) => {\n    const formRef = useRef<HTMLFormElement>(null);\n    const usernameRef = useRef<HTMLInputElement>(null);\n    const passwordRef = useRef<HTMLInputElement>(null);\n    const [breakingNews, setBreakingNews] = useState<string>();\n    useEffect(() => {\n        setTimeout(() => usernameRef.current?.focus(), 50);\n    }, []);\n    useEffect(() => {\n        if (breakingNewsUrl) {\n            const task = new Task(async (cancellationToken) => {\n                const html = await new HttpRequest().fetchHtml(breakingNewsUrl, cancellationToken);\n                const trimmedHtml = html.trim();\n                if (trimmedHtml.length) {\n                    setBreakingNews(trimmedHtml);\n                }\n            });\n            task.start().catch((error) => console.error(error));\n            return () => task.cancel();\n        }\n    }, [breakingNewsUrl]);\n    const handleSubmit = () => {\n        if (usernameRef.current && passwordRef.current) {\n            onSubmit(usernameRef.current.value, passwordRef.current.value);\n        }\n    };\n    useImperativeHandle(ref, () => ({\n        submit() {\n            if (formRef.current?.requestSubmit) {\n                formRef.current.requestSubmit();\n            }\n            else {\n                handleSubmit();\n            }\n        },\n    }));\n    return React.createElement(\"div\", { className: \"login-wrapper\" }, React.createElement(\"div\", { className: \"title\" }, strings.get(\"GUI:Login\")), React.createElement(\"form\", {\n        onSubmit: (e: React.FormEvent) => {\n            e.preventDefault();\n            handleSubmit();\n        },\n        className: \"login-form login-box\",\n        ref: formRef,\n    }, React.createElement(\"div\", { className: \"field\" }, React.createElement(\"label\", null, strings.get(\"TS:Region\")), selectedUser && selectedRegion\n        ? React.createElement(\"input\", {\n            type: \"text\",\n            value: selectedRegion.label,\n            readOnly: true,\n        })\n        : React.createElement(React.Fragment, null, React.createElement(ServerList, {\n            regionId: selectedRegion?.id,\n            regions: regions,\n            pings: pings,\n            strings: strings,\n            onChange: (region: any) => {\n                onRegionChange(region);\n            },\n        }), React.createElement(\"button\", {\n            type: \"button\",\n            className: \"icon-button refresh-button\",\n            onClick: onRequestRegionRefresh,\n        }))), React.createElement(\"div\", { className: \"field\" }, React.createElement(\"label\", null, strings.get(\"GUI:Nickname\")), React.createElement(\"input\", {\n        name: \"user\",\n        type: \"text\",\n        required: true,\n        minLength: MIN_USERNAME_LEN,\n        maxLength: MAX_USERNAME_LEN,\n        pattern: \"[a-zA-Z0-9_\\\\-]+\",\n        ref: usernameRef,\n        defaultValue: selectedUser,\n        readOnly: !!selectedUser,\n    })), React.createElement(\"div\", { className: \"field\" }, React.createElement(\"label\", null, strings.get(\"GUI:Password\")), React.createElement(\"input\", {\n        name: \"pass\",\n        type: \"password\",\n        required: true,\n        maxLength: MAX_PASS_LEN,\n        ref: passwordRef,\n    })), React.createElement(\"button\", {\n        type: \"submit\",\n        style: { visibility: \"hidden\" },\n    })), breakingNews &&\n        React.createElement(\"fieldset\", { className: \"news\" }, React.createElement(\"legend\", null, strings.get(\"GUI:BreakingNews\")), React.createElement(\"div\", {\n            dangerouslySetInnerHTML: { __html: breakingNews },\n        })));\n});\n"
  },
  {
    "path": "src/gui/screen/mainMenu/login/LoginScreen.ts",
    "content": "import { jsx } from \"@/gui/jsx/jsx\";\nimport { WolError } from \"@/network/WolError\";\nimport { LoginBox } from \"@/gui/screen/mainMenu/login/LoginBox\";\nimport { ScreenType } from \"@/gui/screen/mainMenu/ScreenType\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { Task } from \"@puzzl/core/lib/async/Task\";\nimport { sleep } from \"@puzzl/core/lib/async/sleep\";\nimport { StorageKey } from \"LocalPrefs\";\nimport { MainMenuScreen } from \"@/gui/screen/mainMenu/MainMenuScreen\";\nimport { ServerPings } from \"@/gui/screen/mainMenu/login/ServerPings\";\nimport { OperationCanceledError, CancellationToken } from \"@puzzl/core/lib/async/cancellation\";\nimport { MainMenuRoute } from \"@/gui/screen/mainMenu/MainMenuRoute\";\ninterface Region {\n    id: string;\n    wolUrl: string;\n    wladderUrl: string;\n    wgameresUrl: string;\n    mapTransferUrl: string;\n}\ninterface ServerRegions {\n    load(regions: any[]): void;\n    isAvailable(regionId: string): boolean;\n    get(regionId: string): Region;\n    getAll(): Region[];\n    getFirstAvailable(): Region | undefined;\n    setSelectedRegion(regionId: string): void;\n}\ninterface WolService {\n    loadServerList(url: string, cancellationToken?: CancellationToken): Promise<any[]>;\n    validateGameVersion(region: Region): Promise<void>;\n    isConnected(): boolean;\n    getConnection(): {\n        getCurrentUser(): string;\n    };\n    connectAndLogin(config: {\n        url: string;\n        user: string;\n        pass: string;\n    }, onQueue?: (status: {\n        position: number;\n        avgWaitSeconds: number;\n    }) => void): Promise<any[]>;\n    closeWolConnection(): void;\n}\ninterface WladderService {\n    setUrl(url: string): void;\n}\ninterface WgameresService {\n    setUrl(url: string): void;\n}\ninterface MapTransferService {\n    setUrl(url: string): void;\n}\ninterface MessageBoxApi {\n    show(message: string, buttonText?: string, onClose?: () => void): void;\n    destroy(): void;\n}\ninterface ErrorHandler {\n    handle(error: any, message: string, onClose: () => void): void;\n}\ninterface LocalPrefs {\n    getItem(key: string): string | undefined;\n    setItem(key: string, value: string): void;\n}\ninterface RootController {\n    goToScreen(screenType: any, params: any): void;\n}\ninterface LoginScreenParams {\n    clearCredentials?: boolean;\n    useCredentials?: {\n        regionId: string;\n        user: string;\n        pass: string;\n    };\n    forceUser?: string;\n    afterLogin: (messages: any[]) => MainMenuRoute | {\n        screenType: any;\n        params: any;\n    };\n}\ninterface LoginBoxApi {\n    submit(): void;\n}\nlet savedCredentials: {\n    regionId: string;\n    user: string;\n    pass: string;\n} | undefined = undefined;\nexport class LoginScreen extends MainMenuScreen {\n    private wolService: WolService;\n    private wladderService: WladderService;\n    private wgameresService: WgameresService;\n    private mapTransferService: MapTransferService;\n    private strings: any;\n    private jsxRenderer: any;\n    private messageBoxApi: MessageBoxApi;\n    private serverRegions: ServerRegions;\n    private serversUrl: string;\n    private breakingNewsUrl: string;\n    private wolLogger: any;\n    private errorHandler: ErrorHandler;\n    private localPrefs: LocalPrefs;\n    private rootController: RootController;\n    private params!: LoginScreenParams;\n    private needsServerListRefresh: boolean = false;\n    private selectedRegion?: Region;\n    private serverPings!: ServerPings;\n    private loginBoxApi?: LoginBoxApi | null;\n    private loginBox?: any;\n    private isBusy: boolean = false;\n    private formRendered: boolean = false;\n    private serversUpdateTask?: Task<void>;\n    constructor(wolService: WolService, wladderService: WladderService, wgameresService: WgameresService, mapTransferService: MapTransferService, strings: any, jsxRenderer: any, messageBoxApi: MessageBoxApi, serverRegions: ServerRegions, serversUrl: string, breakingNewsUrl: string, wolLogger: any, errorHandler: ErrorHandler, localPrefs: LocalPrefs, rootController: RootController) {\n        super();\n        this.wolService = wolService;\n        this.wladderService = wladderService;\n        this.wgameresService = wgameresService;\n        this.mapTransferService = mapTransferService;\n        this.strings = strings;\n        this.jsxRenderer = jsxRenderer;\n        this.messageBoxApi = messageBoxApi;\n        this.serverRegions = serverRegions;\n        this.serversUrl = serversUrl;\n        this.breakingNewsUrl = breakingNewsUrl;\n        this.wolLogger = wolLogger;\n        this.errorHandler = errorHandler;\n        this.localPrefs = localPrefs;\n        this.rootController = rootController;\n        this.title = this.strings.get(\"GUI:Login\");\n        this.needsServerListRefresh = false;\n        this.handleLoginSubmit = async (username: string, password: string) => {\n            if (!this.isBusy && this.loginBoxApi && this.controller) {\n                this.isBusy = true;\n                const region = this.selectedRegion;\n                if (region) {\n                    await this.controller.hideSidebarButtons();\n                    await this.login(username, password, region.id);\n                }\n            }\n        };\n    }\n    private handleLoginSubmit: (username: string, password: string) => Promise<void>;\n    async onEnter(params: LoginScreenParams): Promise<void> {\n        this.params = params;\n        this.formRendered = false;\n        this.controller.toggleMainVideo(false);\n        if (params.clearCredentials) {\n            savedCredentials = undefined;\n        }\n        else if (params.useCredentials) {\n            savedCredentials = params.useCredentials;\n        }\n        try {\n            await this.loadServerList();\n        }\n        catch (error) {\n            this.handleWolError(error, this.strings.get(\"TXT_NO_SERV_LIST\"), { fatal: true });\n            return;\n        }\n        this.needsServerListRefresh = false;\n        this.serverPings = new ServerPings(this.serverRegions, this.wolLogger);\n        if (savedCredentials && this.serverRegions.isAvailable(savedCredentials.regionId)) {\n            this.isBusy = true;\n            this.login(savedCredentials.user, savedCredentials.pass, savedCredentials.regionId);\n        }\n        else {\n            this.isBusy = false;\n            this.initView(true);\n        }\n    }\n    private async loadServerList(cancellationToken?: CancellationToken): Promise<void> {\n        let isShowingConnecting = false;\n        const timeout = setTimeout(async () => {\n            this.messageBoxApi.show(this.strings.get(\"TXT_CONNECTING\"));\n            isShowingConnecting = true;\n        }, 1000);\n        try {\n            const serverList = await this.wolService.loadServerList(this.serversUrl, cancellationToken);\n            if (cancellationToken?.isCancelled())\n                return;\n            this.serverRegions.load(serverList);\n            if (this.selectedRegion) {\n                this.selectedRegion = this.serverRegions\n                    .getAll()\n                    .find((region) => region.id === this.selectedRegion!.id);\n            }\n        }\n        finally {\n            clearTimeout(timeout);\n            if (isShowingConnecting) {\n                this.messageBoxApi.destroy();\n            }\n        }\n    }\n    private initView(updateServers: boolean = false): void {\n        if (!this.controller)\n            return;\n        this.updateSidebarButtons();\n        if (!this.isBusy) {\n            this.controller.showSidebarButtons();\n        }\n        if (!this.selectedRegion) {\n            const savedRegionId = this.localPrefs.getItem(StorageKey.PreferredServerRegion);\n            const candidateRegion = savedRegionId && this.serverRegions.isAvailable(savedRegionId)\n                ? this.serverRegions.get(savedRegionId)\n                : this.serverRegions.getFirstAvailable();\n            const pings = this.serverPings.getPings();\n            if (!candidateRegion || (pings.has(candidateRegion) && pings.get(candidateRegion) === undefined)) {\n                this.selectedRegion = undefined;\n            }\n            else {\n                this.selectedRegion = candidateRegion;\n            }\n        }\n        if (updateServers && !this.params.forceUser && this.selectedRegion) {\n            this.updateServers();\n        }\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            width: \"100%\",\n            height: \"100%\",\n            component: LoginBox,\n            props: {\n                ref: (ref: LoginBoxApi) => (this.loginBoxApi = ref),\n                regions: this.serverRegions.getAll(),\n                selectedRegion: this.selectedRegion,\n                selectedUser: this.params.forceUser,\n                pings: this.serverPings.getPings(),\n                breakingNewsUrl: this.breakingNewsUrl,\n                strings: this.strings,\n                onRegionChange: (regionId: string) => {\n                    this.selectedRegion = this.serverRegions.get(regionId);\n                    this.loginBox?.applyOptions((options: any) => {\n                        options.selectedRegion = this.selectedRegion;\n                    });\n                    this.updateSidebarButtons();\n                },\n                onRequestRegionRefresh: () => {\n                    this.needsServerListRefresh = true;\n                    this.updateServers();\n                },\n                onSubmit: this.handleLoginSubmit,\n            },\n            innerRef: (ref: any) => (this.loginBox = ref),\n        }));\n        this.controller.setMainComponent(component);\n        this.updateSidebarButtons();\n        this.formRendered = true;\n    }\n    private updateSidebarButtons(): void {\n        if (!this.controller)\n            return;\n        this.controller.setSidebarButtons([\n            {\n                label: this.strings.get(\"GUI:Login\"),\n                disabled: !this.selectedRegion,\n                onClick: () => {\n                    this.submitLoginForm();\n                },\n            },\n            {\n                label: this.strings.get(\"GUI:NewAccount\"),\n                disabled: !!this.params.forceUser,\n                onClick: () => {\n                    this.controller?.goToScreen(ScreenType.NewAccount, {\n                        regionId: this.selectedRegion?.id,\n                        afterLogin: this.params.afterLogin,\n                    });\n                },\n            },\n            {\n                label: this.strings.get(\"GUI:Back\"),\n                isBottom: true,\n                onClick: () => {\n                    this.controller?.goToScreen(ScreenType.Home);\n                },\n            },\n        ]);\n    }\n    private updateServers(): void {\n        if (this.isBusy || this.serversUpdateTask)\n            return;\n        this.serverPings.getPings().clear();\n        this.handleServerPingsUpdate();\n        this.serversUpdateTask = new Task(async (cancellationToken) => {\n            if (!this.formRendered) {\n                await sleep(500, cancellationToken);\n            }\n            if (this.needsServerListRefresh) {\n                this.needsServerListRefresh = false;\n                try {\n                    await this.loadServerList(cancellationToken);\n                }\n                catch (error) {\n                    this.handleWolError(error, this.strings.get(\"TXT_NO_SERV_LIST\"), { fatal: true });\n                    this.serversUpdateTask = undefined;\n                    return;\n                }\n            }\n            this.loginBox?.applyOptions((options: any) => {\n                options.selectedRegion = this.selectedRegion;\n                options.regions = this.serverRegions.getAll();\n            });\n            this.updateSidebarButtons();\n            try {\n                await this.serverPings.update(() => this.handleServerPingsUpdate(), cancellationToken);\n            }\n            finally {\n                this.serversUpdateTask = undefined;\n            }\n            this.handleServerPingsUpdate();\n        });\n        this.serversUpdateTask.start().catch((error) => {\n            if (!(error instanceof OperationCanceledError)) {\n                console.error(error);\n            }\n        });\n    }\n    private handleServerPingsUpdate(): void {\n        if (!this.loginBoxApi)\n            return;\n        const currentRegion = this.selectedRegion;\n        const pings = this.serverPings.getPings();\n        if (currentRegion && pings.has(currentRegion) && pings.get(currentRegion) === undefined) {\n            this.selectedRegion = undefined;\n            this.loginBox?.applyOptions((options: any) => {\n                options.selectedRegion = undefined;\n            });\n            this.updateSidebarButtons();\n        }\n        this.loginBox?.refresh();\n    }\n    private submitLoginForm(): void {\n        if (!this.isBusy && this.loginBoxApi && this.controller && this.selectedRegion) {\n            this.loginBoxApi.submit();\n        }\n    }\n    private async login(username: string, password: string, regionId: string): Promise<void> {\n        if (!username.match(/^[A-Za-z0-9-_]+$/)) {\n            this.handleBadPass();\n            return;\n        }\n        this.serversUpdateTask?.cancel();\n        this.serversUpdateTask = undefined;\n        const connectingTask = new Task(async (cancellationToken) => {\n            await sleep(1000, cancellationToken);\n            if (!cancellationToken.isCancelled()) {\n                this.messageBoxApi.show(this.strings.get(\"TXT_CONNECTING\"));\n            }\n        });\n        connectingTask.start().catch((error) => {\n            if (!(error instanceof OperationCanceledError)) {\n                console.error(error);\n            }\n        });\n        const region = this.serverRegions.get(regionId);\n        this.serverRegions.setSelectedRegion(regionId);\n        try {\n            await this.wolService.validateGameVersion(region);\n        }\n        catch (error) {\n            connectingTask.cancel();\n            this.messageBoxApi.destroy();\n            const message = error instanceof WolError && error.code === WolError.Code.OutdatedClient\n                ? this.strings.get(\"TS:OutdatedClient\")\n                : this.strings.get(\"TXT_NO_SERV_LIST\");\n            this.handleWolError(error, message, { fatal: false });\n            return;\n        }\n        let wasCancelled = false;\n        try {\n            let messages: any[] = [];\n            if (!this.wolService.isConnected() || !this.wolService.getConnection().getCurrentUser()) {\n                messages = await this.wolService.connectAndLogin({ url: region.wolUrl, user: username, pass: password }, ({ position, avgWaitSeconds }) => {\n                    connectingTask.cancel();\n                    this.messageBoxApi.show(this.strings.get(\"TS:ServerFull\") +\n                        \"\\n\\n\\n\" +\n                        this.strings.get(\"TS:LoginPositionInQueue\", position) +\n                        \"\\n\" +\n                        this.strings.get(\"TS:LoginAvgWaitTime\") +\n                        (avgWaitSeconds > 0 && avgWaitSeconds < 3600\n                            ? this.strings.get(\"TS:LoginAvgWaitTimeMinutes\", avgWaitSeconds < 60 ? \"<1\" : \"~\" + Math.ceil(avgWaitSeconds / 60))\n                            : this.strings.get(\"TS:LoginAvgWaitTimeUnavail\")), this.strings.get(\"GUI:Cancel\"), () => {\n                        wasCancelled = true;\n                        this.wolService.closeWolConnection();\n                    });\n                });\n                this.wladderService.setUrl(region.wladderUrl);\n                this.wgameresService.setUrl(region.wgameresUrl);\n                this.mapTransferService.setUrl(region.mapTransferUrl);\n                savedCredentials = { user: username, pass: password, regionId };\n            }\n            connectingTask.cancel();\n            this.messageBoxApi.destroy();\n            this.localPrefs.setItem(StorageKey.PreferredServerRegion, regionId);\n            const result = this.params.afterLogin(messages);\n            if (result instanceof MainMenuRoute) {\n                this.controller?.goToScreen(result.screenType, result.params);\n            }\n            else {\n                this.rootController.goToScreen(result.screenType, result.params);\n            }\n        }\n        catch (error) {\n            connectingTask.cancel();\n            this.messageBoxApi.destroy();\n            if (wasCancelled) {\n                this.isBusy = false;\n                if (this.formRendered) {\n                    this.updateSidebarButtons();\n                    this.controller?.showSidebarButtons();\n                }\n                else {\n                    this.initView();\n                }\n                return;\n            }\n            if (error instanceof WolError && error.code === WolError.Code.OutdatedClient) {\n                this.handleWolError(error, this.strings.get(\"TS:OutdatedClient\"), { fatal: false });\n                return;\n            }\n            if (error instanceof WolError && error.code === WolError.Code.BadLogin) {\n                this.wolService.closeWolConnection();\n                this.handleBadPass();\n            }\n            else if (error instanceof WolError && error.code === WolError.Code.BannedFromServer) {\n                this.wolService.closeWolConnection();\n                this.handleLoginError(error.reason ?? \"This account is banned\");\n            }\n            else if (error instanceof WolError && error.code === WolError.Code.ServerFull) {\n                this.handleLoginError(this.strings.get(\"TS:ServerFull\"));\n            }\n            else {\n                this.handleWolError(error, this.strings.get(\"TS:ConnectFailed\"), {\n                    fatal: false,\n                    netError: true,\n                });\n            }\n        }\n    }\n    private handleBadPass(): void {\n        this.handleLoginError(this.strings.get(\"TXT_BADPASS\"));\n    }\n    private handleLoginError(message: string): void {\n        this.messageBoxApi.show(message, this.strings.get(\"GUI:Ok\"), () => {\n            this.isBusy = false;\n            if (this.formRendered) {\n                this.updateSidebarButtons();\n                this.controller.showSidebarButtons();\n            }\n            else {\n                this.initView();\n            }\n        });\n    }\n    private handleWolError(error: any, message: string, { fatal, netError }: {\n        fatal: boolean;\n        netError?: boolean;\n    } = { fatal: false }): void {\n        this.errorHandler.handle(error, message, () => {\n            this.isBusy = false;\n            this.serversUpdateTask?.cancel();\n            this.serversUpdateTask = undefined;\n            this.wolService.closeWolConnection();\n            if (fatal) {\n                this.controller?.goToScreen(ScreenType.Home);\n            }\n            else if (this.formRendered) {\n                this.updateSidebarButtons();\n                this.controller?.showSidebarButtons();\n            }\n            else {\n                if (netError) {\n                    this.needsServerListRefresh = true;\n                }\n                this.initView(!!netError);\n            }\n        });\n    }\n    async onLeave(): Promise<void> {\n        this.loginBoxApi = null;\n        this.loginBox = undefined;\n        this.formRendered = false;\n        this.serversUpdateTask?.cancel();\n        this.serversUpdateTask = undefined;\n        if (!this.isBusy) {\n            await this.controller.hideSidebarButtons();\n        }\n        this.isBusy = false;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/login/ServerList.tsx",
    "content": "import React from \"react\";\nimport { List, ListItem } from \"@/gui/component/List\";\ninterface Region {\n    id: string;\n    label: string;\n    available: boolean;\n}\ninterface ServerListProps {\n    regionId: string;\n    regions: Region[];\n    pings: Map<Region, number | undefined>;\n    strings: any;\n    onChange: (regionId: string) => void;\n}\nexport const ServerList: React.FC<ServerListProps> = ({ regionId, regions, pings, strings, onChange, }) => React.createElement(List, { className: \"server-list\" }, regions.map((region) => {\n    const ping = pings.get(region);\n    const isDisabled = !region.available || (pings.has(region) && ping === undefined);\n    return React.createElement(ListItem, {\n        key: region.id,\n        selected: region.id === regionId && !isDisabled,\n        disabled: isDisabled,\n        onClick: () => !isDisabled && onChange(region.id),\n    }, React.createElement(\"span\", { className: \"label\" }, region.label), React.createElement(\"span\", { className: \"ping\" }, isDisabled\n        ? React.createElement(\"span\", { className: \"offline-text\" }, strings.get(\"TS:ServerOffline\"))\n        : ping !== undefined &&\n            React.createElement(\"span\", { className: \"online-text\" }, strings.get(\"TS:ServerOnline\"))));\n}));\n"
  },
  {
    "path": "src/gui/screen/mainMenu/login/ServerPingIndicator.tsx",
    "content": "import React from \"react\";\nimport classnames from \"classnames\";\nimport { Image } from \"@/gui/component/Image\";\nenum PingQuality {\n    Good = 1,\n    Average = 2,\n    Bad = 3\n}\ninterface ServerPingIndicatorProps {\n    ping: number;\n    strings: any;\n}\nconst getPingQuality = (ping: number): PingQuality => {\n    if (ping <= 100)\n        return PingQuality.Good;\n    if (ping <= 250)\n        return PingQuality.Average;\n    return PingQuality.Bad;\n};\nconst pingImageMap = new Map<PingQuality, string>()\n    .set(PingQuality.Bad, \"pingr\")\n    .set(PingQuality.Average, \"pingy\")\n    .set(PingQuality.Good, \"pingg\");\nconst pingClassMap = new Map<PingQuality, string>()\n    .set(PingQuality.Bad, \"ping-bad\")\n    .set(PingQuality.Average, \"ping-avg\")\n    .set(PingQuality.Good, \"ping-good\");\nexport const ServerPingIndicator: React.FC<ServerPingIndicatorProps> = ({ ping, strings }) => {\n    const quality = getPingQuality(ping);\n    const imageKey = pingImageMap.get(quality);\n    const cssClass = pingClassMap.get(quality);\n    return React.createElement(\"div\", { className: \"server-ping\" }, React.createElement(\"span\", { className: classnames(\"ping-text\", cssClass) }, strings.get(\"TS:PingValue\", ping)), React.createElement(Image, { src: `${imageKey}.pcx` }));\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/login/ServerPings.ts",
    "content": "import { OperationCanceledError, CancellationToken } from \"@puzzl/core/lib/async/cancellation\";\nimport { WolConnection } from \"@/network/WolConnection\";\ninterface Region {\n    wolUrl: string;\n    available: boolean;\n}\ninterface Regions {\n    getAll(): Region[];\n}\nexport class ServerPings {\n    private static readonly CONNECT_TIMEOUT = 5;\n    private regions: Regions;\n    private wolLogger: any;\n    private pings: Map<Region, number | undefined> = new Map();\n    constructor(regions: Regions, wolLogger: any) {\n        this.regions = regions;\n        this.wolLogger = wolLogger;\n        this.pings = new Map();\n    }\n    async update(onUpdate?: () => void, cancellationToken?: CancellationToken): Promise<void> {\n        await Promise.all(this.regions\n            .getAll()\n            .filter((region) => region.available)\n            .map(async (region) => {\n            const connection = WolConnection.factory(this.wolLogger);\n            try {\n                await connection.connect(region.wolUrl, {\n                    timeoutSeconds: ServerPings.CONNECT_TIMEOUT,\n                    cancelToken: cancellationToken,\n                });\n            }\n            catch (error) {\n                if (error instanceof OperationCanceledError) {\n                    return;\n                }\n                console.error(error);\n                connection.close();\n                this.pings.set(region, undefined);\n                onUpdate?.();\n                return;\n            }\n            let ping: number | undefined;\n            try {\n                ping = await connection.ping(5);\n            }\n            catch (error) {\n                console.error(error);\n            }\n            finally {\n                connection.close();\n            }\n            if (!cancellationToken?.isCancelled()) {\n                this.pings.set(region, ping);\n                onUpdate?.();\n            }\n        }));\n    }\n    getPings(): Map<Region, number | undefined> {\n        return this.pings;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/main/HomeScreen.ts",
    "content": "import { Screen } from '../../Controller';\nimport { MainMenuScreenType } from '../../ScreenType';\nimport { MainMenuController } from '../MainMenuController';\nimport { Strings } from '../../../../data/Strings';\nimport { MusicType } from '../../../../engine/sound/Music';\nimport { MessageBoxApi } from '../../../component/MessageBoxApi';\nimport { FullScreen } from '../../../FullScreen';\nimport { getHumanReadableKey } from '@/gui/screen/options/component/getHumanReadableKey';\ninterface SidebarButton {\n    label: string;\n    tooltip?: string;\n    disabled?: boolean;\n    isBottom?: boolean;\n    onClick: () => void | Promise<void>;\n}\nexport class HomeScreen implements Screen {\n    private strings: Strings;\n    private messageBoxApi: MessageBoxApi;\n    private appVersion: string;\n    private storageEnabled: boolean;\n    private quickMatchEnabled: boolean;\n    private fullScreen?: FullScreen;\n    private controller?: MainMenuController;\n    public title: string;\n    public musicType: MusicType;\n    constructor(strings: Strings, messageBoxApi: MessageBoxApi, appVersion: string, storageEnabled: boolean = false, quickMatchEnabled: boolean = false, fullScreen?: FullScreen) {\n        this.strings = strings;\n        this.messageBoxApi = messageBoxApi;\n        this.appVersion = appVersion;\n        this.storageEnabled = storageEnabled;\n        this.quickMatchEnabled = quickMatchEnabled;\n        this.fullScreen = fullScreen;\n        this.title = this.strings.get(\"GUI:MainMenu\") || \"Main Menu\";\n        this.musicType = MusicType.Intro;\n    }\n    setController(controller: MainMenuController): void {\n        this.controller = controller;\n    }\n    onEnter(): void {\n        console.log('[HomeScreen] Entering home screen');\n        const buttons: SidebarButton[] = [\n            {\n                label: '遭遇战',\n                tooltip: '与AI进行单人遭遇战',\n                onClick: async () => {\n                    console.log('[HomeScreen] 遭遇战 clicked');\n                    try {\n                        if (this.controller) {\n                            this.controller.goToScreen(MainMenuScreenType.Skirmish);\n                        }\n                    }\n                    catch (error) {\n                        console.error('[HomeScreen] Failed to navigate to Skirmish:', error);\n                        await this.messageBoxApi.alert('遭遇战 - 功能开发中\\n\\n基本框架已配置，但仍需完善以下组件：\\n• 游戏规则系统\\n• 地图加载器\\n• AI对手系统\\n• 游戏模式管理器', this.strings.get('GUI:OK') || 'OK');\n                    }\n                }\n            },\n            {\n                label: '直播互动',\n                tooltip: '进入直播互动模式，响应进房、点赞、礼物等事件并驱动双方出兵对抗',\n                onClick: () => {\n                    console.log('[HomeScreen] Live Interaction clicked');\n                    window.location.hash = '/liveinteraction';\n                }\n            },\n            {\n                label: '录像回放',\n                tooltip: '查看和回放游戏录像',\n                onClick: () => {\n                    console.log('[HomeScreen] Replays clicked');\n                    if (this.controller) {\n                        this.controller.pushScreen(MainMenuScreenType.ReplaySelection);\n                    }\n                }\n            },\n            {\n                label: '局域网联机',\n                tooltip: '手工交换 SDP，建立局域网 P2P 数据通道',\n                onClick: () => {\n                    console.log('[HomeScreen] LAN Setup clicked');\n                    if (this.controller) {\n                        this.controller.pushScreen(MainMenuScreenType.LanSetup);\n                    }\n                }\n            },\n        ];\n        if (this.storageEnabled) {\n            buttons.push({\n                label: this.strings.get('GUI:Mods') || 'Mods',\n                tooltip: this.strings.get('STT:Mods') || 'Manage and play modified versions of the base game',\n                onClick: async () => {\n                    console.log('[HomeScreen] Mods clicked');\n                    await this.messageBoxApi.alert('Mods - 功能开发中\\n\\n需要模组管理系统', this.strings.get('GUI:OK') || 'OK');\n                }\n            });\n        }\n        buttons.push({\n            label: this.strings.get('TS:InfoAndCredits') || 'Info & Credits',\n            tooltip: this.strings.get('STT:InfoAndCredits') || 'Information and credits',\n            onClick: () => {\n                console.log('[HomeScreen] Info & Credits clicked');\n                if (this.controller) {\n                    this.controller.pushScreen(MainMenuScreenType.InfoAndCredits);\n                }\n            }\n        }, {\n            label: this.strings.get('GUI:Options') || 'Options',\n            tooltip: this.strings.get('STT:MainButtonOptions') || 'Game options and settings',\n            onClick: () => {\n                console.log('[HomeScreen] Options clicked');\n                if (this.controller) {\n                    this.controller.pushScreen(MainMenuScreenType.Options);\n                }\n            }\n        }, {\n            label: '底层测试入口',\n            tooltip: '进入底层文件系统与测试工具',\n            onClick: () => {\n                console.log('[HomeScreen] Test Entry clicked');\n                if (this.controller) {\n                    this.controller.pushScreen(MainMenuScreenType.TestEntry);\n                }\n            }\n        }, {\n            label: this.strings.get('GUI:Fullscreen', getHumanReadableKey(FullScreen.hotKey)) || 'Fullscreen',\n            tooltip: this.strings.get('STT:Fullscreen') || 'Toggle full screen mode',\n            isBottom: true,\n            disabled: this.fullScreen ? !this.fullScreen.isAvailable() : false,\n            onClick: () => {\n                console.log('[HomeScreen] Fullscreen clicked');\n                this.toggleFullscreen();\n            }\n        });\n        if (this.controller) {\n            this.controller.setSidebarButtons(buttons);\n            this.controller.showSidebarButtons();\n            this.controller.toggleMainVideo(true);\n            this.controller.showVersion(this.appVersion);\n        }\n    }\n    async onLeave(): Promise<void> {\n        console.log('[HomeScreen] Leaving home screen');\n        if (this.controller) {\n            this.controller.hideVersion();\n            await this.controller.hideSidebarButtons();\n        }\n    }\n    async onStack(): Promise<void> {\n        await this.onLeave();\n    }\n    onUnstack(): void {\n        this.onEnter();\n    }\n    update(deltaTime: number): void {\n    }\n    destroy(): void {\n    }\n    private async toggleFullscreen(): Promise<void> {\n        try {\n            if (this.fullScreen?.isAvailable()) {\n                await this.fullScreen.toggleAsync();\n            }\n            else if (document.fullscreenElement) {\n                await document.exitFullscreen();\n            }\n            else {\n                await document.documentElement.requestFullscreen();\n            }\n        }\n        catch (err) {\n            console.error('Error toggling fullscreen:', err);\n            await this.messageBoxApi.alert(document.fullscreenElement\n                ? '无法退出全屏模式'\n                : '无法进入全屏模式\\n\\n请检查浏览器权限设置', this.strings.get('GUI:OK') || 'OK');\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/main/ReportBug.tsx",
    "content": "import React from 'react';\nimport { Strings } from '../../../../data/Strings';\nexport interface ReportBugProps {\n    discordUrl?: string;\n    strings: Strings;\n}\nexport const ReportBug: React.FC<ReportBugProps> = ({ discordUrl, strings }) => {\n    return (<div style={{ padding: '20px', color: 'white' }}>\n      <div style={{ marginBottom: '15px' }}>\n        {strings.get('TS:ReportBugDesc') || '您可以在我们的专用Discord服务器频道上提交错误报告，请点击下面的链接：'}\n      </div>\n      \n      {discordUrl && (<div style={{ textAlign: 'center' }}>\n          <a href={discordUrl} target=\"_blank\" rel=\"noopener noreferrer\" style={{\n                color: '#00ff00',\n                textDecoration: 'underline',\n                fontSize: '16px'\n            }} onClick={() => {\n                window.gtag?.('event', 'discord_click');\n            }}>\n            {discordUrl}\n          </a>\n        </div>)}\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/main/TestEntryScreen.ts",
    "content": "import { Screen } from '../../Controller';\nimport { MainMenuScreenType } from '../../ScreenType';\nimport { MainMenuController } from '../MainMenuController';\nimport { Strings } from '../../../../data/Strings';\nimport { MessageBoxApi } from '../../../component/MessageBoxApi';\ninterface SidebarButton {\n    label: string;\n    tooltip?: string;\n    disabled?: boolean;\n    isBottom?: boolean;\n    onClick: () => void | Promise<void>;\n}\ntype TestEntryView = 'home' | 'asset' | 'mechanic' | 'scene';\nexport class TestEntryScreen implements Screen {\n    private strings: Strings;\n    private messageBoxApi: MessageBoxApi;\n    private appVersion: string;\n    private controller?: MainMenuController;\n    private view: TestEntryView = 'home';\n    public title: string = '底层测试入口';\n    constructor(strings: Strings, messageBoxApi: MessageBoxApi, appVersion: string) {\n        this.strings = strings;\n        this.messageBoxApi = messageBoxApi;\n        this.appVersion = appVersion;\n    }\n    setController(controller: MainMenuController): void {\n        this.controller = controller;\n    }\n    onEnter(): void {\n        console.log('[TestEntryScreen] Entering test entry screen');\n        this.view = 'home';\n        this.renderButtons();\n        if (this.controller) {\n            this.controller.toggleMainVideo(false);\n            this.controller.showVersion(this.appVersion);\n        }\n    }\n    private setView(view: TestEntryView): void {\n        this.view = view;\n        this.renderButtons();\n    }\n    private getSidebarTitle(): string {\n        switch (this.view) {\n            case 'asset':\n                return '素材测试';\n            case 'mechanic':\n                return '机制测试';\n            case 'scene':\n                return '场景测试';\n            default:\n                return this.title;\n        }\n    }\n    private createRouteButton(label: string, tooltip: string, route: string): SidebarButton {\n        return {\n            label,\n            tooltip,\n            onClick: () => {\n                console.log(`[TestEntryScreen] ${label} clicked`);\n                window.location.hash = route;\n            }\n        };\n    }\n    private createBackToCategoriesButton(): SidebarButton {\n        return {\n            label: '返回测试分类',\n            onClick: () => this.setView('home')\n        };\n    }\n    private createBackToMenuButton(): SidebarButton {\n        return {\n            label: '返回主菜单',\n            isBottom: true,\n            onClick: () => {\n                console.log('[TestEntryScreen] Back clicked');\n                this.controller?.leaveCurrentScreen();\n            }\n        };\n    }\n    private renderButtons(): void {\n        const homeButtons: SidebarButton[] = [\n            {\n                label: '素材测试',\n                tooltip: '查看 VXL、SHP、音频素材测试',\n                onClick: () => this.setView('asset')\n            },\n            {\n                label: '机制测试',\n                tooltip: '查看 建筑、载具、步兵、飞行器测试',\n                onClick: () => this.setView('mechanic')\n            },\n            {\n                label: '场景测试',\n                tooltip: '查看 大厅、世界、移动测试',\n                onClick: () => this.setView('scene')\n            },\n            this.createBackToMenuButton()\n        ];\n        const assetButtons: SidebarButton[] = [\n            this.createRouteButton('VXL测试', '打开 VXL 测试工具', '/vxltest'),\n            this.createRouteButton('SHP测试', '打开 SHP 测试工具', '/shptest'),\n            this.createRouteButton('音频测试', '打开 音频 测试工具', '/soundtest'),\n            this.createBackToCategoriesButton(),\n            this.createBackToMenuButton()\n        ];\n        const mechanicButtons: SidebarButton[] = [\n            this.createRouteButton('建筑测试', '打开 建筑 测试工具', '/buildtest'),\n            this.createRouteButton('载具测试', '打开 载具 测试工具', '/vehicletest'),\n            this.createRouteButton('步兵测试', '打开 步兵 测试工具', '/inftest'),\n            this.createRouteButton('飞行器测试', '打开 飞行器 测试工具', '/airtest'),\n            this.createBackToCategoriesButton(),\n            this.createBackToMenuButton()\n        ];\n        const sceneButtons: SidebarButton[] = [\n            this.createRouteButton('大厅测试', '打开 大厅 测试工具', '/lobbytest'),\n            this.createRouteButton('世界测试', '打开 世界场景 测试工具', '/worldscenetest'),\n            this.createRouteButton('移动测试', '打开 单位移动 测试工具', '/unitmovementtest'),\n            this.createRouteButton('性能测试', '打开 性能 基准 测试工具', '/perftest'),\n            this.createBackToCategoriesButton(),\n            this.createBackToMenuButton()\n        ];\n        const buttons = this.view === 'asset'\n            ? assetButtons\n            : this.view === 'mechanic'\n                ? mechanicButtons\n                : this.view === 'scene'\n                    ? sceneButtons\n                    : homeButtons;\n        if (this.controller) {\n            this.controller.setSidebarTitle(this.getSidebarTitle());\n            this.controller.setSidebarButtons(buttons);\n            this.controller.showSidebarButtons();\n        }\n    }\n    async onLeave(): Promise<void> {\n        console.log('[TestEntryScreen] Leaving test entry screen');\n        if (this.controller) {\n            await this.controller.hideSidebarButtons();\n            this.controller.setSidebarTitle('');\n            this.controller.hideVersion();\n        }\n    }\n    async onStack(): Promise<void> {\n        await this.onLeave();\n    }\n    onUnstack(): void {\n        this.onEnter();\n    }\n    update(deltaTime: number): void {\n    }\n    destroy(): void {\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/mapSel/MapSelScreen.ts",
    "content": "import { jsx } from \"@/gui/jsx/jsx\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { MapSel, SortType } from \"@/gui/screen/mainMenu/mapSel/component/MapSel\";\nimport { MapPreviewRenderer } from \"@/gui/screen/mainMenu/lobby/MapPreviewRenderer\";\nimport { DownloadError } from \"@/network/HttpRequest\";\nimport { Task } from \"@puzzl/core/lib/async/Task\";\nimport { CancellationTokenSource, OperationCanceledError, CancellationToken } from \"@puzzl/core/lib/async/cancellation\";\nimport { MainMenuScreen } from \"@/gui/screen/mainMenu/MainMenuScreen\";\nimport { GameModeType } from \"@/game/ini/GameModeType\";\nimport { StorageKey } from \"@/LocalPrefs\";\nimport { MapFile } from \"@/data/MapFile\";\nimport { VirtualFile } from \"@/data/vfs/VirtualFile\";\nimport { MapSupport } from \"@/engine/MapSupport\";\nimport { IOError } from \"@/data/vfs/IOError\";\nimport { StorageQuotaError } from \"@/data/vfs/StorageQuotaError\";\nimport { FileNotFoundError } from \"@/data/vfs/FileNotFoundError\";\nimport { Engine } from \"@/engine/Engine\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { MapManifest } from \"@/engine/MapManifest\";\nimport { NameNotAllowedError } from \"@/data/vfs/NameNotAllowedError\";\ninterface GameMode {\n    id: number;\n    label: string;\n    type: GameModeType;\n}\ninterface MapData {\n    mapName: string;\n    mapTitle: string;\n    maxSlots: number;\n    gameModes: {\n        id: number;\n    }[];\n}\ninterface GameModes {\n    getAll(): GameMode[];\n}\ninterface MapListEntry {\n    fileName: string;\n    maxSlots: number;\n    gameModes: {\n        id: number;\n    }[];\n    getFullMapTitle(strings: any): string;\n}\ninterface MapList {\n    getAll(): MapListEntry[];\n    getByName(name: string): MapListEntry | undefined;\n    add(manifest: MapManifest): void;\n}\ninterface MapFileLoader {\n    load(mapName: string, cancellationToken?: CancellationToken): Promise<VirtualFile>;\n}\ninterface ErrorHandler {\n    handle(error: any, message: string, onClose: () => void): void;\n}\ninterface MessageBoxApi {\n    alert(message: string, buttonText: string): Promise<void>;\n    confirm(message: string, confirmText: string, cancelText: string): Promise<boolean>;\n    destroy(): void;\n}\ninterface LocalPrefs {\n    getItem(key: string): string | undefined;\n    setItem(key: string, value: string): void;\n}\ninterface MapDirectory {\n    writeFile(file: VirtualFile): Promise<void>;\n}\ninterface FsAccessLib {\n    showOpenFilePicker(options: any): Promise<any>;\n}\ninterface Sentry {\n    captureException(error: Error): void;\n}\ninterface LobbyType {\n}\ninterface MapSelScreenParams {\n    gameOpts: {\n        gameMode: number;\n        mapName: string;\n    };\n    usedSlots: () => number;\n    lobbyType: LobbyType;\n}\ninterface MapSelScreenResult {\n    gameMode: GameMode;\n    mapName: string;\n    changedMapFile?: VirtualFile;\n}\nexport class MapSelScreen extends MainMenuScreen {\n    private strings: any;\n    private jsxRenderer: any;\n    private mapFileLoader: MapFileLoader;\n    private errorHandler: ErrorHandler;\n    private messageBoxApi: MessageBoxApi;\n    private localPrefs: LocalPrefs;\n    private mapList: MapList;\n    private gameModes: GameModes;\n    private mapDir: MapDirectory;\n    private fsAccessLib: FsAccessLib;\n    private sentry: Sentry;\n    private disposables: CompositeDisposable;\n    private availableGameModes?: GameMode[];\n    private allMaps?: MapData[];\n    private selectedGameMode!: GameMode;\n    private selectedMapName!: string;\n    private lobbyType!: LobbyType;\n    private computeUsedSlots?: () => number;\n    private form?: any;\n    private mapFileUpdateTask?: Task<void>;\n    private changedMapFile?: VirtualFile;\n    constructor(strings: any, jsxRenderer: any, mapFileLoader: MapFileLoader, errorHandler: ErrorHandler, messageBoxApi: MessageBoxApi, localPrefs: LocalPrefs, mapList: MapList, gameModes: GameModes, mapDir: MapDirectory, fsAccessLib: FsAccessLib, sentry: Sentry) {\n        super();\n        this.strings = strings;\n        this.jsxRenderer = jsxRenderer;\n        this.mapFileLoader = mapFileLoader;\n        this.errorHandler = errorHandler;\n        this.messageBoxApi = messageBoxApi;\n        this.localPrefs = localPrefs;\n        this.mapList = mapList;\n        this.gameModes = gameModes;\n        this.mapDir = mapDir;\n        this.fsAccessLib = fsAccessLib;\n        this.sentry = sentry;\n        this.title = this.strings.get(\"GUI:ChooseMap\");\n        this.disposables = new CompositeDisposable();\n        this.handleSelectMap = (mapName: string, doubleClick: boolean) => {\n            const isNewMap = this.selectedMapName !== mapName;\n            this.selectedMapName = mapName;\n            this.refreshMapInfo();\n            if (isNewMap) {\n                this.updateMapDeferred({ updatePreview: !doubleClick });\n                this.initSidebar();\n            }\n            this.form.applyOptions((options: any) => {\n                options.selectedMapName = mapName;\n            });\n            if (doubleClick) {\n                this.handleSubmit();\n            }\n        };\n        this.handleSelectGameMode = (gameMode: GameMode) => {\n            this.selectedGameMode = gameMode;\n            const availableMaps = this.computeAvailableMaps();\n            if (!availableMaps.find((map) => map.mapName === this.selectedMapName)) {\n                this.handleSelectMap(availableMaps[0].mapName, false);\n            }\n            this.refreshMapInfo();\n            this.form.applyOptions((options: any) => {\n                options.selectedGameMode = gameMode;\n                options.maps = availableMaps;\n            });\n        };\n        this.handleSelectSort = (sortType: SortType) => {\n            this.localPrefs.setItem(StorageKey.LastSortMap, sortType);\n        };\n    }\n    private handleSelectMap: (mapName: string, doubleClick: boolean) => void;\n    private handleSelectGameMode: (gameMode: GameMode) => void;\n    private handleSelectSort: (sortType: SortType) => void;\n    onEnter({ gameOpts, usedSlots, lobbyType }: MapSelScreenParams): void {\n        this.updateMapsAndModes();\n        this.selectedGameMode = this.availableGameModes!.find((mode) => mode.id === gameOpts.gameMode)!;\n        this.selectedMapName = gameOpts.mapName;\n        this.lobbyType = lobbyType;\n        this.computeUsedSlots = usedSlots;\n        this.initSidebar();\n        this.initForm();\n    }\n    private updateMapsAndModes(): void {\n        const availableGameModes = this.gameModes\n            .getAll()\n            .filter((gameMode) => this.mapList\n            .getAll()\n            .find((map) => map.gameModes.some((mode) => mode.id === gameMode.id)));\n        const allMaps = this.mapList.getAll().map((map) => ({\n            mapName: map.fileName,\n            mapTitle: map.getFullMapTitle(this.strings),\n            maxSlots: map.maxSlots,\n            gameModes: map.gameModes,\n        }));\n        this.availableGameModes = availableGameModes\n            .filter((mode) => mode.type !== GameModeType.Cooperative)\n            .sort((a, b) => a.id - b.id);\n        this.allMaps = allMaps;\n    }\n    private initForm(): void {\n        this.controller.setMainComponent(this.jsxRenderer.render(jsx(HtmlView, {\n            innerRef: (ref: any) => (this.form = ref),\n            component: MapSel,\n            props: {\n                strings: this.strings,\n                maps: this.computeAvailableMaps(),\n                gameModes: this.availableGameModes,\n                selectedMapName: this.selectedMapName,\n                selectedGameMode: this.selectedGameMode,\n                initialSortType: this.readInitialSort(),\n                onSelectMap: this.handleSelectMap,\n                onSelectGameMode: this.handleSelectGameMode,\n                onSelectSort: this.handleSelectSort,\n            },\n        }))[0]);\n    }\n    private readInitialSort(): SortType {\n        let sortType = this.localPrefs.getItem(StorageKey.LastSortMap) as SortType;\n        if (!sortType || !Object.values(SortType).includes(sortType)) {\n            sortType = SortType.None;\n        }\n        return sortType;\n    }\n    private initSidebar(): void {\n        const buttons = [\n            {\n                label: this.strings.get(\"GUI:UseMap\"),\n                tooltip: this.strings.get(\"STT:ScenarioButtonUseMap\"),\n                onClick: () => {\n                    this.handleSubmit();\n                },\n            },\n            ...(this.mapDir\n                ? [\n                    {\n                        label: this.strings.get(\"TS:ImportMap\"),\n                        tooltip: this.strings.get(\"STT:ImportMap\"),\n                        onClick: async () => {\n                            const cancellationSource = new CancellationTokenSource();\n                            const cleanup = () => cancellationSource.cancel();\n                            this.disposables.add(cleanup);\n                            try {\n                                await this.importMap(cancellationSource.token);\n                            }\n                            catch (error) {\n                                if (!(error instanceof OperationCanceledError)) {\n                                    this.handleMapImportError(error);\n                                }\n                            }\n                            finally {\n                                this.disposables.remove(cleanup);\n                            }\n                        },\n                    },\n                ]\n                : []),\n            {\n                label: this.strings.get(\"GUI:Cancel\"),\n                tooltip: this.strings.get(\"STT:ScenarioButtonCancel\"),\n                isBottom: true,\n                onClick: () => {\n                    this.controller?.popScreen();\n                },\n            },\n        ];\n        this.controller.setSidebarButtons(buttons, true);\n        this.refreshMapInfo();\n        this.controller.showSidebarButtons();\n    }\n    private async importMap(cancellationToken: CancellationToken): Promise<void> {\n        let file: File;\n        try {\n            const fileHandle = await this.fsAccessLib.showOpenFilePicker({\n                types: [\n                    {\n                        description: \"RA2 Map\",\n                        accept: {\n                            \"text/plain\": Engine.supportedMapTypes.map((type) => \".\" + type),\n                        },\n                    },\n                ],\n                excludeAcceptAllOption: true,\n            });\n            const handle = Array.isArray(fileHandle) ? fileHandle[0] : fileHandle;\n            file = await handle.getFile();\n        }\n        catch (error: any) {\n            if (error.name === \"AbortError\")\n                return;\n            if (error instanceof DOMException) {\n                const err = new IOError(`File could not be read (${error.name})`);\n                (err as any).cause = error;\n                throw err;\n            }\n            throw error;\n        }\n        if (!Engine.supportedMapTypes.some((type) => file.name.toLowerCase().endsWith(\".\" + type))) {\n            await this.messageBoxApi.alert(this.strings.get(\"TS:ImportMapUnsupportedType\", Engine.supportedMapTypes.map((type) => \"*.\" + type).join(\", \")), this.strings.get(\"GUI:Ok\"));\n            return;\n        }\n        if (this.mapList.getByName(file.name)) {\n            await this.messageBoxApi.alert(this.strings.get(\"TS:ImportMapDuplicateError\", file.name), this.strings.get(\"GUI:Ok\"));\n            return;\n        }\n        const virtualFile = await VirtualFile.fromRealFile(file);\n        let mapFile: MapFile;\n        let manifest: MapManifest;\n        try {\n            mapFile = new MapFile(virtualFile);\n            const supportError = MapSupport.check(mapFile, this.strings);\n            if (supportError) {\n                await this.messageBoxApi.alert(supportError, this.strings.get(\"GUI:Ok\"));\n                return;\n            }\n            manifest = new MapManifest().fromMapFile(virtualFile, this.gameModes.getAll() as any);\n        }\n        catch (error) {\n            console.error(error);\n            await this.messageBoxApi.alert(this.strings.get(\"TXT_MAP_ERROR\"), this.strings.get(\"GUI:Ok\"));\n            return;\n        }\n        if (mapFile.unknownActionTypes.size || mapFile.unknownEventTypes.size) {\n            if (!(await this.messageBoxApi.confirm(this.strings.get(\"TS:MapUnsupportedTriggers\"), this.strings.get(\"GUI:Continue\"), this.strings.get(\"GUI:Cancel\")))) {\n                return;\n            }\n        }\n        const gameModes = manifest.gameModes;\n        if (!gameModes.length) {\n            await this.messageBoxApi.alert(this.strings.get(\"TS:MapUnsupportedGameMode\"), this.strings.get(\"GUI:Ok\"));\n            return;\n        }\n        await this.mapDir.writeFile(virtualFile);\n        this.mapList.add(manifest);\n        cancellationToken.throwIfCancelled();\n        this.updateMapsAndModes();\n        this.form.applyOptions((options: any) => {\n            options.gameModes = this.availableGameModes;\n            options.maps = this.computeAvailableMaps();\n        });\n        if (!gameModes.some((mode) => this.selectedGameMode.id === mode.id)) {\n            this.handleSelectGameMode(gameModes[0]);\n        }\n        this.handleSelectMap(virtualFile.filename, false);\n    }\n    private handleMapImportError(error: any): void {\n        const strings = this.strings;\n        let message = strings.get(\"TS:ImportMapError\");\n        if (error.name === \"QuotaExceededError\" || error instanceof StorageQuotaError) {\n            message += \"\\n\\n\" + strings.get(\"ts:storage_quota_exceeded\");\n        }\n        else if (error instanceof NameNotAllowedError) {\n            message += \"\\n\\n\" + strings.get(\"TS:FileNameError\");\n        }\n        else if (!(error instanceof IOError || error instanceof FileNotFoundError)) {\n            this.sentry?.captureException(new Error(\"Map import failed \" + (error.message ?? error.name), { cause: error }));\n        }\n        this.errorHandler.handle(error, message, () => { });\n    }\n    private async handleSubmit(): Promise<void> {\n        const cancellationSource = new CancellationTokenSource();\n        const cleanup = () => cancellationSource.cancel();\n        this.disposables.add(cleanup);\n        try {\n            await this.submitMap(cancellationSource.token);\n        }\n        catch (error) {\n            if (!(error instanceof OperationCanceledError)) {\n                throw error;\n            }\n        }\n        finally {\n            this.disposables.remove(cleanup);\n        }\n    }\n    private async submitMap(cancellationToken: CancellationToken): Promise<void> {\n        let isFormHidden = false;\n        if (this.mapFileUpdateTask) {\n            this.form.hide();\n            this.controller?.hideSidebarButtons();\n            isFormHidden = true;\n            try {\n                await this.mapFileUpdateTask.wait();\n            }\n            catch (error) {\n            }\n        }\n        cancellationToken.throwIfCancelled();\n        if (this.changedMapFile) {\n            try {\n                const mapFile = new MapFile(this.changedMapFile);\n                const supportError = MapSupport.check(mapFile, this.strings);\n                if (supportError) {\n                    await this.messageBoxApi.alert(supportError, this.strings.get(\"GUI:Ok\"));\n                    return;\n                }\n            }\n            catch (error) {\n                console.error(error);\n                await this.messageBoxApi.alert(this.strings.get(\"TXT_MAP_ERROR\"), this.strings.get(\"GUI:Ok\"));\n                if (isFormHidden) {\n                    this.form.show();\n                    this.controller?.showSidebarButtons();\n                }\n                return;\n            }\n        }\n        const selectedMap = this.allMaps!.find((map) => map.mapName === this.selectedMapName)!;\n        const shouldProceed = this.computeUsedSlots!() <= selectedMap.maxSlots ||\n            (await this.messageBoxApi.confirm(this.strings.get(\"GUI:EjectPlayers\"), this.strings.get(\"GUI:Ok\"), this.strings.get(\"GUI:Cancel\")));\n        cancellationToken.throwIfCancelled();\n        if (shouldProceed) {\n            await (this.controller as any)?.popScreen({\n                gameMode: this.selectedGameMode,\n                mapName: this.selectedMapName,\n                changedMapFile: this.changedMapFile,\n            });\n        }\n        else if (isFormHidden) {\n            this.form.show();\n            this.controller?.showSidebarButtons();\n        }\n    }\n    private computeAvailableMaps(): MapData[] {\n        return this.allMaps!.filter((map) => map.gameModes.some((mode) => mode.id === this.selectedGameMode.id));\n    }\n    private refreshMapInfo(): void {\n        const selectedMap = this.allMaps!.find((map) => map.mapName === this.selectedMapName);\n        this.controller?.setSidebarMpContent({\n            text: this.strings.get(this.selectedGameMode.label) +\n                \"\\n\\n\" +\n                selectedMap?.mapTitle,\n        });\n    }\n    async onLeave(): Promise<void> {\n        this.computeUsedSlots = undefined;\n        this.availableGameModes = undefined;\n        this.allMaps = undefined;\n        this.messageBoxApi.destroy();\n        this.form = undefined;\n        this.mapFileUpdateTask?.cancel();\n        this.mapFileUpdateTask = undefined;\n        this.controller.setMainComponent();\n        this.disposables.dispose();\n        await this.controller.hideSidebarButtons();\n    }\n    private updateMapDeferred({ updatePreview }: {\n        updatePreview: boolean;\n    }): void {\n        this.mapFileUpdateTask?.cancel();\n        this.mapFileUpdateTask = new Task(async (cancellationToken) => {\n            if (!this.controller)\n                return;\n            if (updatePreview) {\n                this.controller.setSidebarPreview();\n            }\n            this.changedMapFile = undefined;\n            let mapFile: VirtualFile;\n            try {\n                mapFile = this.changedMapFile = await this.mapFileLoader.load(this.selectedMapName, cancellationToken);\n            }\n            catch (error) {\n                if (error instanceof DownloadError) {\n                    this.errorHandler.handle(error, this.strings.get(\"TXT_DOWNLOAD_FAILED\"), () => {\n                        this.controller?.popScreen();\n                    });\n                    return;\n                }\n                throw error;\n            }\n            if (updatePreview && !cancellationToken.isCancelled()) {\n                const preview = new MapPreviewRenderer(this.strings).render(new MapFile(mapFile), this.lobbyType as any, this.controller.getSidebarPreviewSize());\n                this.controller.setSidebarPreview(preview);\n            }\n            this.mapFileUpdateTask = undefined;\n        });\n        this.mapFileUpdateTask.start().catch((error) => {\n            if (!(error instanceof OperationCanceledError)) {\n                console.error(\"Failed to render map preview\");\n                console.error(error);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/mapSel/component/MapSel.tsx",
    "content": "import React, { useRef, useState, useEffect } from \"react\";\nimport { List, ListItem } from \"@/gui/component/List\";\nimport { Select } from \"@/gui/component/Select\";\nimport { Option } from \"@/gui/component/Option\";\nexport enum SortType {\n    None = \"\",\n    NameAsc = \"nameAsc\",\n    NameDesc = \"nameDesc\",\n    MaxSlotsAsc = \"maxSlotsAsc\",\n    MaxSlotsDesc = \"maxSlotsDesc\"\n}\ninterface MapData {\n    mapName: string;\n    mapTitle: string;\n    maxSlots: number;\n}\ninterface GameMode {\n    id: number;\n    label: string;\n    description?: string;\n}\ninterface MapSelProps {\n    strings: any;\n    gameModes: GameMode[];\n    maps: MapData[];\n    selectedGameMode: GameMode;\n    selectedMapName: string;\n    initialSortType: SortType;\n    onSelectGameMode: (gameMode: GameMode) => void;\n    onSelectMap: (mapName: string, doubleClick: boolean) => void;\n    onSelectSort: (sortType: SortType) => void;\n}\nconst sortMaps = (maps: MapData[], sortType: SortType): MapData[] => {\n    switch (sortType) {\n        case SortType.None:\n            return maps;\n        case SortType.NameAsc:\n            return maps.sort((a, b) => a.mapTitle.localeCompare(b.mapTitle));\n        case SortType.NameDesc:\n            return maps.sort((a, b) => b.mapTitle.localeCompare(a.mapTitle));\n        case SortType.MaxSlotsAsc:\n            return maps.sort((a, b) => a.maxSlots - b.maxSlots);\n        case SortType.MaxSlotsDesc:\n            return maps.sort((a, b) => b.maxSlots - a.maxSlots);\n        default:\n            throw new Error(`Unsupported sort type \"${sortType}\"`);\n    }\n};\nexport const MapSel: React.FC<MapSelProps> = ({ strings, gameModes, maps, selectedGameMode, selectedMapName, initialSortType, onSelectGameMode, onSelectMap, onSelectSort, }) => {\n    const selectedRef = useRef<HTMLDivElement>(null);\n    const [filteredMaps, setFilteredMaps] = useState<MapData[]>(maps);\n    const [searchFilter, setSearchFilter] = useState<string>(\"\");\n    const [sortType, setSortType] = useState<SortType>(initialSortType);\n    useEffect(() => {\n        updateFilteredMaps();\n    }, [maps, searchFilter, sortType]);\n    useEffect(() => {\n        const timeout = setTimeout(() => selectedRef.current?.scrollIntoView(), 50);\n        return () => clearTimeout(timeout);\n    }, [maps]);\n    const updateFilteredMaps = () => {\n        const filtered = maps.filter((map) => map.mapTitle.toLowerCase().includes(searchFilter.toLowerCase()));\n        setFilteredMaps(sortMaps(filtered, sortType));\n    };\n    return React.createElement(\"div\", { className: \"map-sel-form\" }, React.createElement(\"div\", { className: \"map-sel-title\" }, strings.get(\"GUI:SelectEngagement\")), React.createElement(\"div\", { className: \"map-sel-body\" }, React.createElement(\"div\", { className: \"map-sel-game-mode\" }, React.createElement(List, { title: strings.get(\"GUI:GameType\"), className: \"game-mode-list\", tooltip: strings.get(\"STT:ScenarioListGameType\") }, gameModes.map((gameMode) => React.createElement(ListItem, {\n        key: gameMode.id,\n        selected: selectedGameMode?.id === gameMode.id,\n        tooltip: gameMode.description ? strings.get(gameMode.description) : undefined,\n        onClick: () => onSelectGameMode(gameMode),\n    }, strings.get(gameMode.label))))), React.createElement(\"div\", { className: \"map-sel-map\" }, React.createElement(List, {\n        title: React.createElement(\"div\", { className: \"map-list-title\" }, React.createElement(\"div\", null, strings.get(\"GUI:GameMap\")), React.createElement(\"div\", { className: \"map-list-sort\", \"data-r-tooltip\": strings.get(\"STT:SortBy\") }, React.createElement(\"label\", null, \"⇵\"), React.createElement(Select, {\n            initialValue: sortType,\n            onSelect: (value: SortType) => {\n                setSortType(value);\n                onSelectSort(value);\n            },\n            className: \"map-list-sort-select\",\n        }, React.createElement(Option, { value: SortType.None, label: strings.get(\"TS:SortNone\") }), React.createElement(Option, { value: SortType.NameAsc, label: strings.get(\"TS:SortName\") + \" ↓\" }), React.createElement(Option, { value: SortType.NameDesc, label: strings.get(\"TS:SortName\") + \" ↑\" }), React.createElement(Option, { value: SortType.MaxSlotsAsc, label: strings.get(\"TS:SortMaxSlots\") + \" ↓\" }), React.createElement(Option, { value: SortType.MaxSlotsDesc, label: strings.get(\"TS:SortMaxSlots\") + \" ↑\" })))),\n        className: \"map-list\",\n        tooltip: strings.get(\"STT:ScenarioListMaps\"),\n    }, filteredMaps.map((map) => {\n        const isSelected = map.mapName === selectedMapName;\n        return React.createElement(ListItem, {\n            key: map.mapName,\n            selected: isSelected,\n            innerRef: isSelected ? selectedRef : null,\n            onClick: () => onSelectMap(map.mapName, false),\n            onDoubleClick: () => onSelectMap(map.mapName, true),\n        }, map.mapTitle);\n    })), React.createElement(\"div\", { className: \"map-sel-search\" }, React.createElement(\"label\", null, React.createElement(\"span\", null, strings.get(\"GUI:Search\")), React.createElement(\"input\", {\n        type: \"text\",\n        className: \"new-message\",\n        value: searchFilter,\n        onChange: (e: React.ChangeEvent<HTMLInputElement>) => setSearchFilter(e.target.value),\n    }))))));\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/modSel/BadModArchiveError.ts",
    "content": "export class BadModArchiveError extends Error {\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/modSel/DuplicateModError.ts",
    "content": "export class DuplicateModError extends Error {\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/modSel/Mod.ts",
    "content": "import { ModStatus } from \"@/gui/screen/mainMenu/modSel/ModStatus\";\ninterface ModMeta {\n    id: string;\n    name: string;\n    supported: boolean;\n    version?: string;\n    download?: string;\n    downloadSize?: number;\n    manualDownload?: boolean;\n    clone(): ModMeta;\n}\nexport class Mod {\n    public status: ModStatus;\n    public meta: ModMeta;\n    public latestVersion?: string;\n    get id(): string {\n        return this.meta.id;\n    }\n    get name(): string {\n        return this.meta.name;\n    }\n    get supported(): boolean {\n        return this.meta.supported;\n    }\n    constructor(localMeta?: ModMeta, remoteMeta?: ModMeta) {\n        if (localMeta) {\n            if (remoteMeta && remoteMeta.version !== localMeta.version) {\n                this.status = ModStatus.UpdateAvailable;\n                this.meta = localMeta.clone();\n                this.meta.download = remoteMeta.download;\n                this.meta.downloadSize = remoteMeta.downloadSize;\n                this.meta.manualDownload = remoteMeta.manualDownload;\n                this.latestVersion = remoteMeta.version;\n            }\n            else {\n                this.status = ModStatus.Installed;\n                this.meta = localMeta;\n                this.latestVersion = localMeta.version;\n            }\n        }\n        else {\n            this.status = ModStatus.NotInstalled;\n            if (!remoteMeta) {\n                throw new Error(\"At least a local or remote meta must be specified\");\n            }\n            this.meta = remoteMeta;\n            this.latestVersion = remoteMeta.version;\n        }\n    }\n    isInstalled(): boolean {\n        return this.status !== ModStatus.NotInstalled;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/modSel/ModDetailsPane.tsx",
    "content": "import React from \"react\";\nimport { ModStatus } from \"@/gui/screen/mainMenu/modSel/ModStatus\";\ninterface ModDetails {\n    supported: boolean;\n    name: string;\n    description?: string;\n    authors?: string[];\n    version?: string;\n    website?: string;\n}\ninterface ModDetailsPaneProps {\n    modDetails: ModDetails;\n    modLoaded: boolean;\n    modStatus: ModStatus;\n    strings: any;\n}\nconst statusLabels = new Map<ModStatus, string>([\n    [ModStatus.Installed, \"GUI:ModStatusInstalled\"],\n    [ModStatus.UpdateAvailable, \"GUI:ModStatusUpdateAvail\"],\n    [ModStatus.NotInstalled, \"GUI:ModStatusNotInstalled\"],\n]);\nexport const ModDetailsPane: React.FC<ModDetailsPaneProps> = ({ modDetails: { supported, name, description, authors, version, website }, modLoaded, modStatus, strings, }) => React.createElement(\"div\", { className: \"mod-details\" }, React.createElement(\"table\", null, React.createElement(\"tbody\", null, React.createElement(\"tr\", null, React.createElement(\"td\", null, strings.get(\"GUI:ModName\"), \":\"), React.createElement(\"td\", null, name)), React.createElement(\"tr\", null, React.createElement(\"td\", null, strings.get(\"GUI:ModStatus\"), \":\"), React.createElement(\"td\", null, strings.get(statusLabels.get(modStatus) ?? \"GUI:Unknown\"), modLoaded ? \", \" + strings.get(\"GUI:ModLoaded\") : \"\", supported ? \"\" : \", \" + strings.get(\"GUI:ModUnsupported\"))), version &&\n    React.createElement(\"tr\", null, React.createElement(\"td\", null, strings.get(\"GUI:ModVersion\"), \":\"), React.createElement(\"td\", null, version)), description &&\n    React.createElement(\"tr\", null, React.createElement(\"td\", null, strings.get(\"GUI:ModDescription\"), \":\"), React.createElement(\"td\", { className: \"mod-desc\" }, description)), authors &&\n    React.createElement(\"tr\", null, React.createElement(\"td\", null, strings.get(\"GUI:ModAuthor\"), \":\"), React.createElement(\"td\", null, authors.join(\", \"))), website &&\n    React.createElement(\"tr\", null, React.createElement(\"td\", null, strings.get(\"GUI:ModWebsite\"), \":\"), React.createElement(\"td\", null, React.createElement(\"a\", {\n        href: website,\n        rel: \"nofollow noopener\",\n        target: \"_blank\",\n    }, website))))));\n"
  },
  {
    "path": "src/gui/screen/mainMenu/modSel/ModDownloadPrompt.tsx",
    "content": "import React from \"react\";\ninterface ModDownloadPromptProps {\n    url: string;\n    sizeMb: number;\n    isUpdate: boolean;\n    strings: any;\n    onClick: () => void;\n}\nexport const ModDownloadPrompt: React.FC<ModDownloadPromptProps> = ({ url, sizeMb, isUpdate, strings, onClick, }) => React.createElement(\"div\", null, isUpdate &&\n    React.createElement(\"p\", { style: { marginTop: 0 } }, strings.get(\"GUI:ModUpdateAvail\")), React.createElement(\"p\", null, strings.get(\"GUI:ManualDownloadModPrompt\")), React.createElement(\"a\", {\n    href: url,\n    rel: \"nofollow noopener\",\n    target: \"_blank\",\n    onClick: onClick,\n}, url), React.createElement(\"br\", null), React.createElement(\"br\", null), React.createElement(\"em\", null, strings.get(\"ts:gameres_download_size\", sizeMb)));\n"
  },
  {
    "path": "src/gui/screen/mainMenu/modSel/ModImporter.ts",
    "content": "import { sleep } from \"@/util/time\";\nimport { IOError } from \"data/vfs/IOError\";\nimport { ArchiveExtractionError } from \"@/engine/gameRes/importError/ArchiveExtractionError\";\nimport { InvalidArchiveError } from \"@/engine/gameRes/importError/InvalidArchiveError\";\nimport { ModManager } from \"@/gui/screen/mainMenu/modSel/ModManager\";\nimport { ModMeta } from \"@/gui/screen/mainMenu/modSel/ModMeta\";\nimport { BadModArchiveError } from \"@/gui/screen/mainMenu/modSel/BadModArchiveError\";\nimport { IniFile } from \"data/IniFile\";\nimport { DuplicateModError } from \"@/gui/screen/mainMenu/modSel/DuplicateModError\";\nimport { VirtualFile } from \"data/vfs/VirtualFile\";\ninterface MessageBoxApi {\n    alert(message: string, buttonText: string): Promise<void>;\n    confirm(message: string, confirmText: string, cancelText: string): Promise<boolean>;\n}\ninterface Storage {\n    estimate?(): Promise<{\n        quota?: number;\n        usage?: number;\n    }>;\n}\ninterface Directory {\n    listEntries(): Promise<string[]>;\n    getOrCreateDirectory(name: string): Promise<Directory>;\n    getFileHandles(): AsyncIterable<{\n        name: string;\n    }>;\n    deleteFile(name: string): Promise<void>;\n    writeFile(file: VirtualFile): Promise<void>;\n    deleteDirectory(name: string, recursive: boolean): Promise<void>;\n    name: string;\n}\ninterface EmscriptenFS {\n    chdir(path: string): void;\n    open(filename: string, flags: string): number;\n    write(fd: number, buffer: Uint8Array, offset: number, length: number, position: number, canOwn: boolean): void;\n    close(fd: number): void;\n    unlink(filename: string): void;\n    lookupPath(path: string): {\n        node: any;\n    };\n    cwd(): string;\n    stat(filename: string): {\n        size: number;\n    };\n}\ninterface SevenZipModule {\n    FS: EmscriptenFS;\n    callMain(args: string[]): void;\n}\ndeclare const SystemJS: {\n    import(module: string): Promise<any>;\n};\nexport class ModImporter {\n    private static readonly modFileExtensions = [\"mix\", \"big\", \"csf\", \"ini\", \"art\", \"rules\"];\n    private strings: any;\n    private messageBoxApi: MessageBoxApi;\n    private storage: Storage;\n    constructor(strings: any, messageBoxApi: MessageBoxApi, storage: Storage) {\n        this.strings = strings;\n        this.messageBoxApi = messageBoxApi;\n        this.storage = storage;\n    }\n    async import(file: File, modDirectory: Directory, overwrite: boolean, onProgress: (message: string) => void): Promise<ModMeta> {\n        const strings = this.strings;\n        let exitCode: number | undefined;\n        let exitError: any;\n        let sevenZipModule: SevenZipModule;\n        try {\n            const sevenZipFactory = await SystemJS.import(\"7z-wasm\");\n            sevenZipModule = await sevenZipFactory({\n                quit: (code: number, error: any) => {\n                    exitCode = code;\n                    exitError = error;\n                },\n            });\n        }\n        catch (error) {\n            if (error instanceof WebAssembly.RuntimeError) {\n                throw new IOError(\"Couldn't load 7z-wasm\", { cause: error });\n            }\n            throw error;\n        }\n        onProgress(strings.get(\"ts:import_loading_archive\"));\n        sevenZipModule.FS.chdir(\"/tmp\");\n        const fileName = file.name;\n        try {\n            const arrayBuffer = await file.arrayBuffer();\n            const fileDescriptor = sevenZipModule.FS.open(fileName, \"w+\");\n            sevenZipModule.FS.write(fileDescriptor, new Uint8Array(arrayBuffer), 0, arrayBuffer.byteLength, 0, true);\n            sevenZipModule.FS.close(fileDescriptor);\n        }\n        catch (error) {\n            if (error instanceof DOMException) {\n                throw new IOError(`File \"${fileName}\" could not be read (${error.name})`, { cause: error });\n            }\n            throw error;\n        }\n        onProgress(strings.get(\"ts:import_extracting_archive\"));\n        await sleep(100);\n        sevenZipModule.callMain([\"x\", \"-ssc-\", \"-x!*/\", fileName, \"*.*\"]);\n        if (exitCode) {\n            if (exitCode !== 1) {\n                throw new InvalidArchiveError(\"7-Zip exited with code \" + exitCode, { cause: exitError });\n            }\n            if (exitError?.message?.match(/out of memory|allocation/i)) {\n                throw new RangeError(\"Out of memory\", { cause: exitError });\n            }\n            throw new ArchiveExtractionError(\"Archive extraction failed with code \" + exitCode, {\n                cause: exitError,\n            });\n        }\n        sevenZipModule.FS.unlink(fileName);\n        let currentNode = sevenZipModule.FS.lookupPath(sevenZipModule.FS.cwd()).node;\n        let extractedFiles = Object.keys(currentNode.contents);\n        const modMeta = new ModMeta();\n        const cleanup = () => {\n            ({ node: currentNode } = sevenZipModule.FS.lookupPath(sevenZipModule.FS.cwd()));\n            extractedFiles = Object.keys(currentNode.contents);\n            for (const filename of extractedFiles) {\n                sevenZipModule.FS.unlink(filename);\n            }\n        };\n        let totalSize = 0;\n        for (const filename of extractedFiles) {\n            totalSize += sevenZipModule.FS.stat(filename).size;\n        }\n        if (this.storage?.estimate) {\n            try {\n                const estimate = await this.storage.estimate();\n                if (estimate?.quota && estimate.usage) {\n                    const available = estimate.quota - estimate.usage;\n                    if (available < totalSize + 1024 * 1024) {\n                        await this.messageBoxApi.alert(strings.get(\"GUI:InstallModStorageFull\", available / 1024 / 1024, totalSize / 1024 / 1024), strings.get(\"GUI:OK\"));\n                        cleanup();\n                        return modMeta;\n                    }\n                }\n            }\n            catch (error) {\n                console.warn(\"Couldn't get storage estimate\", [error]);\n            }\n        }\n        try {\n            const existingEntries = await modDirectory.listEntries();\n            let modId: string;\n            if (extractedFiles.includes(ModManager.modMetaFileName)) {\n                const metaFile = this.readFileFromEmFs(sevenZipModule.FS, ModManager.modMetaFileName);\n                try {\n                    modMeta.fromIniFile(new IniFile(metaFile.readAsString(\"utf-8\")));\n                }\n                catch (error) {\n                    throw new BadModArchiveError(\"Mod meta file is invalid\");\n                }\n                modId = modMeta.id!;\n                if (!overwrite && existingEntries.find((entry) => entry.toLowerCase() === modId)) {\n                    throw new DuplicateModError(`A mod with the id \"${modMeta.id}\" already exists`);\n                }\n            }\n            else {\n                if (!extractedFiles.some((filename) => ModImporter.modFileExtensions.includes(currentNode.contents[filename].name.toLowerCase().split(\".\").pop()))) {\n                    throw new BadModArchiveError(\"Archive doesn't contain a valid mod\");\n                }\n                if (!(await this.messageBoxApi.confirm(this.strings.get(\"GUI:ImportModUnsupportedWarn\"), this.strings.get(\"GUI:Continue\"), this.strings.get(\"GUI:Cancel\")))) {\n                    cleanup();\n                    return modMeta;\n                }\n                modId = await this.promptFolderName(existingEntries);\n                if (!modId) {\n                    cleanup();\n                    return modMeta;\n                }\n                modMeta.id = modId;\n                modMeta.name = modId;\n            }\n            const targetDirectory = await modDirectory.getOrCreateDirectory(modId);\n            for await (const fileHandle of targetDirectory.getFileHandles()) {\n                await targetDirectory.deleteFile(fileHandle.name);\n            }\n            for (const filename of extractedFiles) {\n                onProgress(strings.get(\"ts:import_importing\", filename));\n                try {\n                    const virtualFile = this.readFileFromEmFs(sevenZipModule.FS, filename);\n                    await targetDirectory.writeFile(virtualFile);\n                }\n                catch (error) {\n                    await modDirectory.deleteDirectory(targetDirectory.name, true);\n                    throw error;\n                }\n                finally {\n                    sevenZipModule.FS.unlink(filename);\n                }\n            }\n            return modMeta;\n        }\n        catch (error) {\n            cleanup();\n            throw error;\n        }\n    }\n    private readFileFromEmFs(fs: EmscriptenFS, filename: string): VirtualFile {\n        const stat = fs.stat(filename);\n        const fd = fs.open(filename, \"r\");\n        const buffer = new Uint8Array(stat.size);\n        fs.close(fd);\n        return new VirtualFile(filename, buffer);\n    }\n    private async promptFolderName(existingEntries: string[]): Promise<string | undefined> {\n        const baseName = \"imported-mod\";\n        let counter = 1;\n        let proposedName = baseName;\n        while (existingEntries.includes(proposedName)) {\n            proposedName = `${baseName}-${counter}`;\n            counter++;\n        }\n        return proposedName;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/modSel/ModManager.ts",
    "content": "import { IniFile } from \"data/IniFile\";\nimport { RouteHelper } from \"RouteHelper\";\nimport { Mod } from \"@/gui/screen/mainMenu/modSel/Mod\";\nimport { ModMeta } from \"@/gui/screen/mainMenu/modSel/ModMeta\";\ninterface Directory {\n    getEntries(): AsyncIterable<string>;\n    containsEntry(name: string): Promise<boolean>;\n    getDirectory(name: string, create: boolean): Promise<Directory>;\n    getRawFile(name: string): Promise<RawFile>;\n    deleteDirectory(name: string, recursive: boolean): Promise<void>;\n}\ninterface RawFile {\n    text(): Promise<string>;\n}\ninterface AppResourceLoader {\n    loadText(fileName: string): Promise<string>;\n}\ninterface Location {\n    href: string;\n}\nexport class ModManager {\n    public static readonly remoteListFileName = \"mods.ini\";\n    public static readonly modMetaFileName = \"modcd.ini\";\n    public static readonly modIdRegex = /^[a-z0-9-_]+$/i;\n    private location: Location;\n    private modDir: Directory;\n    private appResourceLoader: AppResourceLoader;\n    constructor(location: Location, modDir: Directory, appResourceLoader: AppResourceLoader) {\n        this.location = location;\n        this.modDir = modDir;\n        this.appResourceLoader = appResourceLoader;\n    }\n    getModDir(): Directory {\n        return this.modDir;\n    }\n    async buildModList(localMods: ModMeta[], remoteMods?: ModMeta[]): Promise<Mod[]> {\n        const mods: Mod[] = [];\n        const remoteModsCopy = [...(remoteMods ?? [])];\n        for (const localMod of localMods) {\n            const remoteIndex = remoteModsCopy.findIndex((remote) => remote.id === localMod.id);\n            const remoteMod = remoteIndex !== -1 ? remoteModsCopy.splice(remoteIndex, 1)[0] : undefined;\n            mods.push(new Mod(localMod, remoteMod));\n        }\n        for (const remoteMod of remoteModsCopy) {\n            mods.push(new Mod(undefined, remoteMod));\n        }\n        return mods;\n    }\n    async listRemote(): Promise<ModMeta[]> {\n        const iniText = await this.appResourceLoader.loadText(ModManager.remoteListFileName);\n        const iniFile = new IniFile(iniText);\n        const generalSection = iniFile.getSection(\"General\");\n        if (!generalSection) {\n            throw new Error(ModManager.remoteListFileName + \" is missing the [General] section\");\n        }\n        const mods: ModMeta[] = [];\n        for (const modId of generalSection.entries.values()) {\n            const modSection = iniFile.getSection(modId);\n            if (modSection) {\n                const modMeta = new ModMeta().fromIniSection(modSection);\n                mods.push(modMeta);\n            }\n            else {\n                console.warn(`Mod \"${modId}\" has no INI section`);\n            }\n        }\n        return mods;\n    }\n    async listLocal(): Promise<ModMeta[]> {\n        const mods: ModMeta[] = [];\n        if (this.modDir) {\n            for await (const modId of this.modDir.getEntries()) {\n                const modMeta = await this.loadModMeta(modId);\n                mods.push(modMeta);\n            }\n        }\n        mods.sort((a, b) => a.name!.localeCompare(b.name!));\n        return mods;\n    }\n    async loadModMeta(modId: string): Promise<ModMeta> {\n        const modMeta = new ModMeta();\n        modMeta.id = modId;\n        modMeta.name = modId;\n        try {\n            const modDirectory = await this.modDir.getDirectory(modId, true);\n            const metaFile = (await modDirectory.containsEntry(ModManager.modMetaFileName))\n                ? await modDirectory.getRawFile(ModManager.modMetaFileName)\n                : undefined;\n            if (metaFile) {\n                try {\n                    modMeta.fromIniFile(new IniFile(await metaFile.text()));\n                }\n                catch (error) {\n                    console.warn(`Couldn't parse meta file in mod folder \"${modId}\"`);\n                    modMeta.name = modId;\n                }\n                modMeta.id = modId;\n            }\n        }\n        catch (error) {\n            console.warn(error);\n        }\n        return modMeta;\n    }\n    async deleteModFiles(modId: string): Promise<void> {\n        if (await this.modDir?.containsEntry(modId)) {\n            await this.modDir.deleteDirectory(modId, true);\n        }\n    }\n    loadMod(modId?: string): void {\n        const url = new URL(this.location.href);\n        if (modId) {\n            url.searchParams.set(RouteHelper.modQueryStringName, modId);\n        }\n        else {\n            url.searchParams.delete(RouteHelper.modQueryStringName);\n        }\n        this.location.href = url.href;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/modSel/ModMeta.ts",
    "content": "import { ModManager } from \"@/gui/screen/mainMenu/modSel/ModManager\";\ninterface IniSection {\n    getString(key: string): string | undefined;\n    get(key: string): string | string[] | undefined;\n    getNumber(key: string): number | undefined;\n    getBool(key: string): boolean;\n}\ninterface IniFile {\n    getSection(name: string): IniSection | undefined;\n}\nexport class ModMeta {\n    public id?: string;\n    public name?: string;\n    public supported: boolean = false;\n    public description?: string;\n    public authors?: string[];\n    public website?: string;\n    public version?: string;\n    public download?: string;\n    public downloadSize?: number;\n    public manualDownload: boolean = false;\n    fromIniFile(iniFile: IniFile): this {\n        const generalSection = iniFile.getSection(\"General\");\n        if (!generalSection) {\n            throw new Error(\"Mod meta missing [General] section\");\n        }\n        return this.fromIniSection(generalSection);\n    }\n    fromIniSection(section: IniSection): this {\n        const id = section.getString(\"ID\");\n        const name = section.getString(\"Name\");\n        if (!id) {\n            throw new Error(\"Mod meta missing ID\");\n        }\n        if (!id.match(ModManager.modIdRegex)) {\n            throw new Error(`Mod meta has invalid ID \"${id}\". ` +\n                \"ID must contain only alphanumeric characters, dash (-) or underscore (_)\");\n        }\n        if (!name) {\n            throw new Error(\"Mod meta missing Name\");\n        }\n        this.id = id;\n        this.name = name;\n        this.supported = true;\n        this.description = section.getString(\"Description\") || undefined;\n        const authors = section.get(\"Author\");\n        if (authors) {\n            this.authors = Array.isArray(authors) ? authors : [authors];\n        }\n        const website = section.getString(\"Website\");\n        if (website) {\n            if (website.match(/^https?:\\/\\, this.website = website))\n                ;\n        }\n        else {\n            console.warn(`Invalid mod meta website \"${website}\"`);\n        }\n    }\n}\nthis.version = section.getString(\"Version\") || undefined;\nthis.download = section.getString(\"Download\") || undefined;\nthis.downloadSize = section.getNumber(\"DownloadSize\") || undefined;\nthis.manualDownload = section.getBool(\"ManualDownload\");\nreturn this;\nclone();\nModMeta;\n{\n    const cloned = new ModMeta();\n    cloned.id = this.id;\n    cloned.name = this.name;\n    cloned.supported = this.supported;\n    cloned.description = this.description;\n    cloned.authors = this.authors?.slice();\n    cloned.website = this.website;\n    cloned.version = this.version;\n    cloned.download = this.download;\n    cloned.downloadSize = this.downloadSize;\n    cloned.manualDownload = this.manualDownload;\n    return cloned;\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/modSel/ModSel.tsx",
    "content": "import React, { useRef, useEffect } from \"react\";\nimport { List, ListItem } from \"@/gui/component/List\";\nimport { ModDetailsPane } from \"@/gui/screen/mainMenu/modSel/ModDetailsPane\";\ninterface Mod {\n    id: string;\n    name: string;\n    supported: boolean;\n    status: any;\n    meta: any;\n}\ninterface ModSelProps {\n    strings: any;\n    mods: Mod[] | null;\n    activeMod: Mod | null;\n    selectedMod: Mod | null;\n    onSelectMod: (mod: Mod, doubleClick?: boolean) => void;\n}\nexport const ModSel: React.FC<ModSelProps> = ({ strings, mods, activeMod, selectedMod, onSelectMod, }) => {\n    const selectedRef = useRef<HTMLElement>(null);\n    useEffect(() => {\n        selectedRef.current?.scrollIntoView();\n    }, []);\n    return React.createElement(\"div\", { className: \"mod-sel-form\" }, React.createElement(List, { title: strings.get(\"GUI:SelectMod\"), className: \"mod-list\" }, mods\n        ? mods.map((mod) => {\n            const isSelected = mod.id === selectedMod?.id;\n            return React.createElement(ListItem, {\n                key: mod.id,\n                selected: isSelected,\n                innerRef: isSelected ? selectedRef : null,\n                onClick: () => onSelectMod(mod),\n                onDoubleClick: () => onSelectMod(mod, true),\n                style: { display: \"flex\" },\n            }, React.createElement(\"div\", { className: \"mod-name\" }, (mod === activeMod ? \"✔ \" : \"\") +\n                mod.name +\n                (mod.supported\n                    ? \"\"\n                    : ` (${strings.get(\"GUI:ModUnsupported\").toUpperCase()})`)));\n        })\n        : React.createElement(ListItem, { style: { textAlign: \"center\" } }, strings.get(\"GUI:LoadingEx\"))), selectedMod &&\n        React.createElement(ModDetailsPane, {\n            modLoaded: activeMod === selectedMod,\n            modStatus: selectedMod.status,\n            modDetails: selectedMod.meta,\n            strings: strings,\n        }));\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/modSel/ModSelScreen.ts",
    "content": "import React from \"react\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { ScreenType } from \"@/gui/screen/ScreenType\";\nimport { ScreenType as MainMenuScreenType } from \"@/gui/screen/mainMenu/ScreenType\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { StorageQuotaError } from \"data/vfs/StorageQuotaError\";\nimport { MainMenuScreen } from \"@/gui/screen/mainMenu/MainMenuScreen\";\nimport { IOError } from \"data/vfs/IOError\";\nimport { FileNotFoundError } from \"data/vfs/FileNotFoundError\";\nimport { ModSel } from \"@/gui/screen/mainMenu/modSel/ModSel\";\nimport { Engine } from \"@/engine/Engine\";\nimport { FileSystemUtil } from \"@/engine/gameRes/FileSystemUtil\";\nimport { ModImporter } from \"@/gui/screen/mainMenu/modSel/ModImporter\";\nimport { InvalidArchiveError } from \"@/engine/gameRes/importError/InvalidArchiveError\";\nimport { ArchiveExtractionError } from \"@/engine/gameRes/importError/ArchiveExtractionError\";\nimport { BadModArchiveError } from \"@/gui/screen/mainMenu/modSel/BadModArchiveError\";\nimport { DuplicateModError } from \"@/gui/screen/mainMenu/modSel/DuplicateModError\";\nimport { Mod } from \"@/gui/screen/mainMenu/modSel/Mod\";\nimport { ModStatus } from \"@/gui/screen/mainMenu/modSel/ModStatus\";\nimport { CancellationTokenSource, OperationCanceledError } from \"@puzzl/core/lib/async/cancellation\";\nimport { ModDownloadPrompt } from \"@/gui/screen/mainMenu/modSel/ModDownloadPrompt\";\ninterface ModManager {\n    listLocal(): Promise<any[]>;\n    listRemote(): Promise<any[]>;\n    buildModList(local: any[], remote?: any[]): Promise<Mod[]>;\n    deleteModFiles(modId: string): Promise<void>;\n    loadMod(modId?: string): void;\n    getModDir(): any;\n}\ninterface RootController {\n    goToScreen(screenType: any): void;\n}\ninterface ErrorHandler {\n    handle(error: any, message: string, onClose: () => void): void;\n}\ninterface MessageBoxApi {\n    show(message: React.ReactElement | string, buttonText?: string, onClose?: () => void): void;\n    confirm(message: React.ReactElement | string, confirmText: string, cancelText: string): Promise<boolean>;\n    destroy(): void;\n    updateText(text: string): void;\n}\ninterface ModResourceLoader {\n    loadResources(resources: any[], cancellationToken: any, onProgress?: (progress: number) => void): Promise<any>;\n    getResourceFileName(resource: any): string;\n}\ninterface FsAccessLib {\n    showOpenFilePicker(options: any): Promise<any>;\n}\ninterface Sentry {\n    captureException(error: Error): void;\n}\nexport class ModSelScreen extends MainMenuScreen {\n    private rootController: RootController;\n    private strings: any;\n    private jsxRenderer: any;\n    private errorHandler: ErrorHandler;\n    private messageBoxApi: MessageBoxApi;\n    private modManager: ModManager;\n    private activeModId: string;\n    private modSdkUrl?: string;\n    private modResourceLoader: ModResourceLoader;\n    private fsAccessLib: FsAccessLib;\n    private sentry: Sentry;\n    private disposables: CompositeDisposable;\n    private availableMods: Mod[] = [];\n    private activeMod?: Mod;\n    private selectedMod?: Mod;\n    private form?: any;\n    constructor(rootController: RootController, strings: any, jsxRenderer: any, errorHandler: ErrorHandler, messageBoxApi: MessageBoxApi, modManager: ModManager, activeModId: string, modSdkUrl: string | undefined, modResourceLoader: ModResourceLoader, fsAccessLib: FsAccessLib, sentry: Sentry) {\n        super();\n        this.rootController = rootController;\n        this.strings = strings;\n        this.jsxRenderer = jsxRenderer;\n        this.errorHandler = errorHandler;\n        this.messageBoxApi = messageBoxApi;\n        this.modManager = modManager;\n        this.activeModId = activeModId;\n        this.modSdkUrl = modSdkUrl;\n        this.modResourceLoader = modResourceLoader;\n        this.fsAccessLib = fsAccessLib;\n        this.sentry = sentry;\n        this.title = this.strings.get(\"GUI:Mods\");\n        this.disposables = new CompositeDisposable();\n        this.handleSelectMod = async (mod: Mod, doubleClick: boolean) => {\n            const isNewSelection = this.selectedMod?.id !== mod.id;\n            this.selectedMod = mod;\n            if (isNewSelection) {\n                this.updateSidebarButtons();\n            }\n            this.form?.applyOptions((options: any) => {\n                options.selectedMod = mod;\n            });\n            if (doubleClick &&\n                mod !== this.activeMod &&\n                mod.status === ModStatus.Installed) {\n                await this.loadOrUnloadMod(mod);\n            }\n        };\n    }\n    private handleSelectMod: (mod: Mod, doubleClick: boolean) => Promise<void>;\n    async onEnter(): Promise<void> {\n        this.availableMods = [];\n        this.controller.toggleMainVideo(false);\n        this.initForm();\n        const mods = await this.loadAvailableMods();\n        if (mods) {\n            this.availableMods = mods;\n            this.activeMod = this.availableMods.find((mod) => mod.id === this.activeModId);\n            this.selectedMod = this.activeMod;\n            this.form.applyOptions((options: any) => {\n                options.mods = this.availableMods;\n                options.activeMod = this.activeMod;\n                options.selectedMod = this.selectedMod;\n            });\n            this.initSidebar();\n        }\n    }\n    private async loadAvailableMods(): Promise<Mod[] | undefined> {\n        try {\n            const [localMods, remoteMods] = await Promise.all([\n                this.modManager.listLocal(),\n                this.modManager.listRemote().catch((error) => {\n                    console.warn(\"Failed to fetch remote mods\", [error]);\n                    return undefined;\n                }),\n            ]);\n            return await this.modManager.buildModList(localMods, remoteMods);\n        }\n        catch (error: any) {\n            if (!(error instanceof IOError ||\n                error instanceof FileNotFoundError ||\n                error instanceof StorageQuotaError)) {\n                this.sentry?.captureException(new Error(`Failed to load mod list (${error.name ?? error.message})`, { cause: error }));\n            }\n            this.handleError(error, this.strings.get(\"GUI:ModListError\"));\n            return undefined;\n        }\n    }\n    private initForm(): void {\n        this.controller.setMainComponent(this.jsxRenderer.render(jsx(HtmlView, {\n            innerRef: (ref: any) => (this.form = ref),\n            component: ModSel,\n            props: {\n                strings: this.strings,\n                mods: undefined,\n                activeMod: undefined,\n                selectedMod: undefined,\n                onSelectMod: this.handleSelectMod,\n            },\n        }))[0]);\n    }\n    private initSidebar(): void {\n        this.updateSidebarButtons();\n        this.controller.showSidebarButtons();\n    }\n    private updateSidebarButtons(): void {\n        this.controller?.setSidebarButtons([\n            {\n                label: this.selectedMod && this.selectedMod === this.activeMod\n                    ? this.strings.get(\"GUI:UnloadMod\")\n                    : this.selectedMod?.isInstalled()\n                        ? this.strings.get(\"GUI:LoadMod\")\n                        : this.strings.get(\"GUI:ModActionInstall\"),\n                disabled: !this.selectedMod,\n                onClick: async () => {\n                    const mod = this.selectedMod!;\n                    if (mod.status === ModStatus.Installed || mod === this.activeMod) {\n                        await this.loadOrUnloadMod(mod);\n                    }\n                    else {\n                        const sizeMb = (mod.meta.downloadSize || 0) / 1024 / 1024;\n                        if (mod.meta.manualDownload) {\n                            const isUpdate = mod.status === ModStatus.UpdateAvailable;\n                            const promptElement = React.createElement(ModDownloadPrompt, {\n                                url: mod.meta.download,\n                                sizeMb: sizeMb,\n                                isUpdate: isUpdate,\n                                strings: this.strings,\n                                onClick: () => this.messageBoxApi.destroy(),\n                            });\n                            if (isUpdate) {\n                                if (!(await this.messageBoxApi.confirm(promptElement, this.strings.get(\"GUI:Close\"), this.strings.get(\"GUI:ModActionLoadAnyway\")))) {\n                                    await this.loadOrUnloadMod(mod);\n                                }\n                            }\n                            else {\n                                this.messageBoxApi.show(promptElement, this.strings.get(\"GUI:Close\"), () => { });\n                            }\n                        }\n                        else {\n                            let shouldLoadAfterInstall = false;\n                            if (mod.status === ModStatus.UpdateAvailable) {\n                                if (!(await this.messageBoxApi.confirm(this.strings.get(\"GUI:ModUpdateAvail\") +\n                                    \"\\n\\n\" +\n                                    this.strings.get(\"GUI:UpdateModPrompt\", mod.latestVersion, sizeMb), this.strings.get(\"GUI:ModActionUpdate\"), this.strings.get(\"GUI:ModActionLoadAnyway\")))) {\n                                    await this.loadOrUnloadMod(mod);\n                                    return;\n                                }\n                                shouldLoadAfterInstall = true;\n                            }\n                            else if (sizeMb > 10 &&\n                                !(await this.messageBoxApi.confirm(this.strings.get(\"GUI:InstallModDownloadPrompt\", sizeMb), this.strings.get(\"GUI:Continue\"), this.strings.get(\"GUI:Cancel\")))) {\n                                return;\n                            }\n                            let downloadedFile: File;\n                            try {\n                                downloadedFile = await this.downloadMod(mod);\n                            }\n                            catch (error) {\n                                if (error instanceof OperationCanceledError) {\n                                    return;\n                                }\n                                this.errorHandler.handle(error, this.strings.get(\"GUI:DownloadFailed\"), () => { });\n                                return;\n                            }\n                            this.messageBoxApi.destroy();\n                            try {\n                                await this.importModFromFile(downloadedFile, mod.status === ModStatus.UpdateAvailable);\n                            }\n                            catch (error) {\n                                this.handleModImportError(error);\n                                return;\n                            }\n                            if (shouldLoadAfterInstall) {\n                                await this.loadOrUnloadMod(mod);\n                            }\n                        }\n                    }\n                },\n            },\n            {\n                label: this.strings.get(\"GUI:ImportMod\"),\n                tooltip: this.strings.get(\"STT:ImportMod\"),\n                onClick: async () => {\n                    try {\n                        let file: File;\n                        try {\n                            const fileHandle = await FileSystemUtil.showArchivePicker(this.fsAccessLib);\n                            file = await fileHandle.getFile();\n                        }\n                        catch (error: any) {\n                            if (error.name === \"AbortError\")\n                                return;\n                            if (error instanceof DOMException) {\n                                throw new IOError(`File could not be read (${error.name})`, { cause: error });\n                            }\n                            throw error;\n                        }\n                        await this.importModFromFile(file, false);\n                    }\n                    catch (error) {\n                        this.handleModImportError(error);\n                    }\n                },\n            },\n            {\n                label: this.strings.get(\"GUI:UninstallMod\"),\n                tooltip: this.strings.get(\"STT:UninstallMod\"),\n                disabled: !(this.selectedMod?.isInstalled() &&\n                    this.activeMod !== this.selectedMod),\n                onClick: async () => {\n                    const mod = this.selectedMod!;\n                    if (await this.messageBoxApi.confirm(this.strings.get(\"GUI:ConfirmUninstallMod\", mod.name), this.strings.get(\"GUI:Ok\"), this.strings.get(\"GUI:Cancel\"))) {\n                        this.messageBoxApi.show(this.strings.get(\"GUI:WorkingPleaseWait\"));\n                        try {\n                            await this.modManager.deleteModFiles(mod.id);\n                        }\n                        catch (error: any) {\n                            const message = error instanceof StorageQuotaError\n                                ? this.strings.get(\"ts:storage_quota_exceeded\")\n                                : this.strings.get(\"GUI:UninstallModError\");\n                            this.errorHandler.handle(error, message, () => { });\n                            return;\n                        }\n                        finally {\n                            this.messageBoxApi.destroy();\n                        }\n                        const updatedMods = await this.loadAvailableMods();\n                        if (updatedMods) {\n                            this.availableMods = updatedMods;\n                            this.selectedMod = undefined;\n                            this.form?.applyOptions((options: any) => {\n                                options.mods = this.availableMods;\n                                options.selectedMod = undefined;\n                            });\n                            this.updateSidebarButtons();\n                        }\n                    }\n                },\n            },\n            {\n                label: this.strings.get(\"GUI:BrowseMod\"),\n                tooltip: this.strings.get(\"STT:BrowseMod\"),\n                onClick: () => {\n                    this.controller?.pushScreen(ScreenType.OptionsStorage, {\n                        startIn: Engine.rfsSettings.modDir +\n                            (this.selectedMod?.isInstalled() ? \"/\" + this.selectedMod.id : \"\"),\n                    });\n                },\n            },\n            ...(this.modSdkUrl\n                ? [\n                    {\n                        label: this.strings.get(\"GUI:ModSDK\"),\n                        tooltip: this.strings.get(\"STT:ModSDK\"),\n                        onClick: () => {\n                            window.open(this.modSdkUrl, \"_blank\");\n                        },\n                    },\n                ]\n                : []),\n            {\n                label: this.strings.get(\"GUI:Back\"),\n                isBottom: true,\n                onClick: () => {\n                    this.controller?.popScreen();\n                },\n            },\n        ]);\n    }\n    private async loadOrUnloadMod(mod: Mod): Promise<void> {\n        await this.controller?.hideSidebarButtons();\n        this.modManager.loadMod(mod !== this.activeMod ? mod.id : undefined);\n    }\n    private async downloadMod(mod: Mod): Promise<File> {\n        const downloadUrl = mod.meta.download;\n        if (!downloadUrl) {\n            throw new Error(\"Mod meta is missing download\");\n        }\n        const cancellationSource = new CancellationTokenSource();\n        this.messageBoxApi.show(this.strings.get(\"TS:Downloading\"), this.strings.get(\"GUI:Cancel\"), () => cancellationSource.cancel());\n        const resource = {\n            id: \"archive\",\n            src: downloadUrl,\n            type: \"binary\",\n            sizeHint: mod.meta.downloadSize,\n        };\n        const resources = await this.modResourceLoader.loadResources([resource], cancellationSource.token, (progress: number) => {\n            this.messageBoxApi.updateText(this.strings.get(\"TS:DownloadingPg\", progress));\n        });\n        const archiveData = resources.pop(\"archive\");\n        return new File([archiveData], this.modResourceLoader.getResourceFileName(resource));\n    }\n    private async importModFromFile(file: File, overwrite: boolean): Promise<void> {\n        this.messageBoxApi.show(this.strings.get(\"ts:import_preparing_for_import\"));\n        const onProgress = (message: string) => {\n            this.messageBoxApi.updateText(message);\n        };\n        let modMeta: any;\n        try {\n            modMeta = await new ModImporter(this.strings, this.messageBoxApi, navigator.storage).import(file, this.modManager.getModDir(), overwrite, onProgress);\n        }\n        finally {\n            this.messageBoxApi.destroy();\n        }\n        if (modMeta) {\n            const mod = new Mod(modMeta, undefined);\n            if (mod) {\n                const existingIndex = this.availableMods.findIndex((m) => m.id === mod.id);\n                if (existingIndex !== -1) {\n                    this.availableMods.splice(existingIndex, 1, mod);\n                }\n                else {\n                    this.availableMods.unshift(mod);\n                }\n                this.selectedMod = mod;\n                this.form?.applyOptions((options: any) => {\n                    options.mods = this.availableMods;\n                    options.selectedMod = mod;\n                });\n                this.updateSidebarButtons();\n            }\n        }\n    }\n    private handleModImportError(error: any): void {\n        const strings = this.strings;\n        let message = strings.get(\"GUI:ImportModError\");\n        if (error instanceof BadModArchiveError) {\n            message += \"\\n\\n\" + strings.get(\"GUI:ImportModBadArchive\");\n        }\n        else if (error instanceof DuplicateModError) {\n            message += \"\\n\\n\" + strings.get(\"GUI:ImportDuplicateModError\");\n        }\n        else if (error instanceof InvalidArchiveError) {\n            message += \"\\n\\n\" + strings.get(\"ts:import_invalid_archive\");\n        }\n        else if (error instanceof ArchiveExtractionError) {\n            if (error.cause?.message?.match(/out of memory|allocation/i)) {\n                message += \"\\n\\n\" + strings.get(\"ts:import_out_of_memory\");\n            }\n            else {\n                message += \"\\n\\n\" + strings.get(\"ts:import_archive_extract_failed\");\n            }\n        }\n        else if (error.message?.match(/out of memory|allocation/i)) {\n            message += \"\\n\\n\" + strings.get(\"ts:import_out_of_memory\");\n        }\n        else if (error.name === \"QuotaExceededError\" || error instanceof StorageQuotaError) {\n            message += \"\\n\\n\" + strings.get(\"ts:storage_quota_exceeded\");\n        }\n        else if (!(error instanceof IOError || error instanceof FileNotFoundError)) {\n            this.sentry?.captureException(new Error(\"Mod import failed \" + (error.message ?? error.name), { cause: error }));\n        }\n        this.errorHandler.handle(error, message, () => { });\n    }\n    async onStack(): Promise<void> {\n        await this.onLeave();\n    }\n    onUnstack(): void {\n        this.onEnter();\n    }\n    async onLeave(): Promise<void> {\n        this.availableMods.length = 0;\n        this.form = undefined;\n        this.messageBoxApi.destroy();\n        this.disposables.dispose();\n        this.controller.setMainComponent();\n        await this.controller.hideSidebarButtons();\n    }\n    private handleError(error: any, message: string): void {\n        this.errorHandler.handle(error, message, () => {\n            this.rootController.goToScreen(MainMenuScreenType.MainMenuRoot);\n        });\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/modSel/ModStatus.ts",
    "content": "export enum ModStatus {\n    NotInstalled = 0,\n    Installed = 1,\n    UpdateAvailable = 2\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/newAccount/NewAccountBox.tsx",
    "content": "import React, { useState, useRef, useEffect, useImperativeHandle, forwardRef } from \"react\";\nimport { MIN_USERNAME_LEN, MAX_USERNAME_LEN, MAX_PASS_LEN } from \"@/network/WolConfig\";\ninterface Region {\n    id: string;\n    label: string;\n    available: boolean;\n}\ninterface NewAccountFormData {\n    user: string;\n    pass: string;\n    passMatch: boolean;\n    regionId: string;\n}\ninterface NewAccountBoxProps {\n    regions: Region[];\n    initialRegion: Region;\n    strings: any;\n    onRegionChange: (regionId: string) => void;\n    onSubmit: (formData: NewAccountFormData) => void;\n}\ninterface NewAccountBoxRef {\n    submit(): void;\n}\nexport const NewAccountBox = forwardRef<NewAccountBoxRef, NewAccountBoxProps>(({ regions, initialRegion, strings, onRegionChange, onSubmit }, ref) => {\n    const [selectedRegionId, setSelectedRegionId] = useState(initialRegion.id);\n    const formRef = useRef<HTMLFormElement>(null);\n    const usernameRef = useRef<HTMLInputElement>(null);\n    const passwordRef = useRef<HTMLInputElement>(null);\n    const confirmPasswordRef = useRef<HTMLInputElement>(null);\n    useEffect(() => {\n        setTimeout(() => usernameRef.current?.focus(), 50);\n    }, []);\n    const handleSubmit = () => {\n        if (!usernameRef.current || !passwordRef.current || !confirmPasswordRef.current)\n            return;\n        const formData: NewAccountFormData = {\n            user: usernameRef.current.value,\n            pass: passwordRef.current.value,\n            passMatch: passwordRef.current.value === confirmPasswordRef.current.value,\n            regionId: selectedRegionId,\n        };\n        onSubmit(formData);\n    };\n    useImperativeHandle(ref, () => ({\n        submit() {\n            if (formRef.current?.requestSubmit) {\n                formRef.current.requestSubmit();\n            }\n            else {\n                handleSubmit();\n            }\n        },\n    }));\n    return React.createElement(\"div\", { className: \"login-wrapper new-account-box\" }, React.createElement(\"div\", { className: \"title\" }, strings.get(\"GUI:NewAccount\")), React.createElement(\"form\", {\n        onSubmit: (e: React.FormEvent) => {\n            e.preventDefault();\n            handleSubmit();\n        },\n        className: \"login-form login-box\",\n        ref: formRef,\n    }, regions.length > 1\n        ? React.createElement(\"div\", { className: \"field\" }, React.createElement(\"label\", null, strings.get(\"TS:Region\")), React.createElement(\"select\", {\n            name: \"server\",\n            value: selectedRegionId,\n            onChange: (e: React.ChangeEvent<HTMLSelectElement>) => {\n                const regionId = e.target.value;\n                setSelectedRegionId(regionId);\n                onRegionChange(regionId);\n            },\n        }, regions.map((region) => React.createElement(\"option\", { value: region.id, key: region.id, disabled: !region.available }, region.label))))\n        : React.createElement(\"input\", {\n            type: \"hidden\",\n            name: \"server\",\n            value: selectedRegionId,\n        }), React.createElement(\"div\", { className: \"field\" }, React.createElement(\"label\", null, strings.get(\"GUI:Nickname\")), React.createElement(\"input\", {\n        name: \"user\",\n        type: \"text\",\n        required: true,\n        minLength: MIN_USERNAME_LEN,\n        maxLength: MAX_USERNAME_LEN,\n        pattern: \"[a-zA-Z0-9_\\\\-]+\",\n        ref: usernameRef,\n    })), React.createElement(\"div\", { className: \"field\" }, React.createElement(\"label\", null, strings.get(\"GUI:Password\")), React.createElement(\"input\", {\n        name: \"pass\",\n        type: \"password\",\n        required: true,\n        maxLength: MAX_PASS_LEN,\n        ref: passwordRef,\n    })), React.createElement(\"div\", { className: \"field\" }, React.createElement(\"label\", null, strings.get(\"GUI:PasswordConfirm\")), React.createElement(\"input\", {\n        name: \"passConfirm\",\n        type: \"password\",\n        required: true,\n        maxLength: MAX_PASS_LEN,\n        ref: confirmPasswordRef,\n    })), React.createElement(\"button\", {\n        type: \"submit\",\n        style: { visibility: \"hidden\" },\n    })));\n});\n"
  },
  {
    "path": "src/gui/screen/mainMenu/newAccount/NewAccountScreen.ts",
    "content": "import { jsx } from \"@/gui/jsx/jsx\";\nimport { NewAccountBox } from \"@/gui/screen/mainMenu/newAccount/NewAccountBox\";\nimport { ScreenType } from \"@/gui/screen/mainMenu/ScreenType\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { Task } from \"@puzzl/core/lib/async/Task\";\nimport { sleep } from \"@/util/time\";\nimport { StorageKey } from \"LocalPrefs\";\nimport { MainMenuScreen } from \"@/gui/screen/mainMenu/MainMenuScreen\";\nimport { HttpRequest } from \"@/network/HttpRequest\";\ninterface Region {\n    id: string;\n    apiRegUrl: string;\n}\ninterface ServerRegions {\n    isAvailable(regionId: string): boolean;\n    get(regionId: string): Region;\n    getFirstAvailable(): Region | undefined;\n    getAll(): Region[];\n    setSelectedRegion(regionId: string): void;\n}\ninterface LocalPrefs {\n    getItem(key: string): string | undefined;\n    setItem(key: string, value: string): void;\n}\ninterface MessageBoxApi {\n    show(message: string, buttonText?: string, onClose?: () => void): void;\n    destroy(): void;\n}\ninterface ErrorHandler {\n    handle(error: any, message: string, onClose: () => void): void;\n}\ninterface NewAccountFormData {\n    user: string;\n    pass: string;\n    passMatch: boolean;\n    regionId: string;\n}\ninterface NewAccountScreenParams {\n    regionId?: string;\n    afterLogin?: (params: any) => void;\n}\ninterface ApiResponse {\n    error?: string;\n}\nexport class NewAccountScreen extends MainMenuScreen {\n    private appLocale: string;\n    private strings: any;\n    private jsxRenderer: any;\n    private messageBoxApi: MessageBoxApi;\n    private serverRegions: ServerRegions;\n    private errorHandler: ErrorHandler;\n    private localPrefs: LocalPrefs;\n    private newAccountBox?: any;\n    private isBusy: boolean = false;\n    constructor(appLocale: string, strings: any, jsxRenderer: any, messageBoxApi: MessageBoxApi, serverRegions: ServerRegions, errorHandler: ErrorHandler, localPrefs: LocalPrefs) {\n        super();\n        this.appLocale = appLocale;\n        this.strings = strings;\n        this.jsxRenderer = jsxRenderer;\n        this.messageBoxApi = messageBoxApi;\n        this.serverRegions = serverRegions;\n        this.errorHandler = errorHandler;\n        this.localPrefs = localPrefs;\n        this.title = this.strings.get(\"GUI:NewAccount\");\n        this.handleSubmit = async (formData: NewAccountFormData, afterLogin?: (params: any) => void) => {\n            if (!this.isBusy && this.controller) {\n                this.isBusy = true;\n                await this.controller.hideSidebarButtons();\n                const { user, pass, passMatch, regionId } = formData;\n                if (passMatch) {\n                    if (user.match(/^[A-Za-z0-9-_]+$/)) {\n                        await this.createAccount(user, pass, regionId, afterLogin);\n                    }\n                    else {\n                        this.handleValidationError(this.strings.get(\"TS:BadNickname\"));\n                    }\n                }\n                else {\n                    this.handleValidationError(this.strings.get(\"TXT_PASSWORD_VERIFY\"));\n                }\n            }\n        };\n    }\n    private handleSubmit: (formData: NewAccountFormData, afterLogin?: (params: any) => void) => Promise<void>;\n    async onEnter(params: NewAccountScreenParams): Promise<void> {\n        this.controller.toggleMainVideo(false);\n        this.isBusy = false;\n        const savedRegionId = params.regionId ?? this.localPrefs.getItem(StorageKey.PreferredServerRegion);\n        const selectedRegion = savedRegionId && this.serverRegions.isAvailable(savedRegionId)\n            ? this.serverRegions.get(savedRegionId)\n            : this.serverRegions.getFirstAvailable();\n        if (selectedRegion) {\n            this.controller.setSidebarButtons([\n                {\n                    label: this.strings.get(\"GUI:Ok\"),\n                    onClick: () => this.submitForm(),\n                },\n                {\n                    label: this.strings.get(\"GUI:Back\"),\n                    isBottom: true,\n                    onClick: () => {\n                        this.controller?.goToScreen(ScreenType.Login, {\n                            afterLogin: params.afterLogin,\n                        });\n                    },\n                },\n            ]);\n            this.controller.showSidebarButtons();\n            const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n                width: \"100%\",\n                height: \"100%\",\n                component: NewAccountBox,\n                props: {\n                    ref: (ref: any) => (this.newAccountBox = ref),\n                    strings: this.strings,\n                    regions: this.serverRegions.getAll(),\n                    initialRegion: selectedRegion,\n                    onRegionChange: (regionId: string) => {\n                        this.localPrefs.setItem(StorageKey.PreferredServerRegion, regionId);\n                    },\n                    onSubmit: (formData: NewAccountFormData) => this.handleSubmit(formData, params.afterLogin),\n                },\n            }));\n            this.controller.setMainComponent(component);\n        }\n        else {\n            this.handleWolError(\"No servers available\", this.strings.get(\"gui:noserversavailable\"), { fatal: true });\n        }\n    }\n    private submitForm(): void {\n        if (!this.isBusy && this.controller) {\n            this.newAccountBox?.submit();\n        }\n    }\n    private async createAccount(username: string, password: string, regionId: string, afterLogin?: (params: any) => void): Promise<void> {\n        const region = this.serverRegions.get(regionId);\n        this.serverRegions.setSelectedRegion(regionId);\n        const connectingTask = new Task(async (cancellationToken) => {\n            await sleep(1000);\n            if (!cancellationToken.isCancelled()) {\n                this.messageBoxApi.show(this.strings.get(\"TXT_CONNECTING\"));\n            }\n        });\n        connectingTask.start();\n        try {\n            const requestBody = {\n                locale: this.appLocale,\n                user: username,\n                pass: password,\n            };\n            const response = await new HttpRequest().fetchJson<ApiResponse>(region.apiRegUrl, undefined, {\n                method: \"POST\",\n                body: JSON.stringify(requestBody),\n            });\n            connectingTask.cancel();\n            this.messageBoxApi.destroy();\n            if (response.error) {\n                this.handleValidationError(response.error);\n                return;\n            }\n            this.controller?.goToScreen(ScreenType.Login, {\n                useCredentials: { regionId: region.id, user: username, pass: password },\n                afterLogin: afterLogin,\n            });\n        }\n        catch (error) {\n            connectingTask.cancel();\n            this.messageBoxApi.destroy();\n            this.handleWolError(error, this.strings.get(\"TS:ConnectFailed\"), { fatal: false });\n        }\n    }\n    private handleValidationError(message: string): void {\n        this.messageBoxApi.show(message, this.strings.get(\"GUI:Ok\"), () => {\n            this.isBusy = false;\n            this.controller?.showSidebarButtons();\n        });\n    }\n    private handleWolError(error: any, message: string, { fatal }: {\n        fatal: boolean;\n    }): void {\n        this.errorHandler.handle(error, message, () => {\n            this.isBusy = false;\n            if (this.controller) {\n                if (fatal) {\n                    this.controller.goToScreen(ScreenType.Home);\n                }\n                else {\n                    this.controller.showSidebarButtons();\n                }\n            }\n        });\n    }\n    async onLeave(): Promise<void> {\n        this.newAccountBox = undefined;\n        if (!this.isBusy && this.controller) {\n            await this.controller.hideSidebarButtons();\n        }\n        this.isBusy = false;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/patchNotes/PatchNotesScreen.ts",
    "content": "import { jsx } from \"@/gui/jsx/jsx\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { Iframe } from \"@/gui/screen/mainMenu/component/Iframe\";\nimport { MainMenuScreen } from \"@/gui/screen/mainMenu/MainMenuScreen\";\nexport class PatchNotesScreen extends MainMenuScreen {\n    private strings: any;\n    private jsxRenderer: any;\n    private patchNotesUrl: string;\n    constructor(strings: any, jsxRenderer: any, patchNotesUrl: string) {\n        super();\n        this.strings = strings;\n        this.jsxRenderer = jsxRenderer;\n        this.patchNotesUrl = patchNotesUrl;\n        this.title = this.strings.get(\"TS:PatchNotes\");\n    }\n    onEnter(): void {\n        this.controller.setSidebarButtons([\n            {\n                label: this.strings.get(\"GUI:Back\"),\n                isBottom: true,\n                onClick: () => {\n                    this.controller?.leaveCurrentScreen();\n                },\n            },\n        ]);\n        this.controller.showSidebarButtons();\n        this.controller.toggleMainVideo(false);\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            width: \"100%\",\n            height: \"100%\",\n            component: Iframe,\n            props: { src: this.patchNotesUrl, className: \"patch-notes\" },\n        }));\n        this.controller.setMainComponent(component);\n    }\n    async onLeave(): Promise<void> {\n        await this.controller.hideSidebarButtons();\n    }\n    async onStack(): Promise<void> {\n        await this.onLeave();\n    }\n    onUnstack(): void {\n        this.onEnter();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/quickGame/ChatUi.ts",
    "content": "import { CancellationToken } from \"@puzzl/core/lib/async/cancellation\";\nimport { WL_CHANNEL_ID_MIN } from \"@/network/ladder/wladderConfig\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { ChatRecipientType } from \"@/network/chat/ChatMessage\";\nimport { SoundKey } from \"@/engine/sound/SoundKey\";\nimport { ChannelType } from \"@/engine/sound/ChannelType\";\nimport { Task } from \"@puzzl/core/lib/async/Task\";\nimport { ChatHistory } from \"@/gui/chat/ChatHistory\";\nimport { formatTime } from \"@/util/time\";\nimport { ChatInput } from \"@/gui/component/ChatInput\";\nexport class ChatUi {\n    private messages: any[];\n    private updateView: () => void;\n    private wolConfig: any;\n    private wolCon: any;\n    private wolService: any;\n    private wladderService: any;\n    private strings: any;\n    private sound: any;\n    private disposables: CompositeDisposable;\n    private users: any[] = [];\n    private chatHistory: ChatHistory;\n    private playerProfiles: Map<string, any>;\n    private channelName?: string;\n    private ranksUpdateTask?: Task<void>;\n    constructor(messages: any[], updateView: () => void, wolConfig: any, wolCon: any, wolService: any, wladderService: any, strings: any, sound: any) {\n        this.messages = messages;\n        this.updateView = updateView;\n        this.wolConfig = wolConfig;\n        this.wolCon = wolCon;\n        this.wolService = wolService;\n        this.wladderService = wladderService;\n        this.strings = strings;\n        this.sound = sound;\n        this.disposables = new CompositeDisposable();\n        this.users = [];\n        this.chatHistory = new ChatHistory();\n        this.playerProfiles = new Map();\n        this.onChannelJoinLeave = (event: any) => {\n            let channelName = event.channel;\n            const match = channelName.match(/#Lob (\\d+) (\\d)/i);\n            if (match) {\n                const [, channelId, lobbyIndex] = match.map(Number);\n                if (this.wolConfig.getAllQuickMatchChannelIds().includes(channelId)) {\n                    return;\n                }\n                channelName = this.strings.get(\"TXT_LOB_\" + (lobbyIndex + 1));\n            }\n            if (event.user.name === this.wolCon.getCurrentUser()) {\n                this.addSystemMessage(this.strings.get(event.type === \"join\" ? \"TXT_JOINED_S\" : \"TXT_YOULEFT\", channelName));\n            }\n            else if (event.channel === this.channelName) {\n                if (event.type === \"join\") {\n                    this.users.push(event.user);\n                    this.users.sort((a, b) => Number(b.operator) - Number(a.operator));\n                }\n                else {\n                    const userIndex = this.users.findIndex((u) => u.name === event.user.name);\n                    if (userIndex !== -1) {\n                        this.users.splice(userIndex, 1);\n                    }\n                }\n                this.updateView();\n                this.refreshPlayerRanks();\n            }\n        };\n        this.onChannelUsers = (event: any) => {\n            if (event.channelName === this.channelName) {\n                this.users = event.users.slice();\n                this.users.sort((a, b) => Number(b.operator) - Number(a.operator));\n                this.updateView();\n                this.refreshPlayerRanks();\n            }\n        };\n        this.onChannelMessage = (message: any) => {\n            if ((message.to.type !== ChatRecipientType.Page &&\n                message.to.type !== ChatRecipientType.Whisper) ||\n                this.sound.play(SoundKey.IncomingMessage, ChannelType.Ui)) {\n                this.messages.push(message);\n                this.updateView();\n            }\n            if (message.to.type === ChatRecipientType.Whisper &&\n                message.to.name !== this.wolCon.getServerName() &&\n                message.from !== this.wolCon.getCurrentUser()) {\n                this.chatHistory.lastWhisperFrom.value = message.from;\n            }\n        };\n    }\n    private onChannelJoinLeave: (event: any) => void;\n    private onChannelUsers: (event: any) => void;\n    private onChannelMessage: (message: any) => void;\n    private addSystemMessage(text: string): void {\n        this.messages.push({ text });\n        this.updateView();\n    }\n    private refreshPlayerRanks(): void {\n        if (this.wladderService.getUrl()) {\n            this.ranksUpdateTask?.cancel();\n            const task = (this.ranksUpdateTask = new Task(async (cancellationToken: CancellationToken) => {\n                const playerNames = this.users.map((user) => user.name);\n                const profiles = await this.wladderService.listSearch(playerNames, cancellationToken);\n                if (!cancellationToken.isCancelled()) {\n                    for (const profile of profiles) {\n                        this.playerProfiles.set(profile.name, profile);\n                    }\n                    this.updateView();\n                }\n            }));\n            task.start().catch((error) => {\n                if (!(error instanceof Error && error.name === \"OperationCanceledError\")) {\n                    console.error(error);\n                }\n            });\n        }\n    }\n    join(channelName: string): void {\n        this.channelName = channelName;\n        this.wolCon.onJoinChannel.subscribe(this.onChannelJoinLeave);\n        this.wolCon.onLeaveChannel.subscribe(this.onChannelJoinLeave);\n        this.wolCon.onChannelUsers.subscribe(this.onChannelUsers);\n        this.wolCon.onChatMessage.subscribe(this.onChannelMessage);\n    }\n    leave(): void {\n        this.channelName = undefined;\n        this.users.length = 0;\n        this.messages.length = 0;\n        this.playerProfiles.clear();\n        this.ranksUpdateTask?.cancel();\n        this.ranksUpdateTask = undefined;\n        this.wolCon.onJoinChannel.unsubscribe(this.onChannelJoinLeave);\n        this.wolCon.onLeaveChannel.unsubscribe(this.onChannelJoinLeave);\n        this.wolCon.onChannelUsers.unsubscribe(this.onChannelUsers);\n        this.wolCon.onChatMessage.unsubscribe(this.onChannelMessage);\n    }\n    dispose(): void {\n        this.disposables.dispose();\n        this.ranksUpdateTask?.cancel();\n    }\n    getChatProps(): any {\n        return {\n            strings: this.strings,\n            messages: this.messages,\n            channels: this.channelName ? [this.channelName] : [],\n            localUsername: this.wolCon.getCurrentUser(),\n            users: this.users,\n            chatHistory: this.chatHistory,\n            playerProfiles: this.playerProfiles,\n            onSendMessage: (message: any) => {\n                if (message.value.length) {\n                    if (this.wolCon.isOpen() && this.channelName) {\n                        this.wolCon.sendChatMessage(message.value, message.recipient);\n                        if (message.recipient.type === ChatRecipientType.Whisper) {\n                            this.chatHistory.lastWhisperTo.value = message.recipient.name;\n                        }\n                    }\n                }\n                else {\n                    this.addSystemMessage(this.strings.get(\"TXT_ENTER_MESSAGE\"));\n                }\n            },\n        };\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/quickGame/QuickGameScreen.ts",
    "content": "import { Task } from \"@puzzl/core/lib/async/Task\";\nimport { CancellationTokenSource, OperationCanceledError } from \"@puzzl/core/lib/async/cancellation\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { MusicType } from \"@/engine/sound/Music\";\nimport { MainMenuScreen } from \"@/gui/screen/mainMenu/MainMenuScreen\";\nimport { ScreenType } from \"@/gui/screen/mainMenu/ScreenType\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { RANDOM_COUNTRY_ID, RANDOM_COLOR_ID, OBS_COUNTRY_ID } from \"@/game/gameopts/constants\";\nimport { SoundKey } from \"@/engine/sound/SoundKey\";\nimport { ChannelType } from \"@/engine/sound/ChannelType\";\nimport { MainMenuRoute } from \"@/gui/screen/mainMenu/MainMenuRoute\";\nimport { QuickGameForm } from \"@/gui/screen/mainMenu/quickGame/component/QuickGameForm\";\nimport { StorageKey } from \"LocalPrefs\";\nimport { WLadderService } from \"@/network/ladder/WLadderService\";\nimport { LadderQueueType } from \"@/network/ladder/wladderConfig\";\nimport { WolError } from \"@/network/WolError\";\nimport { RPL_QUEUE_LIST, RPL_WORKING, RPL_BAD_VERS, RPL_BAD_HASH, RPL_MODE_UNAVAIL, RPL_MATCHED, RPL_REQUEUE, RPL_STATS } from \"@/network/qmCodes\";\nimport { ChatUi } from \"@/gui/screen/mainMenu/quickGame/ChatUi\";\nenum QueueState {\n    None = 0,\n    Initializing = 1,\n    WaitingForMatch = 2,\n    WaitingForStartTimer = 3,\n    WaitingForGameStart = 4\n}\ninterface QueueOptions {\n    type: LadderQueueType;\n    ranked: boolean;\n    countryId: number;\n    colorId: number;\n}\ninterface QuickGameScreenParams {\n    messages: any[];\n}\ninterface WolConnection {\n    getCurrentUser(): string | undefined;\n    isOpen(): boolean;\n    close(): void;\n    onClose: {\n        subscribe(handler: () => void): void;\n        unsubscribe(handler: () => void): void;\n    };\n    onChatMessage: {\n        subscribe(handler: (msg: any) => void): void;\n        unsubscribe(handler: (msg: any) => void): void;\n    };\n    onLeaveChannel: {\n        subscribe(handler: (event: any) => void): void;\n        unsubscribe(handler: (event: any) => void): void;\n    };\n    onGameStart: {\n        subscribe(handler: (event: any) => void): void;\n        unsubscribe(handler: (event: any) => void): void;\n    };\n}\ninterface WolService {\n    isConnected(): boolean;\n    getConnection(): WolConnection;\n    getConfig(): any;\n    onWolConnectionLost: {\n        subscribe(handler: (error: any) => void): void;\n        unsubscribe(handler: (error: any) => void): void;\n    };\n}\ninterface RootController {\n    joinGame(gameId: string, timestamp: number, gservUrl: string, username: string, tournament: boolean, mapTransfer: boolean, fallbackRoute: MainMenuRoute): void;\n}\ninterface MessageBoxApi {\n    show(message: string, buttonText?: string, onClose?: () => void): void;\n    destroy(): void;\n}\ninterface ErrorHandler {\n    handle(error: any, message: string, onClose: () => void): void;\n}\ninterface LocalPrefs {\n    getItem(key: string): string | undefined;\n    setItem(key: string, value: string): void;\n}\ninterface Sound {\n    play(key: SoundKey, channel: ChannelType): void;\n}\ninterface Rules {\n    getMultiplayerCountries(): any[];\n    getMultiplayerColors(): Map<number, any>;\n}\nexport class QuickGameScreen extends MainMenuScreen {\n    private unrankedEnabled: boolean;\n    private engineVersion: string;\n    private engineModHash: string;\n    private clientLocale: string;\n    private rules: Rules;\n    private wolService: WolService;\n    private wolCon: WolConnection;\n    private wladderService: WLadderService;\n    private serverRegions: any;\n    private rootController: RootController;\n    private messageBoxApi: MessageBoxApi;\n    private jsxRenderer: any;\n    private strings: any;\n    private localPrefs: LocalPrefs;\n    private sound: Sound;\n    private errorHandler: ErrorHandler;\n    private partySize: number = 1;\n    private availableQueueTypes: LadderQueueType[] = Object.values(LadderQueueType);\n    private disposables: CompositeDisposable = new CompositeDisposable();\n    private queueState: QueueState = QueueState.None;\n    private queueOpts!: QueueOptions;\n    private playerProfile?: any;\n    private wolConfig?: any;\n    private chatUi?: ChatUi;\n    private form?: any;\n    private quickMatchChannelName?: string;\n    private countdownSeconds?: number;\n    private countdownIntervalId?: number;\n    constructor(unrankedEnabled: boolean, engineVersion: string, engineModHash: string, clientLocale: string, rules: Rules, wolService: WolService, wolCon: WolConnection, wladderService: WLadderService, serverRegions: any, rootController: RootController, messageBoxApi: MessageBoxApi, jsxRenderer: any, strings: any, localPrefs: LocalPrefs, sound: Sound, errorHandler: ErrorHandler) {\n        super();\n        this.unrankedEnabled = unrankedEnabled;\n        this.engineVersion = engineVersion;\n        this.engineModHash = engineModHash;\n        this.clientLocale = clientLocale;\n        this.rules = rules;\n        this.wolService = wolService;\n        this.wolCon = wolCon;\n        this.wladderService = wladderService;\n        this.serverRegions = serverRegions;\n        this.rootController = rootController;\n        this.messageBoxApi = messageBoxApi;\n        this.jsxRenderer = jsxRenderer;\n        this.strings = strings;\n        this.localPrefs = localPrefs;\n        this.sound = sound;\n        this.errorHandler = errorHandler;\n        this.title = this.strings.get(\"GUI:WolMatch\");\n        this.musicType = MusicType.NormalShuffle;\n        this.handleChatMessage = (message: any) => {\n            if (message.text.startsWith(RPL_QUEUE_LIST + \" \") &&\n                this.queueState === QueueState.None) {\n                const queueListText = message.text.split(\" \").slice(1).join(\" \");\n                const availableTypes = queueListText\n                    .split(\",\")\n                    .filter((type: string) => Object.values(LadderQueueType).includes(type as LadderQueueType));\n                this.availableQueueTypes = availableTypes;\n                if (!availableTypes.includes(this.queueOpts.type) && availableTypes.length) {\n                    this.queueOpts.type = availableTypes[0];\n                    if (this.form) {\n                        this.requestPlayerProfileRefresh();\n                    }\n                }\n                this.form?.applyOptions((options: any) => {\n                    options.enabledTypes = availableTypes;\n                    options.type = this.queueOpts.type;\n                });\n            }\n            if (this.queueState !== QueueState.None &&\n                message.from === this.wolConfig.getQuickMatchBotName()) {\n                if ([RPL_WORKING, RPL_BAD_VERS, RPL_BAD_HASH, RPL_MODE_UNAVAIL].includes(message.text)) {\n                    if (this.queueState === QueueState.Initializing) {\n                        if (message.text === RPL_WORKING) {\n                            this.updateQueueState(QueueState.WaitingForMatch);\n                        }\n                        else {\n                            let errorMessage: string;\n                            let isFatal = true;\n                            if (message.text === RPL_BAD_VERS) {\n                                errorMessage = this.strings.get(\"TS:OutdatedClient\");\n                            }\n                            else if (message.text === RPL_BAD_HASH) {\n                                errorMessage = this.strings.get(\"TXT_MISMATCH\");\n                            }\n                            else if (message.text === RPL_MODE_UNAVAIL) {\n                                errorMessage = this.strings.get(\"WOL:MatchModeUnavail\");\n                                isFatal = false;\n                            }\n                            else {\n                                errorMessage = this.strings.get(\"WOL:MatchBadParameters\");\n                            }\n                            if (!isFatal) {\n                                this.leaveQueue();\n                            }\n                            this.handleError(message.text, errorMessage, { fatal: isFatal });\n                        }\n                    }\n                    else {\n                        console.warn(`Unexpected reply \"${message.text}\" from match bot (qs: ${QueueState[this.queueState]})`);\n                    }\n                }\n                else if (message.text.startsWith(RPL_MATCHED + \" \")) {\n                    if (this.queueState === QueueState.WaitingForMatch) {\n                        this.sound.play(SoundKey.PlayerJoined, ChannelType.Ui);\n                        const countdownStr = message.text.split(\" \")[1];\n                        this.countdownSeconds = Number(countdownStr);\n                        this.updateQueueState(QueueState.WaitingForStartTimer);\n                    }\n                    else {\n                        console.warn(`Unexpected reply \"${message.text}\" from match bot (qs: ${QueueState[this.queueState]})`);\n                    }\n                }\n                else if (message.text === RPL_REQUEUE) {\n                    if ([QueueState.WaitingForGameStart, QueueState.WaitingForStartTimer].includes(this.queueState)) {\n                        console.log(\"A player left. Returned to queue.\");\n                        this.updateQueueState(QueueState.WaitingForMatch);\n                    }\n                }\n                else if (message.text.startsWith(RPL_STATS + \" \") &&\n                    this.queueState === QueueState.WaitingForMatch) {\n                    const statsText = message.text.split(\" \").slice(1).join(\" \");\n                    const [, avgWaitSecondsStr] = statsText.split(\",\");\n                    const avgWaitSeconds = avgWaitSecondsStr !== \"-1\" ? Number(avgWaitSecondsStr) : undefined;\n                    this.updateSidebarText(this.strings.get(\"TXT_SEARCHING_FOR\", this.queueOpts.type) +\n                        \"\\n\\n\" +\n                        this.strings.get(\"WOL:MatchAvgWaitTime\") +\n                        \"\\n\" +\n                        (avgWaitSeconds !== undefined && avgWaitSeconds < 3600\n                            ? this.strings.get(\"WOL:MatchAvgWaitTimeMinutes\", avgWaitSeconds < 60 ? \"<1\" : \"~\" + Math.ceil(avgWaitSeconds / 60))\n                            : this.strings.get(\"WOL:MatchAvgWaitTimeUnavail\")));\n                }\n            }\n        };\n        this.handleLeaveChannel = async (event: any) => {\n            if (event.user.name === this.wolCon.getCurrentUser() &&\n                event.channel === this.quickMatchChannelName) {\n                this.quickMatchChannelName = undefined;\n                if (this.queueState !== QueueState.None) {\n                    this.updateQueueState(QueueState.None);\n                    this.wolCon.close();\n                }\n            }\n        };\n        this.handleGameStart = async (event: any) => {\n            if (this.queueState === QueueState.WaitingForGameStart ||\n                this.queueState === QueueState.WaitingForStartTimer) {\n                try {\n                    const username = this.wolCon.getCurrentUser();\n                    if (username === undefined) {\n                        throw new Error(\"User should be logged in\");\n                    }\n                    this.updateQueueState(QueueState.None);\n                    const fallbackRoute = new MainMenuRoute(ScreenType.Login, {\n                        afterLogin: (messages: any[]) => new MainMenuRoute(ScreenType.QuickGame, { messages }),\n                    });\n                    if (!this.form) {\n                        await this.controller?.popScreen();\n                    }\n                    this.rootController.joinGame(event.gameId, event.timestamp, event.gservUrl, username, true, false, fallbackRoute);\n                }\n                catch (error) {\n                    await this.leaveQueue();\n                    if (!this.wolCon.isOpen()) {\n                        return;\n                    }\n                    this.handleError(error, this.strings.get(\"WOL:MatchTimeout\"), { fatal: false });\n                }\n            }\n        };\n        this.onWolClose = () => {\n            this.updateQueueState(QueueState.None);\n        };\n        this.onWolConLost = (error: any) => {\n            this.handleError(error, this.strings.get(\"TXT_YOURE_DISCON\"), { fatal: true });\n        };\n    }\n    private handleChatMessage: (message: any) => void;\n    private handleLeaveChannel: (event: any) => Promise<void>;\n    private handleGameStart: (event: any) => Promise<void>;\n    private onWolClose: () => void;\n    private onWolConLost: (error: any) => void;\n    async onEnter(params: QuickGameScreenParams): Promise<void> {\n        this.updateQueueState(QueueState.None);\n        const savedCountry = this.localPrefs.getItem(StorageKey.LastPlayerCountry);\n        const savedColor = this.localPrefs.getItem(StorageKey.LastPlayerColor);\n        const savedRanked = this.localPrefs.getItem(StorageKey.LastQueueRanked);\n        const savedType = this.localPrefs.getItem(StorageKey.LastQueueType);\n        const countryId = savedCountry !== undefined &&\n            Number(savedCountry) < this.getAvailablePlayerCountries().length\n            ? Number(savedCountry)\n            : RANDOM_COUNTRY_ID;\n        const colorId = savedColor !== undefined &&\n            Number(savedColor) < this.getAvailablePlayerColors().length\n            ? Number(savedColor)\n            : RANDOM_COLOR_ID;\n        const ranked = savedRanked === undefined || !this.unrankedEnabled || Boolean(Number(savedRanked));\n        const queueType = savedType !== undefined &&\n            Object.values(LadderQueueType).includes(savedType as LadderQueueType)\n            ? (savedType as LadderQueueType)\n            : LadderQueueType.Solo1v1;\n        this.queueOpts = {\n            type: queueType,\n            ranked: ranked,\n            countryId: countryId,\n            colorId: colorId,\n        };\n        this.playerProfile = undefined;\n        this.controller.toggleMainVideo(false);\n        if (this.wolService.isConnected() && this.wolCon.getCurrentUser()) {\n            this.wolConfig = this.wolService.getConfig();\n            this.wolCon.onClose.subscribe(this.onWolClose);\n            this.disposables.add(() => this.wolCon.onClose.unsubscribe(this.onWolClose));\n            this.wolService.onWolConnectionLost.subscribe(this.onWolConLost);\n            this.disposables.add(() => this.wolService.onWolConnectionLost.unsubscribe(this.onWolConLost));\n            this.wolCon.onChatMessage.subscribe(this.handleChatMessage);\n            this.disposables.add(() => this.wolCon.onChatMessage.unsubscribe(this.handleChatMessage));\n            this.wolCon.onLeaveChannel.subscribe(this.handleLeaveChannel);\n            this.disposables.add(() => this.wolCon.onLeaveChannel.unsubscribe(this.handleLeaveChannel));\n            this.wolCon.onGameStart.subscribe(this.handleGameStart);\n            this.disposables.add(() => this.wolCon.onGameStart.unsubscribe(this.handleGameStart));\n            const messages = params.messages;\n            this.chatUi = new ChatUi(messages, (updateData: any) => {\n                this.form?.applyOptions((options: any) => {\n                    const chatProps = options.chatProps;\n                    options.chatProps = { ...chatProps, ...updateData };\n                });\n            }, this.wolConfig, this.wolCon, this.wolService, this.wladderService, this.strings, this.sound);\n            this.initForm();\n            this.initSidebar();\n            this.requestPlayerProfileRefresh();\n            await this.joinQuickMatchChannel();\n        }\n        else {\n            this.messageBoxApi.show(this.strings.get(\"TXT_YOURE_DISCON\"), this.strings.get(\"GUI:Ok\"), () => {\n                this.controller?.goToScreen(ScreenType.Home);\n            });\n        }\n    }\n    private getAvailablePlayerCountries(): any[] {\n        return this.rules.getMultiplayerCountries();\n    }\n    private getAvailablePlayerColors(): any[] {\n        return [...this.rules.getMultiplayerColors().values()];\n    }\n    private updateQueueState(newState: QueueState): void {\n        this.queueState = newState;\n        this.updateSidebarButtons();\n        this.form?.applyOptions((options: any) => {\n            options.searchState = newState;\n        });\n        if (newState === QueueState.WaitingForStartTimer) {\n            this.startCountdown();\n        }\n        else {\n            this.stopCountdown();\n        }\n    }\n    private updateSidebarText(text: string): void {\n        this.controller?.setSidebarMpContent({ text });\n    }\n    private startCountdown(): void {\n        this.stopCountdown();\n        this.countdownIntervalId = window.setInterval(() => {\n            if (this.countdownSeconds !== undefined && this.countdownSeconds > 0) {\n                this.countdownSeconds--;\n                this.updateSidebarText(this.strings.get(\"GUI:MatchFound\") +\n                    \"\\n\\n\" +\n                    this.strings.get(\"GUI:StartingIn\", this.countdownSeconds));\n                if (this.countdownSeconds === 0) {\n                    this.updateQueueState(QueueState.WaitingForGameStart);\n                }\n            }\n        }, 1000);\n    }\n    private stopCountdown(): void {\n        if (this.countdownIntervalId) {\n            clearInterval(this.countdownIntervalId);\n            this.countdownIntervalId = undefined;\n        }\n    }\n    private async joinQuickMatchChannel(): Promise<void> {\n    }\n    private initForm(): void {\n    }\n    private initSidebar(): void {\n        this.updateSidebarButtons();\n        this.controller.showSidebarButtons();\n    }\n    private updateSidebarButtons(): void {\n    }\n    private requestPlayerProfileRefresh(): void {\n    }\n    private async leaveQueue(): Promise<void> {\n    }\n    private handleError(error: any, message: string, { fatal }: {\n        fatal: boolean;\n    }): void {\n        this.errorHandler.handle(error, message, () => {\n            if (fatal) {\n                this.controller?.goToScreen(ScreenType.Home);\n            }\n        });\n    }\n    async onLeave(): Promise<void> {\n        await this.leaveQueue();\n        this.chatUi?.leave();\n        this.chatUi?.dispose();\n        this.disposables.dispose();\n        this.stopCountdown();\n        this.form = undefined;\n        await this.controller.hideSidebarButtons();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/quickGame/component/QuickGameChat.tsx",
    "content": "import React from \"react\";\nimport { Chat } from \"@/gui/component/Chat\";\nimport { List } from \"@/gui/component/List\";\nimport { ChannelUser } from \"@/gui/component/ChannelUser\";\nimport { ChatRecipientType } from \"@/network/chat/ChatMessage\";\ninterface QuickGameChatProps {\n    strings: any;\n    messages: any[];\n    channels: any[];\n    localUsername: string;\n    users: any[];\n    chatHistory: any;\n    playerProfiles: Map<string, any>;\n    onSendMessage: (message: any) => void;\n}\nexport const QuickGameChat: React.FC<QuickGameChatProps> = ({ strings, messages, channels, localUsername, users, chatHistory, playerProfiles, onSendMessage, }) => React.createElement(React.Fragment, null, React.createElement(Chat, {\n    strings: strings,\n    messages: messages,\n    channels: channels ?? [],\n    chatHistory: chatHistory,\n    localUsername: localUsername,\n    onSendMessage: onSendMessage,\n    tooltips: {\n        input: strings.get(\"STT:LobbyEditInput\"),\n        output: strings.get(\"STT:LobbyEditOutput\"),\n        button: strings.get(\"STT:EmoteButton\"),\n    },\n}), React.createElement(List, {\n    className: \"players-list\",\n    tooltip: strings.get(\"STT:LobbyListUsers\"),\n}, users.map((user) => {\n    const playerProfile = playerProfiles.get(user.name);\n    return React.createElement(ChannelUser, {\n        key: user.name,\n        user: user,\n        playerProfile: playerProfile,\n        strings: strings,\n        onClick: () => {\n            chatHistory.lastComposeTarget.value = {\n                type: ChatRecipientType.Whisper,\n                name: user.name,\n            };\n        },\n    });\n})));\n"
  },
  {
    "path": "src/gui/screen/mainMenu/quickGame/component/QuickGameForm.tsx",
    "content": "import React from \"react\";\nimport classnames from \"classnames\";\nimport { Image } from \"@/gui/component/Image\";\nimport { ButtonSelect } from \"@/gui/component/ButtonSelect\";\nimport { ColorSelect } from \"@/gui/component/ColorSelect\";\nimport { CountrySelect } from \"@/gui/component/CountrySelect\";\nimport { Option } from \"@/gui/component/Option\";\nimport { RankIndicator } from \"@/gui/screen/mainMenu/lobby/component/RankIndicator\";\nimport { QuickGameChat } from \"@/gui/screen/mainMenu/quickGame/component/QuickGameChat\";\ninterface QuickGameFormProps {\n    strings: any;\n    disabled: boolean;\n    playerName: string;\n    playerProfile: any;\n    unrankedEnabled: boolean;\n    ranked: boolean;\n    type: string;\n    availableTypes: string[];\n    enabledTypes: string[];\n    chatProps: any;\n    onRankedChange: (ranked: boolean) => void;\n    onTypeChange: (type: string) => void;\n}\nexport const QuickGameForm: React.FC<QuickGameFormProps> = ({ strings, disabled, playerName, playerProfile, unrankedEnabled, ranked, type, availableTypes, enabledTypes, chatProps, onRankedChange, onTypeChange, }) => {\n    return React.createElement(\"div\", { className: \"qm-form\" }, React.createElement(\"div\", { className: \"qm-top\" }, React.createElement(\"div\", { className: \"opts\" }, React.createElement(\"div\", { className: \"item qm-game-type-item\" }, React.createElement(\"label\", null, React.createElement(\"span\", { className: \"label\" }, strings.get(\"GUI:QuickMatchGameMode\")), React.createElement(\"div\", { className: \"qm-game-type\" }, React.createElement(ButtonSelect, {\n        initialValue: type,\n        onSelect: (value: string) => onTypeChange(value),\n        disabled: disabled,\n    }, availableTypes.map((typeValue) => React.createElement(Option, {\n        value: typeValue,\n        label: typeValue,\n        key: typeValue,\n        disabled: !enabledTypes.includes(typeValue),\n    })))))), React.createElement(\"div\", { className: \"item qm-ranked-item\" }, React.createElement(\"label\", null, React.createElement(\"span\", { className: \"label\" }, strings.get(\"GUI:QuickMatchRanked\")), React.createElement(\"div\", { className: \"qm-ranked\" }, React.createElement(ButtonSelect, {\n        initialValue: ranked,\n        onSelect: (value: boolean) => onRankedChange(value),\n        disabled: disabled || !unrankedEnabled,\n    }, React.createElement(Option, {\n        value: true,\n        label: strings.get(\"GUI:Yes\"),\n        key: \"ranked\",\n    }), React.createElement(Option, {\n        value: false,\n        label: strings.get(\"GUI:No\"),\n        key: \"unranked\",\n        disabled: !unrankedEnabled,\n    })))))), React.createElement(\"div\", { className: \"qm-player\" }, React.createElement(\"div\", { className: \"qm-player-info\" }, React.createElement(\"div\", { className: \"qm-player-name\" }, playerName), playerProfile &&\n        React.createElement(RankIndicator, {\n            rank: playerProfile.rank,\n            points: playerProfile.points,\n            strings: strings,\n        })))), React.createElement(\"div\", { className: \"qm-bottom\" }, React.createElement(QuickGameChat, chatProps)));\n};\n"
  },
  {
    "path": "src/gui/screen/mainMenu/score/ScoreScreen.ts",
    "content": "import { jsx } from \"@/gui/jsx/jsx\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { ScoreTable } from \"@/gui/screen/mainMenu/score/ScoreTable\";\nimport { SideType } from \"@/game/SideType\";\nimport { MusicType } from \"@/engine/sound/Music\";\nimport { MainMenuScreen } from \"@/gui/screen/mainMenu/MainMenuScreen\";\nimport { Task } from \"@puzzl/core/lib/async/Task\";\nimport { OperationCanceledError } from \"@puzzl/core/lib/async/cancellation/OperationCanceledError\";\nimport { sleep } from \"@puzzl/core/lib/async/sleep\";\ninterface Game {\n    id: string;\n}\ninterface Player {\n    country?: {\n        side: SideType;\n    };\n}\ninterface ScoreScreenParams {\n    game: Game;\n    localPlayer: Player;\n    singlePlayer: boolean;\n    tournament: boolean;\n    returnTo: {\n        screenType: any;\n        params: any;\n    };\n}\ninterface GameReport {\n    gameId: string;\n}\ninterface WolService {\n    getLastGameReport(): GameReport | undefined;\n}\nconst sideAssets = new Map<SideType, {\n    img: string;\n    pal: string;\n}>([\n    [SideType.GDI, { img: \"mpascrnl.shp\", pal: \"mpascrn.pal\" }],\n    [SideType.Nod, { img: \"mpsscrnl.shp\", pal: \"mpsscrn.pal\" }],\n]);\nexport class ScoreScreen extends MainMenuScreen {\n    private strings: any;\n    private jsxRenderer: any;\n    private wolService: WolService;\n    private scoreTable?: any;\n    private reportUpdateTask?: Task<void>;\n    constructor(strings: any, jsxRenderer: any, wolService: WolService) {\n        super();\n        this.strings = strings;\n        this.jsxRenderer = jsxRenderer;\n        this.wolService = wolService;\n        this.musicType = MusicType.Score;\n    }\n    async onEnter(params: ScoreScreenParams): Promise<void> {\n        this.title = params.singlePlayer\n            ? this.strings.get(\"GUI:SkirmishScore\")\n            : this.strings.get(\"GUI:MultiplayerScore\");\n        this.controller.toggleMainVideo(false);\n        this.initView(params);\n        if (!params.singlePlayer) {\n            this.loadGameReport(params.game);\n        }\n    }\n    private initView({ game, localPlayer, singlePlayer, tournament, returnTo, }: ScoreScreenParams): void {\n        this.controller.setSidebarButtons([\n            {\n                label: this.strings.get(\"GUI:Continue\"),\n                tooltip: this.strings.get(\"STT:MPScoreButtonContinue\"),\n                isBottom: true,\n                onClick: () => {\n                    this.controller?.goToScreen(returnTo.screenType, returnTo.params);\n                },\n            },\n        ]);\n        this.controller.showSidebarButtons();\n        const side = localPlayer.country?.side ?? SideType.GDI;\n        const assets = sideAssets.get(side);\n        if (!assets) {\n            throw new Error(\"Unsupported sideType \" + side);\n        }\n        const [component] = this.jsxRenderer.render(jsx(\"container\", { width: \"100%\", height: \"100%\" }, jsx(\"sprite\", { image: assets.img, palette: assets.pal }), jsx(HtmlView, {\n            width: \"100%\",\n            height: \"100%\",\n            component: ScoreTable,\n            innerRef: (ref: any) => (this.scoreTable = ref),\n            props: {\n                game: game,\n                singlePlayer: singlePlayer,\n                localPlayer: localPlayer,\n                tournament: tournament,\n                strings: this.strings,\n            },\n        })));\n        this.controller.setMainComponent(component);\n    }\n    private loadGameReport(game: Game): void {\n        this.reportUpdateTask?.cancel();\n        const task = (this.reportUpdateTask = new Task(async (cancellationToken) => {\n            while (true) {\n                if (cancellationToken.isCancelled())\n                    return;\n                const report = this.wolService.getLastGameReport();\n                if (report?.gameId === game.id) {\n                    this.scoreTable.applyOptions((options: any) => {\n                        options.gameReport = report;\n                    });\n                    return;\n                }\n                await sleep(1000, cancellationToken);\n            }\n        }));\n        task.start().catch((error) => {\n            if (!(error instanceof OperationCanceledError)) {\n                console.error(error);\n            }\n        });\n    }\n    async onLeave(): Promise<void> {\n        if (this.reportUpdateTask) {\n            this.reportUpdateTask.cancel();\n            this.reportUpdateTask = undefined;\n        }\n        await this.controller.hideSidebarButtons();\n    }\n    async onStack(): Promise<void> {\n        await this.onLeave();\n    }\n    onUnstack(): void {\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/mainMenu/score/ScoreTable.tsx",
    "content": "import React from \"react\";\nimport classnames from \"classnames\";\nimport { aiUiNames } from \"@/game/gameopts/constants\";\nimport { CountryIcon } from \"@/gui/component/CountryIcon\";\nimport { RankIndicator } from \"@/gui/screen/mainMenu/lobby/component/RankIndicator\";\nimport { WolGameReportResult } from \"@/network/WolGameReport\";\nimport { formatTimeDuration } from \"@/util/format\";\ninterface ScoreTableProps {\n    game: any;\n    singlePlayer: boolean;\n    tournament: boolean;\n    localPlayer: any;\n    gameReport?: any;\n    strings: any;\n}\nexport const ScoreTable: React.FC<ScoreTableProps> = ({ game, singlePlayer, tournament, localPlayer, gameReport, strings, }) => {\n    const players = game\n        .getNonNeutralPlayers()\n        .filter((player: any) => !player.isObserver || player.defeated)\n        .sort((a: any, b: any) => b.score - a.score);\n    const showReport = tournament && gameReport;\n    const localPlayerReport = gameReport?.players.find((player: any) => player.name.toLowerCase() === localPlayer.name.toLowerCase());\n    let resultType = localPlayerReport?.resultType;\n    if (resultType === undefined) {\n        if (game.stalemateDetectTrait?.isStale() &&\n            game.stalemateDetectTrait.getCountdownTicks() === 0) {\n            resultType = WolGameReportResult.Draw;\n        }\n        else if (localPlayer.defeated) {\n            if (!game.alliances\n                .getAllies(localPlayer)\n                .filter((ally: any) => !ally.isAi && !ally.defeated).length) {\n                resultType = WolGameReportResult.Loss;\n            }\n        }\n        else if (!localPlayer.isObserver) {\n            resultType = WolGameReportResult.Win;\n        }\n    }\n    return React.createElement(\"div\", { className: \"score-wrapper\" }, (resultType || !singlePlayer) &&\n        React.createElement(\"div\", { className: \"score-title\" }, React.createElement(\"div\", { className: \"game-result\" }, resultType === WolGameReportResult.Win\n            ? strings.get(\"gui:gameresultvictory\")\n            : resultType === WolGameReportResult.Draw\n                ? strings.get(\"gui:gameresultdraw\")\n                : resultType === WolGameReportResult.Loss\n                    ? strings.get(\"gui:gameresultdefeat\")\n                    : \"\"), !gameReport &&\n            !singlePlayer &&\n            (tournament || resultType === undefined) &&\n            React.createElement(\"div\", { className: \"pending-results\" }, strings.get(\"gui:gameresultwaiting\")), localPlayerReport?.points &&\n            React.createElement(\"div\", { className: \"points-gain\" }, (localPlayerReport.points > 0 ? \"+\" : \"\") + localPlayerReport.points)),\n        React.createElement(\"div\", { className: \"score-header\" },\n            React.createElement(\"span\", null, strings.get(\"GUI:Map\") + \": \" + (game.gameOpts?.mapTitle ?? \"\")),\n            React.createElement(\"span\", null, strings.get(\"GUI:Time\") + \": \" + formatTimeDuration(Math.floor(game.currentTime / 1000 / (game.speed?.value ?? 1))))),\n        React.createElement(\"div\", { className: \"score-table-wrapper\" }, React.createElement(\"table\", { className: \"score-table\" }, React.createElement(\"thead\", null, React.createElement(\"tr\", null, React.createElement(\"th\", { className: \"player-col\" }, strings.get(\"GUI:Player\")), React.createElement(\"th\", { className: \"country-col\" }, strings.get(\"GUI:Country\")), React.createElement(\"th\", { className: \"color-col\" }, strings.get(\"GUI:Color\")), React.createElement(\"th\", { className: \"score-col\" }, strings.get(\"GUI:Score\")), React.createElement(\"th\", { className: \"units-col\" }, strings.get(\"GUI:Kills\")), React.createElement(\"th\", { className: \"buildings-col\" }, strings.get(\"GUI:Losses\")), showReport && React.createElement(\"th\", { className: \"rank-col\" }, \"Rank\"))), React.createElement(\"tbody\", null, players.map((player: any) => {\n        const isLocalPlayer = player === localPlayer;\n        const playerReport = gameReport?.players.find((p: any) => p.name.toLowerCase() === player.name.toLowerCase());\n        const rowColor = (typeof player.color === \"string\" ? player.color : player.color?.asHexString?.());\n        return React.createElement(\"tr\", {\n            key: player.name,\n            className: classnames({\n                \"local-player\": isLocalPlayer,\n                defeated: player.defeated,\n            }),\n            style: { color: rowColor },\n        }, React.createElement(\"td\", { className: \"player-col\" }, player.isAi\n            ? strings.get(aiUiNames.get(player.aiDifficulty) || \"GUI:AIDummy\")\n            : player.name), React.createElement(\"td\", { className: \"country-col\" }, React.createElement(CountryIcon, { country: player.country })), React.createElement(\"td\", { className: \"color-col\" }, React.createElement(\"div\", {\n            className: \"color-indicator\",\n            style: {\n                backgroundColor: (typeof player.color === \"string\" ? player.color : player.color?.asHexString?.()),\n                width: 14,\n                height: 14,\n                border: \"1px solid #000\",\n                borderRadius: 2,\n            },\n        })), React.createElement(\"td\", { className: \"score-col\" }, player.score), React.createElement(\"td\", { className: \"units-col\" }, player.getUnitsKilled ? player.getUnitsKilled() : player.unitsKilled), React.createElement(\"td\", { className: \"buildings-col\" }, player.getUnitsLost ? player.getUnitsLost() : (player.unitsLost ?? player.buildingsKilled)), showReport &&\n            React.createElement(\"td\", { className: \"rank-col\" }, playerReport &&\n                React.createElement(RankIndicator, {\n                    playerProfile: {\n                        name: player.name,\n                        rankType: playerReport.rank,\n                    },\n                    strings: strings,\n                })));\n    })))));\n};\n"
  },
  {
    "path": "src/gui/screen/options/GeneralOptions.ts",
    "content": "import { FlyerHelperMode } from \"@/engine/renderable/entity/unit/FlyerHelperMode\";\nimport { Base64 } from \"@/util/Base64\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\nimport { GraphicsOptions } from \"@/gui/screen/options/GraphicsOptions\";\nimport { PerformanceOptions } from \"@/performance/PerformanceOptions\";\nexport const SCROLL_BASE_FACTOR = 3;\nexport class GeneralOptions {\n    scrollRate: BoxedVar<number>;\n    flyerHelper: BoxedVar<FlyerHelperMode>;\n    hiddenObjects: BoxedVar<boolean>;\n    targetLines: BoxedVar<boolean>;\n    rightClickMove: BoxedVar<boolean>;\n    rightClickScroll: BoxedVar<boolean>;\n    mouseAcceleration: BoxedVar<boolean>;\n    graphics: GraphicsOptions;\n    performance: PerformanceOptions;\n    constructor() {\n        this.scrollRate = new BoxedVar(12);\n        this.flyerHelper = new BoxedVar(FlyerHelperMode.Selected);\n        this.hiddenObjects = new BoxedVar(true);\n        this.targetLines = new BoxedVar(true);\n        this.rightClickMove = new BoxedVar(false);\n        this.rightClickScroll = new BoxedVar(true);\n        this.mouseAcceleration = new BoxedVar(true);\n        this.graphics = new GraphicsOptions();\n        this.performance = new PerformanceOptions();\n    }\n    unserialize(data: string): this {\n        const [t, i, r, s, a, n, o, l, p] = data.split(\",\");\n        this.scrollRate.value = Number(t);\n        if (i !== undefined) {\n            this.flyerHelper.value = Number(i) as FlyerHelperMode;\n        }\n        if (r !== undefined) {\n            this.graphics.unserialize(Base64.decode(r));\n        }\n        if (s !== undefined) {\n            this.hiddenObjects.value = Boolean(Number(s));\n        }\n        if (a !== undefined) {\n            this.rightClickMove.value = Boolean(Number(a));\n        }\n        if (n !== undefined) {\n            this.rightClickScroll.value = Boolean(Number(n));\n        }\n        if (o !== undefined) {\n            this.targetLines.value = Boolean(Number(o));\n        }\n        if (l !== undefined) {\n            this.mouseAcceleration.value = Boolean(Number(l));\n        }\n        this.performance.unserialize(p);\n        return this;\n    }\n    serialize(): string {\n        return [\n            this.scrollRate.value,\n            this.flyerHelper.value,\n            Base64.encode(this.graphics.serialize()),\n            Number(this.hiddenObjects.value),\n            Number(this.rightClickMove.value),\n            Number(this.rightClickScroll.value),\n            Number(this.targetLines.value),\n            Number(this.mouseAcceleration.value),\n            this.performance.serialize(),\n        ].join(\",\");\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/options/GraphicsOptions.ts",
    "content": "import { ModelQuality } from \"@/engine/renderable/entity/unit/ModelQuality\";\nimport { ShadowQuality } from \"@/engine/renderable/entity/unit/ShadowQuality\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\ninterface Resolution {\n    width: number;\n    height: number;\n}\nexport class GraphicsOptions {\n    resolution: BoxedVar<Resolution | undefined>;\n    models: BoxedVar<ModelQuality>;\n    shadows: BoxedVar<ShadowQuality>;\n    constructor() {\n        this.resolution = new BoxedVar<Resolution | undefined>(undefined);\n        this.models = new BoxedVar(ModelQuality.High);\n        this.shadows = new BoxedVar(ShadowQuality.High);\n    }\n    unserialize(data: string): this {\n        const [t, i, r] = data.split(\",\");\n        this.models.value = Number(t) as ModelQuality;\n        this.shadows.value = Number(i) as ShadowQuality;\n        if (r !== undefined) {\n            const s = r.length ? r.split(\"x\").map((e) => Number(e)) : undefined;\n            this.resolution.value = s ? { width: s[0], height: s[1] } : undefined;\n        }\n        return this;\n    }\n    serialize(): string {\n        return [\n            this.models.value,\n            this.shadows.value,\n            this.resolution.value\n                ? [this.resolution.value.width, this.resolution.value.height].join(\"x\")\n                : \"\",\n        ].join(\",\");\n    }\n    applyLowPreset(): void {\n        this.models.value = ModelQuality.Low;\n        this.shadows.value = ShadowQuality.Low;\n    }\n    applyHighPreset(): void {\n        this.models.value = ModelQuality.High;\n        this.shadows.value = ShadowQuality.High;\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/options/KeyboardScreen.ts",
    "content": "import { jsx } from \"@/gui/jsx/jsx\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { KeyOpts } from \"@/gui/screen/options/component/KeyOpts\";\nimport { KeyCommandType } from \"@/gui/screen/game/worldInteraction/keyboard/KeyCommandType\";\ninterface ScreenController {\n    setSidebarButtons(buttons: SidebarButton[]): void;\n    showSidebarButtons(): void;\n    hideSidebarButtons(): Promise<void>;\n    setMainComponent(component: any): void;\n    leaveCurrentScreen?(): void;\n}\ninterface SidebarButton {\n    label: string;\n    isBottom?: boolean;\n    onClick: () => void;\n}\ninterface Strings {\n    get(key: string): string;\n}\ninterface KeyBinds {\n    changeHotKey(commandType: KeyCommandType, hotKey: any): void;\n    resetAndReload(): Promise<void>;\n    save(): Promise<void>;\n}\ninterface JsxRenderer {\n    render(element: any): [\n        any\n    ];\n}\nexport class KeyboardScreen {\n    private controller!: ScreenController;\n    private isDirty: boolean = false;\n    public title: string;\n    constructor(private strings: Strings, private jsxRenderer: JsxRenderer, private keyBinds: KeyBinds) {\n        this.title = this.strings.get(\"GUI:KeyboardOptions\");\n    }\n    setController(controller: ScreenController): void {\n        this.controller = controller;\n    }\n    onEnter(): void {\n        this.isDirty = false;\n        this.controller.setSidebarButtons([\n            {\n                label: this.strings.get(\"GUI:Back\"),\n                isBottom: true,\n                onClick: () => {\n                    this.controller?.leaveCurrentScreen();\n                },\n            },\n        ]);\n        this.controller.showSidebarButtons();\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            width: \"100%\",\n            height: \"100%\",\n            component: KeyOpts,\n            props: {\n                keyBinds: this.keyBinds,\n                strings: this.strings,\n                onHotKeyChange: (commandType: KeyCommandType, hotKey: any) => {\n                    this.keyBinds.changeHotKey(commandType, hotKey);\n                    this.isDirty = true;\n                },\n                onResetAll: async () => {\n                    try {\n                        await this.keyBinds.resetAndReload();\n                    }\n                    catch (error) {\n                        console.error(error);\n                    }\n                },\n            },\n        }));\n        this.controller.setMainComponent(component);\n    }\n    async onLeave(): Promise<void> {\n        await this.controller.hideSidebarButtons();\n        if (this.isDirty) {\n            this.isDirty = false;\n            try {\n                await this.keyBinds.save();\n            }\n            catch (error) {\n                console.error(error);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/options/OptionsScreen.ts",
    "content": "import { jsx } from \"@/gui/jsx/jsx\";\nimport { MainMenuController } from \"@/gui/screen/mainMenu/MainMenuController\";\nimport { GameMenuController } from \"@/gui/screen/game/gameMenu/GameMenuController\";\nimport { ScreenType as GameScreenType } from \"@/gui/screen/game/gameMenu/ScreenType\";\nimport { MainMenuScreenType } from \"@/gui/screen/ScreenType\";\nimport { StorageKey } from \"@/LocalPrefs\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { GeneralOpts } from \"@/gui/screen/options/component/GeneralOpts\";\nimport { GeneralOptions } from \"@/gui/screen/options/GeneralOptions\";\ninterface ScreenController {\n    setSidebarButtons(buttons: SidebarButton[]): void;\n    showSidebarButtons(): void;\n    hideSidebarButtons(): Promise<void>;\n    setMainComponent(component: any): void;\n    leaveCurrentScreen?(): void;\n    pushScreen(screenType: any, params?: any): void;\n    toggleMainVideo?(enabled: boolean): void;\n}\ninterface SidebarButton {\n    label: string;\n    isBottom?: boolean;\n    onClick: () => void;\n}\ninterface Strings {\n    get(key: string): string;\n}\ninterface LocalPrefs {\n    setItem(key: StorageKey | string, value: string): void;\n    getItem?(key: StorageKey | string): string | undefined;\n}\ninterface FullScreen {\n    isAvailable?(): boolean;\n    isFullScreen?(): boolean;\n    toggle?(): void;\n    onChange?: {\n        subscribe: (listener: (value: boolean) => void) => void;\n        unsubscribe: (listener: (value: boolean) => void) => void;\n    };\n}\ninterface JsxRenderer {\n    render(element: any): [\n        any\n    ];\n}\nexport class OptionsScreen {\n    private controller!: ScreenController;\n    private initialOptionsStr: string = \"\";\n    public title: string;\n    constructor(private strings: Strings, private jsxRenderer: JsxRenderer, private options: GeneralOptions, private localPrefs: LocalPrefs, private fullScreen: FullScreen, private inGame: boolean, private storageOptsEnabled: boolean) {\n        this.title = this.strings.get(\"GUI:Options\");\n    }\n    setController(controller: ScreenController): void {\n        this.controller = controller;\n    }\n    onEnter(): void {\n        this.initialOptionsStr = this.options.serialize();\n        if (this.controller instanceof MainMenuController) {\n            this.controller.toggleMainVideo?.(false);\n        }\n        const buttons: SidebarButton[] = [\n            {\n                label: this.strings.get(\"GUI:Sound\"),\n                onClick: () => {\n                    if (this.controller instanceof GameMenuController) {\n                        this.controller.pushScreen(GameScreenType.OptionsSound);\n                    }\n                    else {\n                        this.controller?.pushScreen(MainMenuScreenType.OptionsSound);\n                    }\n                },\n            },\n            {\n                label: this.strings.get(\"GUI:Keyboard\"),\n                onClick: () => {\n                    if (this.controller instanceof GameMenuController) {\n                        this.controller.pushScreen(GameScreenType.OptionsKeyboard);\n                    }\n                    else {\n                        this.controller?.pushScreen(MainMenuScreenType.OptionsKeyboard);\n                    }\n                },\n            },\n        ];\n        if (this.controller instanceof MainMenuController && this.storageOptsEnabled) {\n            buttons.push({\n                label: this.strings.get(\"GUI:Storage\"),\n                onClick: () => {\n                    (this.controller as MainMenuController).pushScreen(MainMenuScreenType.OptionsStorage, {});\n                },\n            });\n        }\n        buttons.push({\n            label: this.strings.get(\"GUI:Back\"),\n            isBottom: true,\n            onClick: () => {\n                this.controller?.leaveCurrentScreen();\n            },\n        });\n        this.controller.setSidebarButtons(buttons);\n        this.controller.showSidebarButtons();\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            width: \"100%\",\n            height: \"100%\",\n            component: GeneralOpts,\n            props: {\n                options: this.options,\n                fullScreen: this.fullScreen,\n                strings: this.strings,\n                inGame: this.inGame,\n                localPrefs: this.localPrefs,\n            },\n        }));\n        this.controller.setMainComponent(component);\n    }\n    async onLeave(): Promise<void> {\n        const optionsStr = this.options.serialize();\n        if (optionsStr !== this.initialOptionsStr) {\n            this.localPrefs.setItem(StorageKey.Options, optionsStr);\n        }\n        await this.controller.hideSidebarButtons();\n    }\n    async onStack(): Promise<void> {\n        await this.onLeave();\n    }\n    onUnstack(): void {\n        this.onEnter();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/options/SoundOptsScreen.ts",
    "content": "import { jsx } from \"@/gui/jsx/jsx\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { SoundOpts } from \"@/gui/screen/options/component/SoundOpts\";\nimport { StorageKey } from \"@/LocalPrefs\";\ninterface ScreenController {\n    setSidebarButtons(buttons: SidebarButton[]): void;\n    showSidebarButtons(): void;\n    hideSidebarButtons(): Promise<void>;\n    setMainComponent(component: any): void;\n    leaveCurrentScreen?(): void;\n}\ninterface SidebarButton {\n    label: string;\n    isBottom?: boolean;\n    onClick: () => void;\n}\ninterface Strings {\n    get(key: string): string;\n}\ninterface Mixer {\n    serialize(): string;\n}\ninterface Music {\n    serializeOptions(): string;\n}\ninterface LocalPrefs {\n    setItem(key: StorageKey, value: string): void;\n}\ninterface JsxRenderer {\n    render(element: any): [\n        any\n    ];\n}\nexport class SoundOptsScreen {\n    private controller!: ScreenController;\n    private initialSettings: string = \"\";\n    public title: string;\n    constructor(private strings: Strings, private jsxRenderer: JsxRenderer, private mixer: Mixer, private music: Music, private localPrefs: LocalPrefs) {\n        this.title = this.strings.get(\"GUI:Sound\");\n    }\n    setController(controller: ScreenController): void {\n        this.controller = controller;\n    }\n    onEnter(): void {\n        this.initialSettings = this.mixer.serialize();\n        this.controller.setSidebarButtons([\n            {\n                label: this.strings.get(\"GUI:Back\"),\n                isBottom: true,\n                onClick: () => {\n                    this.controller?.leaveCurrentScreen();\n                },\n            },\n        ]);\n        this.controller.showSidebarButtons();\n        const [component] = this.jsxRenderer.render(jsx(HtmlView, {\n            width: \"100%\",\n            height: \"100%\",\n            component: SoundOpts,\n            props: {\n                mixer: this.mixer,\n                music: this.music,\n                strings: this.strings,\n            },\n        }));\n        this.controller.setMainComponent(component);\n    }\n    async onLeave(): Promise<void> {\n        const mixerSettings = this.mixer.serialize();\n        if (mixerSettings !== this.initialSettings) {\n            this.localPrefs.setItem(StorageKey.Mixer, mixerSettings);\n        }\n        if (this.music) {\n            const musicOptions = this.music.serializeOptions();\n            this.localPrefs.setItem(StorageKey.MusicOpts, musicOptions);\n        }\n        await this.controller.hideSidebarButtons();\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/options/StorageScreen.ts",
    "content": "import { jsx } from '../../jsx/jsx';\nimport { HtmlView } from '../../jsx/HtmlView';\nimport StorageExplorer from './component/StorageExplorer';\nimport { MainMenuScreen } from '../mainMenu/MainMenuScreen';\nimport { Strings } from '../../../data/Strings';\nimport { JsxRenderer } from '../../jsx/JsxRenderer';\nimport { MessageBoxApi } from '../../component/MessageBoxApi';\nimport { Engine } from '../../../engine/Engine';\nexport class StorageScreen extends MainMenuScreen {\n    private strings: Strings;\n    private messageBoxApi: MessageBoxApi;\n    private appVersion: string;\n    private storageEnabled: boolean;\n    private quickMatchEnabled: boolean;\n    declare title: string;\n    constructor(strings: Strings, messageBoxApi: MessageBoxApi, appVersion: string, storageEnabled: boolean = false, quickMatchEnabled: boolean = false) {\n        super();\n        this.strings = strings;\n        this.messageBoxApi = messageBoxApi;\n        this.appVersion = appVersion;\n        this.storageEnabled = storageEnabled;\n        this.quickMatchEnabled = quickMatchEnabled;\n        this.title = this.strings.get(\"GUI:Storage\") || \"Storage\";\n    }\n    onEnter(params?: any): void {\n        console.log('[StorageScreen] Entering storage screen');\n        if (this.controller) {\n            this.controller.setSidebarButtons([\n                {\n                    label: this.strings.get(\"GUI:Back\") || \"Back\",\n                    isBottom: true,\n                    onClick: () => {\n                        console.log('[StorageScreen] Back button clicked');\n                        this.controller?.leaveCurrentScreen();\n                    },\n                },\n            ]);\n            this.controller.showSidebarButtons();\n            const mainMenuController = this.controller as any;\n            const jsxRenderer = mainMenuController.mainMenu?.jsxRenderer;\n            const rfs = Engine.rfs;\n            if (!jsxRenderer) {\n                console.error('[StorageScreen] JSX renderer not available from main menu');\n                return;\n            }\n            const storageDirHandle = rfs?.getRootDirectoryHandle();\n            if (!storageDirHandle) {\n                console.error('[StorageScreen] No storage directory handle available');\n                const ErrorComponent = () => {\n                    return jsx('div', { style: { padding: '20px', textAlign: 'center' } }, jsx('h3', null, 'Storage Error'), jsx('p', null, 'No storage directory handle available. Please ensure the game resources are properly imported.'));\n                };\n                const [errorElement] = jsxRenderer.render(jsx(HtmlView, {\n                    width: \"100%\",\n                    height: \"100%\",\n                    component: ErrorComponent,\n                    props: {}\n                }));\n                this.controller.setMainComponent(errorElement);\n                return;\n            }\n            const messageBoxApi = this.messageBoxApi;\n            const [element] = jsxRenderer.render(jsx(HtmlView, {\n                width: \"100%\",\n                height: \"100%\",\n                component: StorageExplorer,\n                props: {\n                    strings: this.strings,\n                    messageBoxApi: messageBoxApi,\n                    storageDirHandle: storageDirHandle,\n                    startIn: params?.startIn,\n                    onFileSystemChange: () => {\n                        console.log('[StorageScreen] File system changed, updating sidebar');\n                        this.controller?.setSidebarButtons([\n                            {\n                                label: this.strings.get(\"GUI:ExitAndReload\") || \"Exit and Reload\",\n                                isBottom: true,\n                                onClick: () => {\n                                    console.log('[StorageScreen] Exit and Reload clicked');\n                                    location.reload();\n                                },\n                            },\n                        ]);\n                    },\n                },\n            }));\n            this.controller.setMainComponent(element);\n        }\n    }\n    async onLeave(): Promise<void> {\n        console.log('[StorageScreen] Leaving storage screen');\n        if (this.controller) {\n            await this.controller.hideSidebarButtons();\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/options/component/GeneralOpts.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { Slider } from \"@/gui/component/Slider\";\nimport { SCROLL_BASE_FACTOR, GeneralOptions } from \"@/gui/screen/options/GeneralOptions\";\nimport { Select } from \"@/gui/component/Select\";\nimport { Option } from \"@/gui/component/Option\";\nimport { FlyerHelperMode } from \"@/engine/renderable/entity/unit/FlyerHelperMode\";\nimport { ModelQuality } from \"@/engine/renderable/entity/unit/ModelQuality\";\nimport { ShadowQuality } from \"@/engine/renderable/entity/unit/ShadowQuality\";\nimport { Image } from \"@/gui/component/Image\";\nimport { ResolutionSelect } from \"@/gui/screen/options/component/Resolution\";\ninterface Strings {\n    get(key: string): string;\n}\ninterface FullScreen {\n    isAvailable?(): boolean;\n    isFullScreen?(): boolean;\n    toggle?(): void;\n    onChange?: {\n        subscribe: (listener: (value: boolean) => void) => void;\n        unsubscribe: (listener: (value: boolean) => void) => void;\n    };\n}\ninterface LocalPrefs {\n    getItem?(key: string): string | undefined;\n    setItem?(key: string, value: string): void;\n}\ninterface GeneralOptsProps {\n    strings: Strings;\n    options: GeneralOptions;\n    fullScreen: FullScreen;\n    inGame: boolean;\n    localPrefs?: LocalPrefs;\n}\nconst speedLabels = new Map([\n    [1, \"TXT_SLOWEST\"],\n    [2, \"TXT_SLOWER\"],\n    [3, \"TXT_SLOW\"],\n    [4, \"TXT_MEDIUM\"],\n    [5, \"TXT_FAST\"],\n    [6, \"TXT_FASTER\"],\n    [7, \"TXT_FASTEST\"],\n]);\nconst isCoarsePointer = () => !!window.matchMedia?.(\"(pointer: coarse)\")?.matches;\nconst getJoystickPreference = (localPrefs?: LocalPrefs) => {\n    const storedValue = localPrefs?.getItem?.(\"ra2web.mobileJoystickLite.enabled\");\n    if (storedValue === \"0\") {\n        return false;\n    }\n    if (storedValue === \"1\") {\n        return true;\n    }\n    return isCoarsePointer();\n};\nconst performanceOptionItems = [\n    {\n        key: 'raycastHelperReuse',\n        label: 'Raycast Helper Reuse',\n    },\n    {\n        key: 'entityIntersectTraversal',\n        label: 'Entity Intersect Traversal',\n    },\n    {\n        key: 'mapTileHitTest',\n        label: 'Map Tile Hit Test',\n    },\n    {\n        key: 'worldViewportCache',\n        label: 'World Viewport Cache',\n    },\n    {\n        key: 'worldSoundLoopCache',\n        label: 'World Sound Loop Cache',\n    },\n    {\n        key: 'telemetry',\n        label: 'Telemetry & Benchmarks',\n    },\n] as const;\nexport const GeneralOpts: React.FC<GeneralOptsProps> = ({ strings, options, fullScreen, inGame, localPrefs, }) => {\n    const [mobileLayout, setMobileLayout] = useState(() => isCoarsePointer());\n    const [mobileJoystickEnabled, setMobileJoystickEnabled] = useState(() => getJoystickPreference(localPrefs));\n    useEffect(() => {\n        const handleEnvironmentChange = () => {\n            setMobileLayout(isCoarsePointer());\n        };\n        window.addEventListener(\"resize\", handleEnvironmentChange);\n        window.visualViewport?.addEventListener(\"resize\", handleEnvironmentChange);\n        return () => {\n            window.removeEventListener(\"resize\", handleEnvironmentChange);\n            window.visualViewport?.removeEventListener(\"resize\", handleEnvironmentChange);\n        };\n    }, []);\n    return (<div className=\"opts general-opts\">\n    <fieldset>\n      <legend>{strings.get(\"TS:GameplayOpts\")}</legend>\n      <div className=\"slider-item\">\n        <span className=\"label\">{strings.get(\"GUI:ScrollRate\")}</span>\n        <Slider min={1} max={7} value={String(Math.floor(options.scrollRate.value / SCROLL_BASE_FACTOR))} getLabel={(value) => strings.get(speedLabels.get(Number(value))!)} onChange={(e) => (options.scrollRate.value =\n            Number(e.target.value) * SCROLL_BASE_FACTOR)}/>\n      </div>\n      <div className=\"item\" data-r-tooltip={strings.get(\"STT:MouseAccel\")}>\n        <label>\n          <span className=\"label\">{strings.get(\"TS:MouseAccel\")}</span>\n          <Select initialValue={String(Number(options.mouseAcceleration.value))} onSelect={(value) => (options.mouseAcceleration.value = Boolean(Number(value)))}>\n            <Option value=\"1\" label={strings.get(\"TXT_ON\")}/>\n            <Option value=\"0\" label={strings.get(\"TXT_OFF\")}/>\n          </Select>\n          <span className=\"info\" title={strings.get(\"TS:MouseAccelHint\")}>\n            <Image src=\"info.png\"/>\n          </span>\n        </label>\n      </div>\n      <div className=\"item\" data-r-tooltip={strings.get(\"STT:AttackMoveButton\")}>\n        <label>\n          <span className=\"label\">{strings.get(\"TS:AttackMoveButton\")}</span>\n          <Select initialValue={String(Number(options.rightClickMove.value))} onSelect={(value) => (options.rightClickMove.value = Boolean(Number(value)))}>\n            <Option value=\"0\" label={strings.get(\"TS:AttackMoveButtonLeft\")}/>\n            <Option value=\"1\" label={strings.get(\"TS:AttackMoveButtonRight\")}/>\n          </Select>\n        </label>\n      </div>\n      <div className=\"item\" data-r-tooltip={strings.get(\"STT:RightClickScroll\")}>\n        <label>\n          <span className=\"label\">{strings.get(\"TS:RightClickScroll\")}</span>\n          <Select initialValue={String(Number(options.rightClickScroll.value))} onSelect={(value) => (options.rightClickScroll.value = Boolean(Number(value)))}>\n            <Option value=\"1\" label={strings.get(\"TXT_ON\")}/>\n            <Option value=\"0\" label={strings.get(\"TXT_OFF\")}/>\n          </Select>\n        </label>\n      </div>\n      <div className=\"item\" data-r-tooltip={strings.get(\"STT:FlyerLabel\")}>\n        <span className=\"label\">{strings.get(\"TS:FlyerLabel\")}</span>\n        <Select initialValue={String(options.flyerHelper.value)} onSelect={(value) => (options.flyerHelper.value = Number(value) as FlyerHelperMode)}>\n          <Option value={String(FlyerHelperMode.Always)} label={strings.get(\"TS:FlyerAlways\")}/>\n          <Option value={String(FlyerHelperMode.Selected)} label={strings.get(\"TS:FlyerSelected\")}/>\n          <Option value={String(FlyerHelperMode.Never)} label={strings.get(\"TS:FlyerNever\")}/>\n        </Select>\n      </div>\n      <div className=\"item\" data-r-tooltip={strings.get(\"STT:IGGameOptCBoxHidden\")}>\n        <label>\n          <span className=\"label\">{strings.get(\"GUI:ShowHidden\")}</span>\n          <input type=\"checkbox\" defaultChecked={options.hiddenObjects.value} onChange={(e) => (options.hiddenObjects.value = e.target.checked)}/>\n        </label>\n      </div>\n      <div className=\"item\" data-r-tooltip={strings.get(\"STT:IGGameOptCBoxTargetLines\")}>\n        <label>\n          <span className=\"label\">{strings.get(\"GUI:TargetLines\")}</span>\n          <input type=\"checkbox\" defaultChecked={options.targetLines.value} onChange={(e) => (options.targetLines.value = e.target.checked)}/>\n        </label>\n      </div>\n      {mobileLayout && (<div className=\"item\">\n          <label>\n            <span className=\"label\">摇杆</span>\n            <input type=\"checkbox\" checked={mobileJoystickEnabled} onChange={(event) => {\n                const enabled = event.target.checked;\n                const globalWindow = window as any;\n                setMobileJoystickEnabled(enabled);\n                localPrefs?.setItem?.(\"ra2web.mobileJoystickLite.enabled\", enabled ? \"1\" : \"0\");\n                document.documentElement.dataset.ra2JoystickLite = enabled ? \"1\" : \"0\";\n                globalWindow.__ra2webVirtualJoystickLite?.setEnabled?.(enabled, false);\n            }}/>\n          </label>\n        </div>)}\n    </fieldset>\n    <fieldset>\n      <legend>{strings.get(\"TS:GfxOpts\")}</legend>\n      <div className=\"item\">\n        <span className=\"label\">{strings.get(\"TS:Resolution\")}</span>\n        <ResolutionSelect resolution={options.graphics.resolution} fullScreen={fullScreen as any} strings={strings}/>\n        <span className=\"info\" title={strings.get(\"TS:ResolutionHint\")}>\n          <Image src=\"info.png\"/>\n        </span>\n      </div>\n      <div className=\"item\" data-r-tooltip={strings.get(\"STT:GfxModels\")}>\n        <span className=\"label\">{strings.get(\"TS:GfxModels\")}</span>\n        <Select disabled={inGame} initialValue={String(options.graphics.models.value)} onSelect={(value) => (options.graphics.models.value = Number(value) as ModelQuality)}>\n          <Option value={String(ModelQuality.High)} label={strings.get(\"TS:GfxQualityHigh\")}/>\n          <Option value={String(ModelQuality.Low)} label={strings.get(\"TS:GfxQualityLow\")}/>\n        </Select>\n      </div>\n      <div className=\"item\" data-r-tooltip={strings.get(\"STT:GfxShadows\")}>\n        <span className=\"label\">{strings.get(\"TS:GfxShadows\")}</span>\n        <Select initialValue={String(options.graphics.shadows.value)} onSelect={(value) => (options.graphics.shadows.value = Number(value) as ShadowQuality)}>\n          <Option value={String(ShadowQuality.High)} label={strings.get(\"TS:GfxQualityHigh\")}/>\n          <Option value={String(ShadowQuality.Medium)} label={strings.get(\"TS:GfxQualityMed\")}/>\n          <Option value={String(ShadowQuality.Low)} label={strings.get(\"TS:GfxQualityLow\")}/>\n          <Option value={String(ShadowQuality.Off)} label={strings.get(\"TS:GfxQualityOff\")}/>\n        </Select>\n      </div>\n    </fieldset>\n    <fieldset>\n      <legend>Performance</legend>\n      {performanceOptionItems.map((item) => (<div className=\"item\" key={item.key}>\n          <label>\n            <span className=\"label\">{item.label}</span>\n            <input type=\"checkbox\" defaultChecked={options.performance[item.key].value} onChange={(event) => (options.performance[item.key].value = event.target.checked)}/>\n          </label>\n        </div>))}\n    </fieldset>\n  </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/options/component/KeyOpts.tsx",
    "content": "import React, { useState } from \"react\";\nimport { configurableCmds } from \"@/gui/screen/options/component/configurableCmds\";\nimport { List, ListItem } from \"@/gui/component/List\";\nimport { PressKeyInput } from \"@/gui/screen/options/component/PressKeyInput\";\nimport { getHumanReadableKey } from \"@/gui/screen/options/component/getHumanReadableKey\";\nimport { KeyboardHandler } from \"@/gui/screen/game/worldInteraction/keyboard/KeyboardHandler\";\nimport { KeyCommandType } from \"@/gui/screen/game/worldInteraction/keyboard/KeyCommandType\";\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface KeyEvent {\n    keyCode?: number;\n    shiftKey: boolean;\n    ctrlKey: boolean;\n    altKey: boolean;\n    metaKey: boolean;\n}\ninterface KeyBinds {\n    getHotKey(commandType: KeyCommandType): KeyEvent | undefined;\n    getCommandType(keyEvent: KeyEvent): KeyCommandType | undefined;\n    changeHotKey(commandType: KeyCommandType, keyEvent: KeyEvent | undefined): void;\n}\ninterface KeyOptsProps {\n    strings: Strings;\n    keyBinds: KeyBinds;\n    onResetAll?: () => Promise<void>;\n    onHotKeyChange?: (commandType: KeyCommandType, keyEvent: KeyEvent | undefined) => void;\n}\nexport const KeyOpts: React.FC<KeyOptsProps> = ({ strings, keyBinds, onResetAll, onHotKeyChange, }) => {\n    const [selectedCommand, setSelectedCommand] = useState<KeyCommandType | undefined>();\n    const [currentCommandType, setCurrentCommandType] = useState<KeyCommandType | undefined>();\n    const [currentKeyEvent, setCurrentKeyEvent] = useState<KeyEvent | undefined>();\n    const [inputKey, setInputKey] = useState(0);\n    const [errorMessage, setErrorMessage] = useState<string | undefined>();\n    const getLabel = (labelOrFunc: string | ((strings: Strings) => string)): string => {\n        return typeof labelOrFunc === \"function\" ? labelOrFunc(strings) : strings.get(labelOrFunc);\n    };\n    const selectedCommandDesc = selectedCommand && configurableCmds.has(selectedCommand)\n        ? configurableCmds.get(selectedCommand)!.desc\n        : undefined;\n    const descriptionText = selectedCommandDesc\n        ? (typeof selectedCommandDesc === \"function\" ? selectedCommandDesc(strings) : strings.get(selectedCommandDesc))\n        : undefined;\n    const currentHotKey = selectedCommand ? keyBinds.getHotKey(selectedCommand) : undefined;\n    return (<div className=\"opts key-opts\">\n      <div className=\"key-opts-list\">\n        <div className=\"key-opts-left\">\n          <List title={strings.get(\"GUI:Commands\")} className=\"key-list\">\n            {[...configurableCmds].map(([commandType, { label }]) => (<ListItem key={commandType} selected={selectedCommand === commandType} onClick={() => {\n                setSelectedCommand(commandType);\n                setCurrentCommandType(undefined);\n                setInputKey(inputKey + 1);\n                setErrorMessage(undefined);\n            }}>\n                {getLabel(label)}\n              </ListItem>))}\n          </List>\n        </div>\n        <div className=\"key-opts-right\">\n          <fieldset className=\"key-opts-desc-container\">\n            <legend>{strings.get(\"GUI:Description\")}</legend>\n            <div className=\"key-opts-desc\">\n              {descriptionText}\n            </div>\n          </fieldset>\n        </div>\n      </div>\n      <div className=\"key-opts-assigns\">\n        <div className=\"key-opts-cur-assign\">\n          <div className=\"key-opts-left\" data-r-tooltip={strings.get(\"STT:KeyboardLabelAssigned\")}>\n            <div className=\"key-opts-cur-assign-label\">\n              {strings.get(\"GUI:CurrentShortcut\")}\n            </div>\n            <div className=\"key-opts-cur-assign-value\">\n              {currentHotKey && getHumanReadableKey(currentHotKey)}\n            </div>\n          </div>\n          <div className=\"key-opts-right\">\n            {errorMessage}\n          </div>\n        </div>\n        <div className=\"key-opts-ch-assign\">\n          <div className=\"key-opts-left\">\n            <div className=\"key-opts-ch-assign-label\">\n              {strings.get(\"GUI:PressShortcut\")}\n            </div>\n            <PressKeyInput key={inputKey} onChange={(keyEvent) => {\n            setCurrentCommandType(keyEvent ? keyBinds.getCommandType(keyEvent) : undefined);\n            setCurrentKeyEvent(keyEvent);\n            setErrorMessage(undefined);\n        }} tooltip={strings.get(\"STT:KeyboardEditEntry\")}/>\n            <div className=\"key-opts-ch-assign-current\">\n              <div>{strings.get(\"GUI:CurAssignedTo\")}</div>\n              <div>\n                {currentCommandType && configurableCmds.has(currentCommandType)\n            ? getLabel(configurableCmds.get(currentCommandType)!.label)\n            : \"\"}\n              </div>\n            </div>\n          </div>\n          <div className=\"key-opts-right\">\n            <button className=\"dialog-button\" disabled={!selectedCommand} onClick={() => {\n            if (selectedCommand) {\n                if (currentKeyEvent &&\n                    (currentKeyEvent.shiftKey ||\n                        currentKeyEvent.ctrlKey ||\n                        currentKeyEvent.altKey ||\n                        currentKeyEvent.metaKey)) {\n                    if (KeyboardHandler.anyModifierCommands.includes(selectedCommand)) {\n                        setErrorMessage(strings.get(\"Error:CannotMap\"));\n                        return;\n                    }\n                    const baseKeyCommand = keyBinds.getCommandType({\n                        keyCode: currentKeyEvent.keyCode,\n                        altKey: false,\n                        ctrlKey: false,\n                        shiftKey: false,\n                        metaKey: false,\n                    });\n                    if (baseKeyCommand !== undefined &&\n                        KeyboardHandler.anyModifierCommands.includes(baseKeyCommand)) {\n                        setErrorMessage(strings.get(\"Error:CannotRemap\"));\n                        return;\n                    }\n                }\n                onHotKeyChange?.(selectedCommand, currentKeyEvent);\n                setCurrentCommandType(undefined);\n                setCurrentKeyEvent(undefined);\n                setInputKey(inputKey + 1);\n                setErrorMessage(undefined);\n            }\n        }} data-r-tooltip={strings.get(\"STT:KeyboardButtonAssign\")}>\n              {strings.get(\"GUI:Assign\")}\n            </button>\n            <button className=\"dialog-button\" onClick={async () => {\n            setSelectedCommand(undefined);\n            setCurrentCommandType(undefined);\n            setCurrentKeyEvent(undefined);\n            setErrorMessage(undefined);\n            await onResetAll?.();\n            setInputKey(inputKey + 1);\n        }} data-r-tooltip={strings.get(\"STT:KeyboardButtonResetAll\")}>\n              {strings.get(\"GUI:ResetAll\")}\n            </button>\n          </div>\n        </div>\n        <fieldset className=\"key-opts-ch-assign-warn\">\n          <legend>{strings.get(\"TS:Warning\").toLocaleUpperCase()}</legend>\n          {strings.get(\"TS:HotKeyFSWarning\")}\n        </fieldset>\n      </div>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/options/component/MusicJukebox.tsx",
    "content": "import { List, ListItem } from \"@/gui/component/List\";\nimport React, { useState } from \"react\";\nimport { pad } from \"@/util/string\";\ninterface Strings {\n    get(key: string): string;\n}\ninterface PlaylistItem {\n    name: string;\n}\ninterface Music {\n    getCurrentPlaylistItem(): PlaylistItem;\n    getPlaylist(): PlaylistItem[];\n    getShuffleMode(): boolean;\n    setShuffleMode(enabled: boolean): void;\n    getRepeatMode(): boolean;\n    setRepeatMode(enabled: boolean): void;\n    selectPlaylistItem(item: PlaylistItem): void;\n    stopPlaying(): void;\n}\ninterface MusicJukeboxProps {\n    music: Music;\n    strings: Strings;\n}\nexport const MusicJukebox: React.FC<MusicJukeboxProps> = ({ music, strings }) => {\n    const [selectedItem, setSelectedItem] = useState<PlaylistItem>(() => music.getCurrentPlaylistItem());\n    return (<div className=\"music-jukebox\">\n      <div className=\"jukebox-content\">\n        <div className=\"controls\">\n          <div>\n            <label>\n              <input type=\"checkbox\" defaultChecked={music.getShuffleMode()} onChange={(e) => music.setShuffleMode(e.target.checked)}/>\n              {strings.get(\"GUI:Shuffle\")}\n            </label>\n          </div>\n          <div>\n            <label>\n              <input type=\"checkbox\" defaultChecked={music.getRepeatMode()} onChange={(e) => music.setRepeatMode(e.target.checked)}/>\n              {strings.get(\"GUI:Repeat\")}\n            </label>\n          </div>\n        </div>\n        <List className=\"playlist\">\n          {music\n            .getPlaylist()\n            .map((item, index) => (<ListItem key={item.name} selected={item === selectedItem} onClick={() => setSelectedItem(item)}>\n                {pad(index + 1, \"00\")} - {strings.get(item.name)}\n              </ListItem>))}\n        </List>\n      </div>\n      <div className=\"jukebox-footer\">\n        <button className=\"dialog-button\" onClick={() => selectedItem && music.selectPlaylistItem(selectedItem)}>\n          {strings.get(\"GUI:Play\")}\n        </button>\n        <button className=\"dialog-button\" onClick={() => music.stopPlaying()}>\n          {strings.get(\"GUI:Stop\")}\n        </button>\n      </div>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/options/component/PressKeyInput.tsx",
    "content": "import { FullScreen } from \"@/gui/FullScreen\";\nimport React, { useState } from \"react\";\nimport { getHumanReadableKey } from \"@/gui/screen/options/component/getHumanReadableKey\";\ninterface KeyEvent {\n    shiftKey: boolean;\n    ctrlKey: boolean;\n    altKey: boolean;\n    metaKey: boolean;\n    keyCode?: number;\n}\ninterface PressKeyInputProps {\n    tooltip?: string;\n    onChange: (keyEvent: KeyEvent | undefined) => void;\n}\nconst hasModifiers = (event: KeyboardEvent): boolean => event.shiftKey || event.ctrlKey || event.altKey || event.metaKey;\nconst isModifierKey = (event: KeyboardEvent): boolean => [\"Alt\", \"Control\", \"Shift\", \"Meta\"].includes(event.key);\nconst forbiddenKeys = [\n    \"Escape\",\n    \"Backspace\",\n    \"Enter\",\n    \"Tab\",\n    \"ArrowLeft\",\n    \"ArrowRight\",\n    \"ArrowUp\",\n    \"ArrowDown\",\n    \" \",\n];\nexport const PressKeyInput: React.FC<PressKeyInputProps> = ({ tooltip, onChange }) => {\n    const [currentKeyEvent, setCurrentKeyEvent] = useState<KeyEvent | undefined>();\n    const updateKeyEvent = (keyEvent: KeyEvent | undefined) => {\n        setCurrentKeyEvent(keyEvent);\n        if (!keyEvent || keyEvent.keyCode === undefined) {\n            onChange(keyEvent);\n        }\n        else {\n            onChange(keyEvent);\n        }\n    };\n    const displayValue = currentKeyEvent ? getHumanReadableKey(currentKeyEvent) : \"\";\n    return (<input type=\"text\" value={displayValue} data-r-tooltip={tooltip} onChange={() => { }} onKeyDown={(e) => {\n            e.preventDefault();\n            e.stopPropagation();\n            if (e.repeat || e.keyCode > 255) {\n                return;\n            }\n            if (forbiddenKeys.includes(e.key) || FullScreen.isFullScreenHotKey(e as any)) {\n                updateKeyEvent(undefined);\n            }\n            else {\n                updateKeyEvent({\n                    shiftKey: e.shiftKey,\n                    ctrlKey: e.ctrlKey,\n                    altKey: e.altKey,\n                    metaKey: e.metaKey,\n                    keyCode: isModifierKey(e as any) ? undefined : e.keyCode,\n                });\n            }\n        }} onKeyUp={(e) => {\n            e.preventDefault();\n            e.stopPropagation();\n            if (e.repeat || e.keyCode > 255) {\n                return;\n            }\n            if (currentKeyEvent?.keyCode === undefined &&\n                isModifierKey(e as any)) {\n                updateKeyEvent(hasModifiers(e as any)\n                    ? {\n                        shiftKey: e.shiftKey,\n                        ctrlKey: e.ctrlKey,\n                        altKey: e.altKey,\n                        metaKey: e.metaKey,\n                        keyCode: undefined,\n                    }\n                    : undefined);\n            }\n        }} onBlur={() => {\n            if (currentKeyEvent && currentKeyEvent.keyCode === undefined) {\n                updateKeyEvent(undefined);\n            }\n        }}/>);\n};\n"
  },
  {
    "path": "src/gui/screen/options/component/Resolution.tsx",
    "content": "import { Select } from \"@/gui/component/Select\";\nimport { Option } from \"@/gui/component/Option\";\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\ninterface Resolution {\n    width: number;\n    height: number;\n}\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface FullScreen {\n    isFullScreen(): boolean;\n    onChange?: {\n        subscribe: (listener: (value: boolean) => void) => void;\n        unsubscribe: (listener: (value: boolean) => void) => void;\n    };\n}\ninterface ResolutionSelectProps {\n    resolution: BoxedVar<Resolution | undefined>;\n    fullScreen: FullScreen;\n    strings: Strings;\n}\nconst desktopResolutions: Resolution[] = [\n    { width: 1920, height: 1080 },\n    { width: 1600, height: 900 },\n    { width: 1366, height: 768 },\n    { width: 1280, height: 1024 },\n    { width: 1280, height: 720 },\n    { width: 1024, height: 768 },\n    { width: 800, height: 600 },\n];\nconst mobileResolutions: Resolution[] = [\n    { width: 1280, height: 720 },\n    { width: 1024, height: 768 },\n    { width: 960, height: 720 },\n    { width: 800, height: 600 },\n];\nconst isCoarsePointer = () => !!window.matchMedia?.(\"(pointer: coarse)\")?.matches;\nconst getCurrentScreenSize = (): Resolution => ({\n    width: Math.max(320, Math.floor(window.visualViewport?.width ?? window.innerWidth)),\n    height: Math.max(240, Math.floor(window.visualViewport?.height ?? window.innerHeight)),\n});\nconst formatResolution = (resolution: Resolution): string => `${resolution.width} x ${resolution.height}`;\nconst isSameResolution = (left?: Resolution, right?: Resolution) => !!left &&\n    !!right &&\n    left.width === right.width &&\n    left.height === right.height;\nexport const ResolutionSelect: React.FC<ResolutionSelectProps> = ({ resolution, fullScreen, strings, }) => {\n    const [screenSize, setScreenSize] = useState<Resolution>(() => getCurrentScreenSize());\n    const [currentResolution, setCurrentResolution] = useState<Resolution | undefined>(resolution.value);\n    const [fullScreenMode, setFullScreenMode] = useState(() => fullScreen.isFullScreen());\n    const [mobileLayout, setMobileLayout] = useState(() => isCoarsePointer());\n    useEffect(() => {\n        const handleResize = () => {\n            setScreenSize(getCurrentScreenSize());\n            setMobileLayout(isCoarsePointer());\n            setFullScreenMode(fullScreen.isFullScreen());\n        };\n        const handleFullScreenChange = (value: boolean) => {\n            setFullScreenMode(value);\n            handleResize();\n        };\n        window.addEventListener(\"resize\", handleResize);\n        window.visualViewport?.addEventListener(\"resize\", handleResize);\n        resolution.onChange.subscribe(setCurrentResolution);\n        fullScreen.onChange?.subscribe(handleFullScreenChange);\n        return () => {\n            window.removeEventListener(\"resize\", handleResize);\n            window.visualViewport?.removeEventListener(\"resize\", handleResize);\n            resolution.onChange.unsubscribe(setCurrentResolution);\n            fullScreen.onChange?.unsubscribe(handleFullScreenChange);\n        };\n    }, [fullScreen, resolution]);\n    const availableResolutions = useMemo(() => {\n        const baseList = mobileLayout ? mobileResolutions : desktopResolutions;\n        const filtered = baseList.filter((entry, index) => (entry.width <= screenSize.width && entry.height <= screenSize.height) ||\n            index === baseList.length - 1);\n        if (currentResolution && !filtered.some((entry) => isSameResolution(entry, currentResolution))) {\n            return [currentResolution, ...filtered];\n        }\n        return filtered;\n    }, [currentResolution, mobileLayout, screenSize.height, screenSize.width]);\n    if (fullScreenMode) {\n        return (<Select className=\"resolution-select\" initialValue=\"\" disabled={true} onSelect={() => { }}>\n        <Option value=\"\" label={strings.get(\"TS:ResolutionFullScreen\", formatResolution(screenSize))}/>\n      </Select>);\n    }\n    return (<Select className=\"resolution-select\" initialValue={currentResolution ? formatResolution(currentResolution) : \"\"} onSelect={(value) => {\n            const nextResolution = value.length\n                ? value.split(\" x \").map((item) => Number(item))\n                : undefined;\n            resolution.value = nextResolution\n                ? { width: nextResolution[0], height: nextResolution[1] }\n                : undefined;\n        }}>\n      <Option value=\"\" label={strings.get(\"TS:ResolutionFit\", formatResolution(screenSize))}/>\n      {availableResolutions.map((entry) => {\n            const value = formatResolution(entry);\n            return <Option key={value} value={value} label={value}/>;\n        })}\n    </Select>);\n};\n"
  },
  {
    "path": "src/gui/screen/options/component/SoundOpts.tsx",
    "content": "import React from \"react\";\nimport { Slider } from \"@/gui/component/Slider\";\nimport { ChannelType } from \"@/engine/sound/ChannelType\";\nimport { MusicJukebox } from \"@/gui/screen/options/component/MusicJukebox\";\ninterface Strings {\n    get(key: string): string;\n}\ninterface Music {\n    getCurrentPlaylistItem(): any;\n    getPlaylist(): any[];\n    getShuffleMode(): boolean;\n    setShuffleMode(enabled: boolean): void;\n    getRepeatMode(): boolean;\n    setRepeatMode(enabled: boolean): void;\n    selectPlaylistItem(item: any): void;\n    stopPlaying(): void;\n}\ninterface Mixer {\n    getVolume(channelType: ChannelType): number;\n    setVolume(channelType: ChannelType, volume: number): void;\n}\ninterface SoundOptsProps {\n    strings: Strings;\n    music?: Music;\n    mixer: Mixer;\n}\nconst channelLabels = new Map<ChannelType, string>([\n    [ChannelType.Master, \"GUI:MasterVolume\"],\n    [ChannelType.Music, \"GUI:MusicVolume\"],\n    [ChannelType.Effect, \"GUI:SFXVolume\"],\n    [ChannelType.Voice, \"GUI:VoiceVolume\"],\n    [ChannelType.Ambient, \"GUI:AmbientVolume\"],\n    [ChannelType.Ui, \"GUI:UIVolume\"],\n    [ChannelType.CreditTicks, \"GUI:CreditsVolume\"],\n]);\nexport const SoundOpts: React.FC<SoundOptsProps> = ({ strings, music, mixer }) => (<div className=\"opts sound-opts\">\n    <div className=\"sound-sliders\">\n      {[...channelLabels].map(([channelType, labelKey]) => (<div className=\"slider-item\" key={channelType}>\n          <span className=\"label\">{strings.get(labelKey)}</span>\n          <Slider min={0} max={10} value={String(10 * mixer.getVolume(channelType))} onChange={(e) => mixer.setVolume(channelType, Number(e.target.value) / 10)}/>\n        </div>))}\n    </div>\n    {music && <MusicJukebox music={music} strings={strings}/>}\n  </div>);\n"
  },
  {
    "path": "src/gui/screen/options/component/StorageExplorer.tsx",
    "content": "import React from 'react';\nimport { Strings } from '../../../../data/Strings';\nimport { Engine } from '../../../../engine/Engine';\nimport { MessageBoxApi } from '../../../component/MessageBoxApi';\nimport { StorageFileExplorer } from '../../../component/fileExplorer/StorageFileExplorer';\n\ninterface StorageExplorerProps {\n    strings: Strings;\n    messageBoxApi: MessageBoxApi;\n    storageDirHandle: FileSystemDirectoryHandle;\n    startIn?: string;\n    onFileSystemChange?: () => void;\n}\n\nconst StorageExplorer: React.FC<StorageExplorerProps> = ({ strings, messageBoxApi, storageDirHandle, startIn, onFileSystemChange }) => {\n    const modIdRegex = /^[a-z0-9-_]+$/i;\n    const isSystemFile = (path: string): boolean => {\n        const systemPatterns: (string | RegExp)[] = [\n            /^\\/[^\\/]*\\.mix$/i,\n            /^\\/[^\\/]*\\.bag$/i,\n            /^\\/[^\\/]*\\.idx$/i,\n            /^\\/[^\\/]*\\.ini$/i,\n            /^\\/[^\\/]*\\.csf$/i,\n        ];\n        return systemPatterns.some(pattern => typeof pattern === 'string'\n            ? path.toLowerCase() === pattern.toLowerCase()\n            : pattern.test(path));\n    };\n\n    const isUploadAllowed = (path: string): boolean => {\n        const allowedPatterns: (string | RegExp)[] = [\n            '/keyboard.ini',\n            /^\\/(language|multi|ra2)\\.mix$/i,\n            /^\\/music\\/[^\\/]+\\.mp3$/i,\n            /^\\/replays\\/.*$/i,\n            /^\\/taunts\\/tau[^\\/]+\\.wav$/i,\n            /^\\/mods\\/[^\\/]+\\/.*$/i,\n            /^\\/maps\\/[^\\/]+\\.(map|mpr|yrm)$/i,\n        ];\n        return allowedPatterns.some(pattern => typeof pattern === 'string'\n            ? path.toLowerCase() === pattern.toLowerCase()\n            : pattern.test(path));\n    };\n\n    const shouldLowerCaseFile = (path: string): boolean => {\n        const lowerCasePatterns: (string | RegExp)[] = [\n            '/keyboard.ini',\n            /^\\/[^\\/]+\\.mix$/i,\n            /^\\/music\\/.*$/i,\n            /^\\/taunts\\/.*$/i,\n        ];\n        return lowerCasePatterns.some(pattern => typeof pattern === 'string'\n            ? path.toLowerCase() === pattern.toLowerCase()\n            : pattern.test(path));\n    };\n\n    return (\n        <div className=\"storage-explorer\" style={{ height: '100%' }}>\n            <StorageFileExplorer\n                rootHandle={storageDirHandle}\n                rootLabel=\"/\"\n                startIn={startIn}\n                isSystemFile={isSystemFile}\n                isUploadAllowed={isUploadAllowed}\n                shouldLowerCaseFile={shouldLowerCaseFile}\n                onFileSystemChange={onFileSystemChange}\n                canCreateFolder={(path, segments) => {\n                    const lastSegment = segments[segments.length - 1];\n                    return path === `/${Engine.rfsSettings.modDir}` || lastSegment === Engine.rfsSettings.modDir;\n                }}\n                validateNewFolderName={(name) => {\n                    if (!modIdRegex.test(name)) {\n                        return '文件夹名称只允许字母、数字、- 和 _。';\n                    }\n                    return undefined;\n                }}\n                promptForText={async (promptText) => messageBoxApi.prompt(\n                    promptText,\n                    strings.get('GUI:Create') || 'Create',\n                    strings.get('GUI:Cancel') || 'Cancel',\n                )}\n                confirmAction={async (message, confirmLabel, cancelLabel) => messageBoxApi.confirm(\n                    message,\n                    confirmLabel || strings.get('GUI:Ok') || 'OK',\n                    cancelLabel || strings.get('GUI:Cancel') || 'Cancel',\n                )}\n                showAlert={async (message, title) => {\n                    messageBoxApi.show(\n                        title ? `${title}\\n\\n${message}` : message,\n                        strings.get('GUI:Ok') || 'OK',\n                    );\n                }}\n                loadingLabel={strings.get('GUI:LoadingFileExplorer') || 'Loading file explorer...'}\n            />\n        </div>\n    );\n};\n\nexport default StorageExplorer;\n"
  },
  {
    "path": "src/gui/screen/options/component/configurableCmds.ts",
    "content": "import { KeyCommandType } from \"@/gui/screen/game/worldInteraction/keyboard/KeyCommandType\";\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface CommandConfig {\n    label: string | ((strings: Strings) => string);\n    desc: string | ((strings: Strings) => string);\n}\nexport const configurableCmds = new Map<KeyCommandType, CommandConfig>([\n    [\n        KeyCommandType.Options,\n        { label: \"txt_options\", desc: \"txt_options_desc\" },\n    ],\n    [\n        KeyCommandType.ToggleAlliance,\n        { label: \"txt_alliance\", desc: \"txt_alliance_desc\" },\n    ],\n    [\n        KeyCommandType.PlaceBeacon,\n        { label: \"txt_place_beacon\", desc: \"txt_place_beacon_desc\" },\n    ],\n    [\n        KeyCommandType.AllToCheer,\n        { label: \"cmnd:allcheer\", desc: \"cmnd:allcheerdesc\" },\n    ],\n    [\n        KeyCommandType.DeployObject,\n        { label: \"txt_deploy_object\", desc: \"txt_deploy_object_desc\" },\n    ],\n    [\n        KeyCommandType.Follow,\n        { label: \"txt_follow\", desc: \"txt_follow_desc\" },\n    ],\n    [\n        KeyCommandType.GuardObject,\n        { label: \"txt_guard\", desc: \"txt_guard_desc\" },\n    ],\n    [\n        KeyCommandType.StopObject,\n        { label: \"txt_stop_object\", desc: \"txt_stop_object_desc\" },\n    ],\n    [\n        KeyCommandType.ScatterObject,\n        { label: \"txt_scatter\", desc: \"txt_scatter_desc\" },\n    ],\n    [\n        KeyCommandType.CenterBase,\n        { label: \"txt_center_base\", desc: \"txt_center_base_desc\" },\n    ],\n    [\n        KeyCommandType.ToggleRepair,\n        { label: \"txt_repair_mode\", desc: \"txt_repair_mode_desc\" },\n    ],\n    [\n        KeyCommandType.ToggleSell,\n        { label: \"txt_sell_mode\", desc: \"txt_sell_mode_desc\" },\n    ],\n    [\n        KeyCommandType.PreviousObject,\n        { label: \"txt_prev_object\", desc: \"txt_prev_object_desc\" },\n    ],\n    [\n        KeyCommandType.NextObject,\n        { label: \"txt_next_object\", desc: \"txt_next_object_desc\" },\n    ],\n    [\n        KeyCommandType.TypeSelect,\n        { label: \"cmnd:typeselect\", desc: \"cmnd:typeselectdesc\" },\n    ],\n    [\n        KeyCommandType.CombatantSelect,\n        {\n            label: \"cmnd:combatantselect\",\n            desc: \"cmnd:combatantselectdesc\",\n        },\n    ],\n    [\n        KeyCommandType.StructureTab,\n        { label: \"txt_structure_tab\", desc: \"txt_structure_tab_desc\" },\n    ],\n    [\n        KeyCommandType.DefenseTab,\n        { label: \"txt_defense_tab\", desc: \"txt_defense_tab_desc\" },\n    ],\n    [\n        KeyCommandType.InfantryTab,\n        { label: \"txt_infantry_tab\", desc: \"txt_infantry_tab_desc\" },\n    ],\n    [\n        KeyCommandType.UnitTab,\n        { label: \"txt_unit_tab\", desc: \"txt_unit_tab_desc\" },\n    ],\n    [\n        KeyCommandType.PlanningMode,\n        { label: \"cmnd:planningmode\", desc: \"cmnd:planningmodedesc\" },\n    ],\n    [\n        KeyCommandType.CenterOnRadarEvent,\n        { label: \"txt_radar_event\", desc: \"txt_radar_event_desc\" },\n    ],\n    [\n        KeyCommandType.HealthNav,\n        {\n            label: \"cmnd:healthnavigation\",\n            desc: \"cmnd:healthnavigationdesc\",\n        },\n    ],\n    [\n        KeyCommandType.VeterancyNav,\n        { label: \"cmnd:vetnavigation\", desc: \"cmnd:vetnavigationdesc\" },\n    ],\n    [\n        KeyCommandType.TeamSelect_1,\n        {\n            label: (e) => e.get(\"txt_select_team\", 1),\n            desc: (e) => e.get(\"txt_select_team_desc\", 1),\n        },\n    ],\n    [\n        KeyCommandType.TeamSelect_2,\n        {\n            label: (e) => e.get(\"txt_select_team\", 2),\n            desc: (e) => e.get(\"txt_select_team_desc\", 2),\n        },\n    ],\n    [\n        KeyCommandType.TeamSelect_3,\n        {\n            label: (e) => e.get(\"txt_select_team\", 3),\n            desc: (e) => e.get(\"txt_select_team_desc\", 3),\n        },\n    ],\n    [\n        KeyCommandType.TeamSelect_4,\n        {\n            label: (e) => e.get(\"txt_select_team\", 4),\n            desc: (e) => e.get(\"txt_select_team_desc\", 4),\n        },\n    ],\n    [\n        KeyCommandType.TeamSelect_5,\n        {\n            label: (e) => e.get(\"txt_select_team\", 5),\n            desc: (e) => e.get(\"txt_select_team_desc\", 5),\n        },\n    ],\n    [\n        KeyCommandType.TeamSelect_6,\n        {\n            label: (e) => e.get(\"txt_select_team\", 6),\n            desc: (e) => e.get(\"txt_select_team_desc\", 6),\n        },\n    ],\n    [\n        KeyCommandType.TeamSelect_7,\n        {\n            label: (e) => e.get(\"txt_select_team\", 7),\n            desc: (e) => e.get(\"txt_select_team_desc\", 7),\n        },\n    ],\n    [\n        KeyCommandType.TeamSelect_8,\n        {\n            label: (e) => e.get(\"txt_select_team\", 8),\n            desc: (e) => e.get(\"txt_select_team_desc\", 8),\n        },\n    ],\n    [\n        KeyCommandType.TeamSelect_9,\n        {\n            label: (e) => e.get(\"txt_select_team\", 9),\n            desc: (e) => e.get(\"txt_select_team_desc\", 9),\n        },\n    ],\n    [\n        KeyCommandType.TeamSelect_10,\n        {\n            label: (e) => e.get(\"txt_select_team\", 10),\n            desc: (e) => e.get(\"txt_select_team_desc\", 10),\n        },\n    ],\n    [\n        KeyCommandType.TeamCreate_1,\n        {\n            label: (e) => e.get(\"txt_create_team\", 1),\n            desc: (e) => e.get(\"txt_create_team_desc\", 1),\n        },\n    ],\n    [\n        KeyCommandType.TeamCreate_2,\n        {\n            label: (e) => e.get(\"txt_create_team\", 2),\n            desc: (e) => e.get(\"txt_create_team_desc\", 2),\n        },\n    ],\n    [\n        KeyCommandType.TeamCreate_3,\n        {\n            label: (e) => e.get(\"txt_create_team\", 3),\n            desc: (e) => e.get(\"txt_create_team_desc\", 3),\n        },\n    ],\n    [\n        KeyCommandType.TeamCreate_4,\n        {\n            label: (e) => e.get(\"txt_create_team\", 4),\n            desc: (e) => e.get(\"txt_create_team_desc\", 4),\n        },\n    ],\n    [\n        KeyCommandType.TeamCreate_5,\n        {\n            label: (e) => e.get(\"txt_create_team\", 5),\n            desc: (e) => e.get(\"txt_create_team_desc\", 5),\n        },\n    ],\n    [\n        KeyCommandType.TeamCreate_6,\n        {\n            label: (e) => e.get(\"txt_create_team\", 6),\n            desc: (e) => e.get(\"txt_create_team_desc\", 6),\n        },\n    ],\n    [\n        KeyCommandType.TeamCreate_7,\n        {\n            label: (e) => e.get(\"txt_create_team\", 7),\n            desc: (e) => e.get(\"txt_create_team_desc\", 7),\n        },\n    ],\n    [\n        KeyCommandType.TeamCreate_8,\n        {\n            label: (e) => e.get(\"txt_create_team\", 8),\n            desc: (e) => e.get(\"txt_create_team_desc\", 8),\n        },\n    ],\n    [\n        KeyCommandType.TeamCreate_9,\n        {\n            label: (e) => e.get(\"txt_create_team\", 9),\n            desc: (e) => e.get(\"txt_create_team_desc\", 9),\n        },\n    ],\n    [\n        KeyCommandType.TeamCreate_10,\n        {\n            label: (e) => e.get(\"txt_create_team\", 10),\n            desc: (e) => e.get(\"txt_create_team_desc\", 10),\n        },\n    ],\n    [\n        KeyCommandType.TeamAddSelect_1,\n        {\n            label: (e) => e.get(\"txt_add_select_team\", 1),\n            desc: (e) => e.get(\"txt_add_select_team_desc\", 1),\n        },\n    ],\n    [\n        KeyCommandType.TeamAddSelect_2,\n        {\n            label: (e) => e.get(\"txt_add_select_team\", 2),\n            desc: (e) => e.get(\"txt_add_select_team_desc\", 2),\n        },\n    ],\n    [\n        KeyCommandType.TeamAddSelect_3,\n        {\n            label: (e) => e.get(\"txt_add_select_team\", 3),\n            desc: (e) => e.get(\"txt_add_select_team_desc\", 3),\n        },\n    ],\n    [\n        KeyCommandType.TeamAddSelect_4,\n        {\n            label: (e) => e.get(\"txt_add_select_team\", 4),\n            desc: (e) => e.get(\"txt_add_select_team_desc\", 4),\n        },\n    ],\n    [\n        KeyCommandType.TeamAddSelect_5,\n        {\n            label: (e) => e.get(\"txt_add_select_team\", 5),\n            desc: (e) => e.get(\"txt_add_select_team_desc\", 5),\n        },\n    ],\n    [\n        KeyCommandType.TeamAddSelect_6,\n        {\n            label: (e) => e.get(\"txt_add_select_team\", 6),\n            desc: (e) => e.get(\"txt_add_select_team_desc\", 6),\n        },\n    ],\n    [\n        KeyCommandType.TeamAddSelect_7,\n        {\n            label: (e) => e.get(\"txt_add_select_team\", 7),\n            desc: (e) => e.get(\"txt_add_select_team_desc\", 7),\n        },\n    ],\n    [\n        KeyCommandType.TeamAddSelect_8,\n        {\n            label: (e) => e.get(\"txt_add_select_team\", 8),\n            desc: (e) => e.get(\"txt_add_select_team_desc\", 8),\n        },\n    ],\n    [\n        KeyCommandType.TeamAddSelect_9,\n        {\n            label: (e) => e.get(\"txt_add_select_team\", 9),\n            desc: (e) => e.get(\"txt_add_select_team_desc\", 9),\n        },\n    ],\n    [\n        KeyCommandType.TeamAddSelect_10,\n        {\n            label: (e) => e.get(\"txt_add_select_team\", 10),\n            desc: (e) => e.get(\"txt_add_select_team_desc\", 10),\n        },\n    ],\n    [\n        KeyCommandType.TeamCenter_1,\n        {\n            label: (e) => e.get(\"txt_center_team\", 1),\n            desc: (e) => e.get(\"txt_center_team_desc\", 1),\n        },\n    ],\n    [\n        KeyCommandType.TeamCenter_2,\n        {\n            label: (e) => e.get(\"txt_center_team\", 2),\n            desc: (e) => e.get(\"txt_center_team_desc\", 2),\n        },\n    ],\n    [\n        KeyCommandType.TeamCenter_3,\n        {\n            label: (e) => e.get(\"txt_center_team\", 3),\n            desc: (e) => e.get(\"txt_center_team_desc\", 3),\n        },\n    ],\n    [\n        KeyCommandType.TeamCenter_4,\n        {\n            label: (e) => e.get(\"txt_center_team\", 4),\n            desc: (e) => e.get(\"txt_center_team_desc\", 4),\n        },\n    ],\n    [\n        KeyCommandType.TeamCenter_5,\n        {\n            label: (e) => e.get(\"txt_center_team\", 5),\n            desc: (e) => e.get(\"txt_center_team_desc\", 5),\n        },\n    ],\n    [\n        KeyCommandType.TeamCenter_6,\n        {\n            label: (e) => e.get(\"txt_center_team\", 6),\n            desc: (e) => e.get(\"txt_center_team_desc\", 6),\n        },\n    ],\n    [\n        KeyCommandType.TeamCenter_7,\n        {\n            label: (e) => e.get(\"txt_center_team\", 7),\n            desc: (e) => e.get(\"txt_center_team_desc\", 7),\n        },\n    ],\n    [\n        KeyCommandType.TeamCenter_8,\n        {\n            label: (e) => e.get(\"txt_center_team\", 8),\n            desc: (e) => e.get(\"txt_center_team_desc\", 8),\n        },\n    ],\n    [\n        KeyCommandType.TeamCenter_9,\n        {\n            label: (e) => e.get(\"txt_center_team\", 9),\n            desc: (e) => e.get(\"txt_center_team_desc\", 9),\n        },\n    ],\n    [\n        KeyCommandType.TeamCenter_10,\n        {\n            label: (e) => e.get(\"txt_center_team\", 10),\n            desc: (e) => e.get(\"txt_center_team_desc\", 10),\n        },\n    ],\n    [\n        KeyCommandType.CenterView,\n        { label: \"txt_center_view\", desc: \"txt_center_view_desc\" },\n    ],\n    [\n        KeyCommandType.View1,\n        {\n            label: \"txt_view_bookmark1\",\n            desc: \"txt_view_bookmark1_desc\",\n        },\n    ],\n    [\n        KeyCommandType.View2,\n        {\n            label: \"txt_view_bookmark2\",\n            desc: \"txt_view_bookmark2_desc\",\n        },\n    ],\n    [\n        KeyCommandType.View3,\n        {\n            label: \"txt_view_bookmark3\",\n            desc: \"txt_view_bookmark3_desc\",\n        },\n    ],\n    [\n        KeyCommandType.View4,\n        {\n            label: \"txt_view_bookmark4\",\n            desc: \"txt_view_bookmark4_desc\",\n        },\n    ],\n    [\n        KeyCommandType.SetView1,\n        { label: \"txt_set_bookmark1\", desc: \"txt_set_bookmark1_desc\" },\n    ],\n    [\n        KeyCommandType.SetView2,\n        { label: \"txt_set_bookmark2\", desc: \"txt_set_bookmark2_desc\" },\n    ],\n    [\n        KeyCommandType.SetView3,\n        { label: \"txt_set_bookmark3\", desc: \"txt_set_bookmark3_desc\" },\n    ],\n    [\n        KeyCommandType.SetView4,\n        { label: \"txt_set_bookmark4\", desc: \"txt_set_bookmark4_desc\" },\n    ],\n    [\n        KeyCommandType.Taunt_1,\n        {\n            label: (e) => e.get(\"txt_taunt_number\", 1),\n            desc: (e) => e.get(\"txt_taunt_desc\", 1),\n        },\n    ],\n    [\n        KeyCommandType.Taunt_2,\n        {\n            label: (e) => e.get(\"txt_taunt_number\", 2),\n            desc: (e) => e.get(\"txt_taunt_desc\", 2),\n        },\n    ],\n    [\n        KeyCommandType.Taunt_3,\n        {\n            label: (e) => e.get(\"txt_taunt_number\", 3),\n            desc: (e) => e.get(\"txt_taunt_desc\", 3),\n        },\n    ],\n    [\n        KeyCommandType.Taunt_4,\n        {\n            label: (e) => e.get(\"txt_taunt_number\", 4),\n            desc: (e) => e.get(\"txt_taunt_desc\", 4),\n        },\n    ],\n    [\n        KeyCommandType.Taunt_5,\n        {\n            label: (e) => e.get(\"txt_taunt_number\", 5),\n            desc: (e) => e.get(\"txt_taunt_desc\", 5),\n        },\n    ],\n    [\n        KeyCommandType.Taunt_6,\n        {\n            label: (e) => e.get(\"txt_taunt_number\", 6),\n            desc: (e) => e.get(\"txt_taunt_desc\", 6),\n        },\n    ],\n    [\n        KeyCommandType.Taunt_7,\n        {\n            label: (e) => e.get(\"txt_taunt_number\", 7),\n            desc: (e) => e.get(\"txt_taunt_desc\", 7),\n        },\n    ],\n    [\n        KeyCommandType.Taunt_8,\n        {\n            label: (e) => e.get(\"txt_taunt_number\", 8),\n            desc: (e) => e.get(\"txt_taunt_desc\", 8),\n        },\n    ],\n    [\n        KeyCommandType.Delete,\n        { label: \"cmnd:delete\", desc: \"cmnd:deletedesc\" },\n    ],\n    [\n        KeyCommandType.PageUser,\n        { label: \"txt_pageuser\", desc: \"txt_pageuser_desc\" },\n    ],\n    [\n        KeyCommandType.SidebarPageUp,\n        { label: \"txt_sidebar_pgup\", desc: \"txt_sidebar_pgup_desc\" },\n    ],\n    [\n        KeyCommandType.SidebarUp,\n        { label: \"txt_sidebar_up\", desc: \"txt_sidebar_up_desc\" },\n    ],\n    [\n        KeyCommandType.SidebarPageDown,\n        { label: \"txt_sidebar_pgdn\", desc: \"txt_sidebar_pgdn_desc\" },\n    ],\n    [\n        KeyCommandType.SidebarDown,\n        { label: \"txt_sidebar_down\", desc: \"txt_sidebar_down_desc\" },\n    ],\n    [\n        KeyCommandType.ToggleFps,\n        { label: \"cmnd:togglefps\", desc: \"cmnd:togglefpsdesc\" },\n    ],\n]);\n"
  },
  {
    "path": "src/gui/screen/options/component/getHumanReadableKey.ts",
    "content": "import { getKeyName } from \"@/util/keyNames\";\nimport { isMac, isIpad } from \"@/util/userAgent\";\ninterface KeyEvent {\n    ctrlKey: boolean;\n    altKey: boolean;\n    shiftKey: boolean;\n    metaKey: boolean;\n    keyCode?: number;\n}\nexport function getHumanReadableKey(event: KeyEvent): string {\n    const isMacOrIpad = isMac() || isIpad();\n    const parts: string[] = [];\n    if (event.ctrlKey) {\n        parts.push(\"Ctrl\");\n    }\n    if (event.altKey) {\n        parts.push(isMacOrIpad ? \"⌥\" : \"Alt\");\n    }\n    if (event.shiftKey) {\n        parts.push(\"Shift\");\n    }\n    if (event.metaKey) {\n        parts.push(isMacOrIpad ? \"⌘\" : \"Win\");\n    }\n    if (event.keyCode !== undefined) {\n        parts.push(getKeyName(event.keyCode));\n        return parts.join(\"+\");\n    }\n    else {\n        return parts.join(\"+\") + \"+\";\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/replay/KeepReplayBox.tsx",
    "content": "import React, { useRef, useState, useEffect } from 'react';\nimport { Dialog } from '@/gui/component/Dialog';\nimport { Replay } from '@/network/gamestate/Replay';\ninterface KeepReplayBoxProps {\n    defaultName: string;\n    strings: {\n        get(key: string, ...args: any[]): string;\n    };\n    viewport: any;\n    onSubmit: (name: string) => void;\n    onDismiss?: () => void;\n}\nexport const KeepReplayBox: React.FC<KeepReplayBoxProps> = ({ defaultName, strings, viewport, onSubmit, onDismiss }) => {\n    const inputRef = useRef<HTMLInputElement>(null);\n    const [hidden, setHidden] = useState(false);\n    useEffect(() => {\n        setTimeout(() => {\n            inputRef.current?.focus();\n            inputRef.current?.setSelectionRange(0, inputRef.current.value.length);\n        }, 50);\n    }, []);\n    const handleSubmit = (e?: React.FormEvent) => {\n        if (e) {\n            e.preventDefault();\n        }\n        const value = inputRef.current?.value;\n        if (value) {\n            setHidden(true);\n            onSubmit(value);\n        }\n    };\n    return (<Dialog className=\"keep-replay-box\" hidden={hidden} viewport={viewport} zIndex={100} buttons={[\n            { label: strings.get(\"GUI:Ok\"), onClick: handleSubmit },\n            {\n                label: strings.get(\"GUI:Cancel\"),\n                onClick: () => {\n                    setHidden(true);\n                    onDismiss?.();\n                }\n            }\n        ]}>\n      <form onSubmit={handleSubmit}>\n        <div className=\"field\">\n          <label>{strings.get(\"GUI:ReplayNamePrompt\")}</label>\n          <input type=\"text\" name=\"replayname\" autoComplete=\"off\" ref={inputRef} defaultValue={defaultName} maxLength={(Replay as any).maxNameLength}/>\n        </div>\n        <button type=\"submit\" style={{ visibility: \"hidden\" }}/>\n      </form>\n    </Dialog>);\n};\n"
  },
  {
    "path": "src/gui/screen/replay/ReplayDetailsPane.tsx",
    "content": "import React from 'react';\nimport { formatTimeDuration } from '@/util/format';\ninterface Player {\n    name: string;\n    color: string;\n}\ninterface ReplayDetails {\n    engineVersion: string;\n    durationSeconds?: number;\n    gameId: string;\n    gameTimestamp?: number;\n    mapName?: string;\n    players?: Player[];\n}\ninterface ReplayDetailsPaneProps {\n    replayDetails: ReplayDetails;\n    strings: {\n        get(key: string, ...args: any[]): string;\n    };\n}\nexport const ReplayDetailsPane: React.FC<ReplayDetailsPaneProps> = ({ replayDetails: { engineVersion, durationSeconds, gameId, gameTimestamp, mapName, players }, strings }) => (<div className=\"replay-details\">\n    <table>\n      <tbody>\n        {gameTimestamp ? (<tr>\n            <td>{strings.get(\"GUI:ReplayTime\")}:</td>\n            <td dir=\"auto\">\n              {new Date(gameTimestamp * (String(gameTimestamp).length < 13 ? 1000 : 1)).toLocaleString()}\n            </td>\n          </tr>) : null}\n        <tr>\n          <td>{strings.get(\"GUI:GameVersion\")}:</td>\n          <td>{engineVersion}</td>\n        </tr>\n        {gameId !== \"0\" ? (<tr>\n            <td>{strings.get(\"GUI:GameID\")}:</td>\n            <td>{gameId}</td>\n          </tr>) : null}\n        {mapName !== undefined && (<tr>\n            <td>{strings.get(\"GUI:Map\")}:</td>\n            <td>{mapName}</td>\n          </tr>)}\n        {players && (<tr>\n            <td>{strings.get(\"GUI:Players\")}:</td>\n            <td>\n              {players.map((player, index) => (<React.Fragment key={player.name}>\n                  {index ? \", \" : \"\"}\n                  <span style={{ color: player.color }}>\n                    {player.name}\n                  </span>\n                </React.Fragment>))}\n            </td>\n          </tr>)}\n        {durationSeconds !== undefined && (<tr>\n            <td>{strings.get(\"GUI:Duration\")}:</td>\n            <td>{formatTimeDuration(durationSeconds)}</td>\n          </tr>)}\n      </tbody>\n    </table>\n  </div>);\n"
  },
  {
    "path": "src/gui/screen/replay/ReplayScreen.ts",
    "content": "import { Engine } from '@/engine/Engine';\nimport { SidebarModel } from '@/gui/screen/game/component/hud/viewmodel/SidebarModel';\nimport { DevToolsApi } from '@/tools/DevToolsApi';\nimport { GameAnimationLoop } from '@/engine/GameAnimationLoop';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { SoundHandler } from '@/gui/screen/game/SoundHandler';\nimport { WorldInteractionFactory } from '@/gui/screen/game/worldInteraction/WorldInteractionFactory';\nimport { ObserverUi } from '@/gui/screen/game/ObserverUi';\nimport { GameMenu } from '@/gui/screen/game/GameMenu';\nimport { WorldView } from '@/gui/screen/game/WorldView';\nimport { Eva } from '@/engine/sound/Eva';\nimport { EvaSpecs } from '@/engine/sound/EvaSpecs';\nimport { HudFactory } from '@/gui/screen/game/HudFactory';\nimport { Minimap } from '@/gui/screen/game/component/Minimap';\nimport { SideType } from '@/game/SideType';\nimport { ReplayTurnManager } from '@/network/gamestate/ReplayTurnManager';\nimport { ActionFactory } from '@/game/action/ActionFactory';\nimport { ActionFactoryReg } from '@/game/action/ActionFactoryReg';\nimport { MessageList } from '@/gui/screen/game/component/hud/viewmodel/MessageList';\nimport { Music, MusicType } from '@/engine/sound/Music';\nimport { ChatMessageReplayEvent } from '@/network/gamestate/replay/ChatMessageReplayEvent';\nimport { SoundKey } from '@/engine/sound/SoundKey';\nimport { ChannelType } from '@/engine/sound/ChannelType';\nimport { TauntReplayEvent } from '@/network/gamestate/replay/TauntReplayEvent';\nimport { TauntPlayback } from '@/gui/screen/game/TauntPlayback';\nimport { CommandBarButtonType } from '@/gui/screen/game/component/hud/commandBar/CommandBarButtonType';\nimport { isIpad } from '@/util/userAgent';\nimport { RootScreen } from '@/gui/screen/RootScreen';\nimport { LoadingScreenApiFactory, LoadingScreenType } from '@/gui/screen/game/loadingScreen/LoadingScreenApiFactory';\nimport { MapFile } from '@/data/MapFile';\nimport { ResourceLoader } from '@/engine/ResourceLoader';\nimport { MapDigest } from '@/engine/MapDigest';\nimport { ChatHistory } from '@/gui/chat/ChatHistory';\ninterface Replay {\n    gameId: string;\n    gameTimestamp: number;\n    gameOpts: GameOpts;\n    engineVersion: string;\n    modHash: string;\n}\ninterface GameOpts {\n    mapName: string;\n    mapDigest: string;\n    mapOfficial: boolean;\n    humanPlayers: Array<{\n        name: string;\n        countryId: number;\n        colorId: number;\n    }>;\n}\ninterface ReplayParams {\n    replay: Replay;\n}\ninterface Config {\n    devMode: boolean;\n    discordUrl?: string;\n}\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface Renderer {\n    removeScene(scene: any): void;\n    addScene(scene: any): void;\n}\ninterface UiScene {\n    viewport: any;\n    add(object: any): void;\n    remove(object: any): void;\n}\ninterface RuntimeVars {\n    debugText: any;\n    freeCamera: any;\n    debugPaths: any;\n}\ninterface MessageBoxApi {\n    show(message: string, buttonText: string, onClose?: () => void): void;\n}\ninterface UiAnimationLoop {\n    stop(): void;\n    start(): void;\n}\ninterface Viewport {\n    value: any;\n}\ninterface JsxRenderer {\n    render(jsx: any): any[];\n}\ninterface Pointer {\n    lock(): void;\n    unlock(): void;\n    setVisible(visible: boolean): void;\n    pointerEvents: any;\n}\ninterface Sound {\n    play(key: SoundKey, channel: ChannelType): void;\n    audioSystem: any;\n}\ninterface KeyBinds {\n}\ninterface GeneralOptions {\n}\ninterface ActionLogger {\n}\ninterface FullScreen {\n    onChange: {\n        subscribe(callback: (value: any) => void): void;\n        unsubscribe(callback: (value: any) => void): void;\n    };\n}\ninterface MapFileLoader {\n    load(mapName: string): Promise<any>;\n}\ninterface GameLoader {\n    load(gameId: string, gameTimestamp: number, gameOpts: GameOpts, mapFile: MapFile, localPlayer?: any, isSinglePlayer?: boolean, loadingScreenApi?: any): Promise<{\n        game: Game;\n        theater: Theater;\n        hudSide: any;\n        cameoFilenames: string[];\n    }>;\n    clearStaticCaches(): void;\n}\ninterface VxlGeometryPool {\n}\ninterface BuildingImageDataCache {\n}\ninterface ErrorHandler {\n    handle(error: any, message: string, onClose?: () => void): void;\n}\ninterface Game {\n    speed: {\n        value: number;\n    };\n    desiredSpeed: {\n        value: number;\n    };\n    rules: {\n        audioVisual: {\n            messageDuration: number;\n        };\n        general: {\n            radar: any;\n        };\n    };\n    debugText: any;\n    getCombatants(): any[];\n    stalemateDetectTrait: any;\n    countdownTimer: any;\n    start(): void;\n    getPlayer(playerId: number): Player;\n    events: any;\n    gameOpts: GameOpts;\n    getUnitSelection(): any;\n}\ninterface Player {\n    name: string;\n    color: {\n        asHexString(): string;\n    };\n}\ninterface Theater {\n    type: any;\n}\ninterface LoadingScreenApi {\n    updateViewport(): void;\n    dispose(): void;\n}\ninterface Hud {\n    sidebarWidth: number;\n    actionBarHeight: number;\n    onCommandBarButtonClick: {\n        subscribe(callback: (buttonType: CommandBarButtonType) => void): void;\n    };\n    getTextColor(): string;\n    setMinimap(minimap: Minimap): void;\n    destroy(): void;\n}\ninterface WorldInteraction {\n    setEnabled(enabled: boolean): void;\n}\ninterface PlayerUi {\n    onPlayerChange: {\n        subscribe(callback: (data: {\n            player: Player;\n            sidebarModel: SidebarModel;\n        }) => void): void;\n    };\n    worldInteraction: WorldInteraction;\n    init(hud: Hud): void;\n    handleHudChange(hud: Hud): void;\n    dispose(): void;\n}\ninterface GameMenuType {\n    onOpen: {\n        subscribe(callback: () => void): void;\n    };\n    onQuit: {\n        subscribe(callback: () => void): void;\n    };\n    onCancel: {\n        subscribe(callback: () => void): void;\n    };\n    handleHudChange(hud: Hud): void;\n}\nexport class ReplayScreen extends RootScreen {\n    public preventUnload = true;\n    private disposables = new CompositeDisposable();\n    private params?: ReplayParams;\n    private game?: Game;\n    private baseSpeed = 0;\n    private sidebarModel?: SidebarModel;\n    private messageList?: MessageList;\n    private hudFactory?: HudFactory;\n    private hud?: Hud;\n    private minimap?: Minimap;\n    private worldView?: WorldView;\n    private activeWorldScene?: any;\n    private gameTurnMgr?: ReplayTurnManager;\n    private gameAnimationLoop?: GameAnimationLoop;\n    private menu?: GameMenuType;\n    private playerUi?: PlayerUi;\n    private loadingScreenApi?: LoadingScreenApi;\n    private replayEndHandled = false;\n    constructor(private engineVersion: string, private engineModHash: string, private errorHandler: ErrorHandler, private gameMenuSubScreens: any, private loadingScreenApiFactory: LoadingScreenApiFactory, private config: Config, private strings: Strings, private renderer: Renderer, private uiScene: UiScene, private runtimeVars: RuntimeVars, private messageBoxApi: MessageBoxApi, private uiAnimationLoop: UiAnimationLoop, private viewport: Viewport, private jsxRenderer: JsxRenderer, private pointer: Pointer, private sound: Sound, private music: Music, private keyBinds: KeyBinds, private generalOptions: GeneralOptions, private actionLogger: ActionLogger, private fullScreen: FullScreen, private mapFileLoader: MapFileLoader, private gameLoader: GameLoader, private vxlGeometryPool: VxlGeometryPool, private buildingImageDataCache: BuildingImageDataCache, private leaveAction: (params?: any) => void, private battleControlApi: any) {\n        super();\n    }\n    async onEnter(params: ReplayParams): Promise<void> {\n        this.replayEndHandled = false;\n        this.params = params;\n        this.disposables.add(() => (this.params = undefined));\n        this.pointer.lock();\n        this.pointer.setVisible(false);\n        await this.music?.play(MusicType.Loading);\n        const { gameId, gameTimestamp, gameOpts, engineVersion, modHash } = params.replay;\n        let errorMessage: string | undefined;\n        if (engineVersion !== this.engineVersion) {\n            errorMessage = this.strings.get(\"GUI:ReplayVersionMismatch\", engineVersion);\n        }\n        else if (modHash !== this.engineModHash) {\n            errorMessage = this.strings.get(\"GUI:ReplayModMismatch\");\n        }\n        if (errorMessage) {\n            this.messageBoxApi.show(errorMessage, this.strings.get(\"GUI:Ok\"), () => {\n                this.leaveAction();\n            });\n            return;\n        }\n        const loadingScreenApi = this.loadingScreenApiFactory.create(LoadingScreenType.Replay);\n        this.loadingScreenApi = loadingScreenApi;\n        this.disposables.add(loadingScreenApi, () => (this.loadingScreenApi = undefined));\n        let gameData: {\n            game: Game;\n            theater: Theater;\n            hudSide: any;\n            cameoFilenames: string[];\n        };\n        const mapName = gameOpts.mapName;\n        try {\n            const mapFileData = await this.mapFileLoader.load(mapName);\n            if (MapDigest.compute(mapFileData) !== gameOpts.mapDigest) {\n                this.handleError(\"Map digest mismatch\", this.strings.get(\"TS:MapMismatch\", mapName));\n                return;\n            }\n            const mapFile = new MapFile(mapFileData);\n            gameData = await this.gameLoader.load(gameId, gameTimestamp, gameOpts, mapFile, undefined, gameOpts.humanPlayers.length === 1, loadingScreenApi);\n        }\n        catch (error: any) {\n            let message: string;\n            if (error.message?.match(/memory|allocation/i)) {\n                message = this.strings.get(\"TS:GameInitOom\");\n            }\n            else if (error.name === 'DownloadError') {\n                message = this.strings.get(\"TS:MapNotFound\", mapName);\n            }\n            else {\n                message = this.strings.get(\"TS:GameInitError\");\n                if (!gameOpts.mapOfficial) {\n                    message += \"\\n\\n\" + this.strings.get(\"TS:CustomMapCrash\");\n                }\n            }\n            this.handleError(error, message);\n            return;\n        }\n        const { game, theater, hudSide, cameoFilenames } = gameData;\n        this.game = game;\n        this.baseSpeed = this.game.speed.value;\n        this.disposables.add(() => (this.game = undefined));\n        this.disposables.add(() => {\n            Engine.unloadTheater(theater.type);\n            this.gameLoader.clearStaticCaches();\n        });\n        this.disposables.add(game as any);\n        const sidebarModel = new SidebarModel(game, params.replay);\n        const messageList = new MessageList(game.rules.audioVisual.messageDuration, 6, undefined);\n        const chatHistory = new ChatHistory();\n        this.sidebarModel = sidebarModel;\n        this.disposables.add(() => (this.sidebarModel = undefined));\n        this.messageList = messageList;\n        this.disposables.add(() => (this.messageList = undefined));\n        const replayCommandButtons = [\n            CommandBarButtonType.ReplayRewind,\n            CommandBarButtonType.ReplayPlay,\n            CommandBarButtonType.ReplayPause,\n            CommandBarButtonType.ReplaySpeed\n        ];\n        this.hudFactory = new HudFactory(hudSide, this.viewport.value, sidebarModel, messageList, chatHistory, game.debugText, this.runtimeVars.debugText, undefined, game.getCombatants(), game.stalemateDetectTrait, game.countdownTimer, cameoFilenames, this.jsxRenderer, this.strings, replayCommandButtons, undefined);\n        this.disposables.add(() => (this.hudFactory = undefined));\n        const hud = this.hudFactory.create();\n        this.hud = hud;\n        const minimap = this.minimap = new Minimap(game, undefined, hud.getTextColor() as any, game.rules.general.radar as any);\n        hud.setMinimap(minimap);\n        this.disposables.add(minimap, () => (this.minimap = undefined));\n        minimap.setPointerEvents(this.pointer.pointerEvents);\n        const hudDimensions = { width: hud.sidebarWidth, height: hud.actionBarHeight };\n        const worldView = new WorldView(hudDimensions, game, this.sound, this.renderer, this.runtimeVars, minimap, this.strings, this.generalOptions, this.vxlGeometryPool, this.buildingImageDataCache);\n        const { worldScene, worldSound, renderableManager } = worldView.init(undefined, this.viewport.value, theater);\n        this.worldView = worldView;\n        this.disposables.add(worldView, () => (this.worldView = undefined));\n        worldScene.create3DObject();\n        const actionFactory = new ActionFactory();\n        new ActionFactoryReg().register(actionFactory, game, undefined);\n        const gameTurnMgr = this.gameTurnMgr = new ReplayTurnManager(game, params.replay, actionFactory, this.actionLogger as any);\n        this.gameTurnMgr.init();\n        const tauntPlayback = new TauntPlayback(this.sound.audioSystem, Engine.getTaunts());\n        const handleReplayEvent = (event: any) => {\n            if (event instanceof ChatMessageReplayEvent) {\n                const payload = event.payload;\n                const player = game.getPlayer(payload.playerId);\n                const message = this.strings.get(\"TS:ReplayChatFrom\", player.name) + \" \" + payload.message;\n                const color = player.color.asHexString();\n                messageList.addChatMessage(message, color);\n            }\n            else if (event instanceof TauntReplayEvent) {\n                const payload = event.payload;\n                const player = game.getPlayer(payload.playerId);\n                const tauntNo = payload.tauntNo;\n                tauntPlayback.playTaunt(player, tauntNo).catch((error: any) => console.error(error));\n            }\n        };\n        gameTurnMgr.onReplayEvent.subscribe(handleReplayEvent);\n        this.disposables.add(() => gameTurnMgr.onReplayEvent.unsubscribe(handleReplayEvent));\n        this.onGameStart(game, minimap, messageList, worldScene, worldSound, renderableManager);\n        DevToolsApi.registerCommand(\"reset\", async () => {\n            await this.onLeave();\n            await this.onEnter(params);\n        });\n        DevToolsApi.registerVar(\"speed\", game.desiredSpeed as any);\n        this.disposables.add(() => DevToolsApi.unregisterCommand(\"reset\"), () => DevToolsApi.unregisterVar(\"speed\"));\n    }\n    onViewportChange(): void {\n        this.loadingScreenApi?.updateViewport();\n        this.rerenderHud();\n    }\n    private rerenderHud(): void {\n        if (!this.hud)\n            return;\n        this.uiScene.remove(this.hud);\n        this.hud.destroy();\n        this.hudFactory!.setSidebarModel(this.sidebarModel!);\n        this.hudFactory!.setViewport(this.viewport.value);\n        const hud = this.hudFactory!.create();\n        this.hud = hud;\n        hud.setMinimap(this.minimap!);\n        this.worldView?.handleViewportChange(this.viewport.value);\n        if (this.worldView) {\n            this.uiScene.add(hud);\n            this.menu?.handleHudChange(hud);\n            this.playerUi?.handleHudChange(hud);\n            this.initHudEvents(hud, this.messageList!);\n        }\n    }\n    private onGameStart(game: Game, minimap: Minimap, messageList: MessageList, worldScene: any, worldSound: any, renderableManager: any): void {\n        this.loadingScreenApi?.dispose();\n        this.music?.play(MusicType.Normal);\n        const evaSpecs = new EvaSpecs(SideType.GDI).readIni(Engine.getIni(\"eva.ini\"));\n        const eva = new Eva(evaSpecs, this.sound as any, this.renderer as any);\n        eva.init();\n        this.disposables.add(eva);\n        try {\n            this.initUi(game, worldScene, worldSound, eva, renderableManager, minimap, messageList);\n        }\n        catch (error: any) {\n            const message = error.message?.match(/memory|allocation/i)\n                ? this.strings.get(\"TS:GameInitOom\")\n                : this.strings.get(\"TS:GameInitError\");\n            this.handleError(error, message);\n            return;\n        }\n        this.activeWorldScene = worldScene;\n        this.renderer.removeScene(this.uiScene);\n        this.renderer.addScene(worldScene);\n        this.renderer.addScene(this.uiScene);\n        this.pointer.setVisible(true);\n        game.start();\n        this.gameAnimationLoop = new GameAnimationLoop(undefined, this.renderer as any, this.sound, this.gameTurnMgr!, {\n            skipFrames: true,\n            skipBudgetMillis: 8,\n            onError: this.config.devMode ? undefined : (error: any, isCritical?: boolean) => this.handleError(error, this.strings.get(\"TS:GameCrashed\") +\n                (isCritical || game.gameOpts.mapOfficial\n                    ? \"\"\n                    : \"\\n\\n\" + this.strings.get(\"TS:CustomMapCrash\")), isCritical)\n        });\n        this.uiAnimationLoop.stop();\n        this.gameAnimationLoop.start();\n        const handleReplayFinished = () => this.onReplayEnd();\n        this.gameTurnMgr!.onFinished.subscribe(handleReplayFinished);\n        this.disposables.add(() => this.gameTurnMgr?.onFinished.unsubscribe(handleReplayFinished));\n    }\n    private initUi(game: Game, worldScene: any, worldSound: any, eva: Eva, renderableManager: any, minimap: Minimap, messageList: MessageList): void {\n        const soundHandler = new SoundHandler(game, worldSound, eva, this.sound, game.events, messageList, this.strings, undefined);\n        soundHandler.init();\n        this.disposables.add(soundHandler);\n        messageList.onNewMessage.subscribe((message: any) => {\n            if (message.animate) {\n                this.sound.play(SoundKey.IncomingMessage, ChannelType.Ui);\n            }\n        });\n        if (isIpad()) {\n            const handleFullScreenChange = (value: boolean) => {\n                this.sidebarModel!.topTextLeftAlign = value;\n            };\n            this.fullScreen.onChange.subscribe(handleFullScreenChange);\n            this.disposables.add(() => this.fullScreen.onChange.unsubscribe(handleFullScreenChange));\n        }\n        this.uiScene.add(this.hud!);\n        this.initHudEvents(this.hud!, messageList);\n        const menu = this.menu = new GameMenu(this.gameMenuSubScreens, game, undefined, undefined, undefined, true);\n        menu.init(this.hud!);\n        this.initGameMenuEvents(menu);\n        this.disposables.add(menu, () => (this.menu = undefined));\n        const unitSelection = game.getUnitSelection();\n        const freeCamera = this.runtimeVars.freeCamera;\n        const debugPaths = this.runtimeVars.debugPaths;\n        const debugText = this.runtimeVars.debugText;\n        const devMode = this.config.devMode;\n        const worldInteractionFactory = new WorldInteractionFactory(undefined, game, unitSelection, renderableManager, this.uiScene, worldScene, this.pointer, this.renderer, this.keyBinds, this.generalOptions, freeCamera, debugPaths, devMode, document, minimap, this.strings, this.hud!.getTextColor(), debugText, this.battleControlApi);\n        const discordUrl = this.config.discordUrl;\n        const playerUi = this.playerUi = new ObserverUi(game, undefined, this.sidebarModel!, this.params!.replay, this.renderer, worldScene, this.sound, worldInteractionFactory, menu, this.runtimeVars, this.strings, renderableManager, this.messageBoxApi, discordUrl) as any;\n        playerUi.onPlayerChange.subscribe(({ player, sidebarModel }) => {\n            this.sidebarModel = sidebarModel;\n            this.rerenderHud();\n            this.worldView?.changeLocalPlayer(player);\n            this.minimap!.changeLocalPlayer(player);\n        });\n        this.playerUi.init(this.hud!);\n        this.disposables.add(this.playerUi, () => (this.playerUi = undefined));\n    }\n    private initGameMenuEvents(menu: GameMenuType): void {\n        menu.onOpen.subscribe(() => {\n            this.pointer.unlock();\n            this.playerUi!.worldInteraction.setEnabled(false);\n        });\n        menu.onQuit.subscribe(async () => {\n            this.playerUi!.dispose();\n            this.gameTurnMgr!.dispose();\n            this.leaveAction();\n        });\n        menu.onCancel.subscribe(() => {\n            this.pointer.lock();\n            this.playerUi!.worldInteraction.setEnabled(true);\n        });\n    }\n    private initHudEvents(hud: Hud, messageList: MessageList): void {\n        hud.onCommandBarButtonClick.subscribe((buttonType: CommandBarButtonType) => {\n            this.sound.play(SoundKey.GenericClick, ChannelType.Ui);\n            switch (buttonType) {\n                case CommandBarButtonType.ReplayRewind:\n                    (async () => {\n                        const params = this.params!;\n                        await this.onLeave();\n                        await this.onEnter(params);\n                    })().catch((error: any) => console.error(error));\n                    break;\n                case CommandBarButtonType.ReplayPlay:\n                    this.game!.desiredSpeed.value = this.baseSpeed;\n                    if (this.game!.speed.value === Number.EPSILON) {\n                        this.gameTurnMgr!.doGameTurn(performance.now());\n                    }\n                    else if (this.game!.speed.value !== this.baseSpeed) {\n                        messageList.addSystemMessage(this.strings.get(\"TS:ReplaySpeedConfirm\", \"1x\"), \"grey\");\n                    }\n                    break;\n                case CommandBarButtonType.ReplayPause:\n                    this.game!.desiredSpeed.value = Number.EPSILON;\n                    break;\n                case CommandBarButtonType.ReplaySpeed: {\n                    if (this.game!.speed.value === Number.EPSILON) {\n                        this.game!.desiredSpeed.value = this.baseSpeed;\n                        this.gameTurnMgr!.doGameTurn(performance.now());\n                    }\n                    let speedMultiplier = Math.floor(this.game!.desiredSpeed.value / this.baseSpeed);\n                    speedMultiplier = speedMultiplier === 16 ? 1 : 2 * speedMultiplier;\n                    this.game!.desiredSpeed.value = speedMultiplier * this.baseSpeed;\n                    messageList.addSystemMessage(this.strings.get(\"TS:ReplaySpeedConfirm\", speedMultiplier + \"x\"), \"grey\");\n                    break;\n                }\n                default:\n                    console.warn(\"Unhandled command type \" + buttonType);\n            }\n        });\n    }\n    async onLeave(): Promise<void> {\n        this.pointer.unlock();\n        if (this.gameAnimationLoop) {\n            this.gameAnimationLoop.destroy();\n            this.gameAnimationLoop = undefined;\n            this.uiAnimationLoop.start();\n        }\n        if (this.activeWorldScene) {\n            this.renderer.removeScene(this.activeWorldScene);\n            this.activeWorldScene = undefined;\n        }\n        if (this.hud) {\n            this.uiScene.remove(this.hud);\n            this.hud.destroy();\n            this.hud = undefined;\n        }\n        this.gameTurnMgr?.dispose();\n        this.gameTurnMgr = undefined;\n        this.disposables.dispose();\n    }\n    private onReplayEnd(): void {\n        if (this.replayEndHandled) {\n            return;\n        }\n        this.replayEndHandled = true;\n    }\n    private handleError(error: any, message: string, isCritical?: boolean): void {\n        if (this.gameTurnMgr) {\n            this.gameTurnMgr.setErrorState();\n        }\n        this.pointer.unlock();\n        this.errorHandler.handle(error, message, isCritical ? undefined : () => {\n            this.leaveAction();\n        });\n        if (isCritical) {\n            this.playerUi?.dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/replay/ReplaySel.tsx",
    "content": "import React, { useRef, useEffect } from 'react';\nimport { List, ListItem } from '@/gui/component/List';\nimport { StorageWarning } from '@/gui/screen/replay/StorageWarning';\nimport { ReplayDetailsPane } from '@/gui/screen/replay/ReplayDetailsPane';\ninterface Replay {\n    id: string;\n    name: string;\n    timestamp: number;\n    keep?: boolean;\n}\ninterface ReplayDetails {\n    engineVersion: string;\n    durationSeconds?: number;\n    gameId: string;\n    gameTimestamp?: number;\n    mapName?: string;\n    players?: Array<{\n        name: string;\n        color: string;\n    }>;\n}\ninterface ReplaySelProps {\n    strings: {\n        get(key: string, ...args: any[]): string;\n    };\n    replays?: Replay[];\n    selectedReplay?: Replay;\n    selectedReplayDetails?: ReplayDetails;\n    onSelectReplay: (replay: Replay, doubleClick?: boolean) => void;\n}\nexport const ReplaySel: React.FC<ReplaySelProps> = ({ strings, replays, selectedReplay, selectedReplayDetails, onSelectReplay }) => {\n    const selectedRef = useRef<HTMLDivElement>(null);\n    useEffect(() => {\n        selectedRef.current?.scrollIntoView();\n    }, []);\n    return (<div className=\"replay-sel-form\">\n      <List title={strings.get(\"GUI:SelectReplay\")} className=\"replay-list\">\n        {replays ? (replays.map((replay) => {\n            const isSelected = replay.id === selectedReplay?.id;\n            return (<ListItem key={replay.id} selected={isSelected} innerRef={isSelected ? selectedRef : null} onClick={() => onSelectReplay(replay)} onDoubleClick={() => onSelectReplay(replay, true)} style={{ display: \"flex\" }}>\n                <div className=\"replay-name\">\n                  {replay.keep ? \"\" : \"* \"}\n                  {replay.name}\n                </div>\n                <div className=\"replay-time\" dir=\"auto\">\n                  {new Date(replay.timestamp).toLocaleString()}\n                </div>\n              </ListItem>);\n        })) : (<ListItem style={{ textAlign: \"center\" }}>\n            {strings.get(\"GUI:LoadingEx\")}\n          </ListItem>)}\n      </List>\n      {selectedReplayDetails && (<ReplayDetailsPane replayDetails={selectedReplayDetails} strings={strings}/>)}\n      <StorageWarning strings={strings}/>\n    </div>);\n};\n"
  },
  {
    "path": "src/gui/screen/replay/ReplaySelScreen.ts",
    "content": "import { jsx } from '@/gui/jsx/jsx';\nimport { HtmlView } from '@/gui/jsx/HtmlView';\nimport { ReplaySel } from '@/gui/screen/replay/ReplaySel';\nimport { Replay } from '@/network/gamestate/Replay';\nimport { ScreenType } from '@/gui/screen/ScreenType';\nimport { KeepReplayBox } from '@/gui/screen/replay/KeepReplayBox';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { ReplayStorageError } from '@/gui/replay/ReplayStorageError';\nimport { ResourceLoader } from '@/engine/ResourceLoader';\nimport { StorageQuotaError } from '@/data/vfs/StorageQuotaError';\nimport { Task } from '@puzzl/core/lib/async/Task';\nimport { Parser } from '@/network/gameopt/Parser';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { ReplayExistsError } from '@/gui/replay/ReplayExistsError';\nimport { MainMenuScreen } from '@/gui/screen/mainMenu/MainMenuScreen';\nimport { OperationCanceledError } from '@puzzl/core/lib/async/cancellation';\nimport { IOError } from '@/data/vfs/IOError';\nimport { FileNotFoundError } from '@/data/vfs/FileNotFoundError';\nimport { RouteHelper } from '@/RouteHelper';\nimport { GameOptRandomGen } from '@/game/gameopts/GameOptRandomGen';\nimport { OBS_COUNTRY_ID } from '@/game/gameopts/constants';\ninterface ReplayMeta {\n    id: string;\n    name: string;\n    timestamp: number;\n    keep?: boolean;\n}\ninterface ReplayDetails {\n    engineVersion: string;\n    durationSeconds?: number;\n    gameId: string;\n    gameTimestamp?: number;\n    mapName?: string;\n    players?: Array<{\n        name: string;\n        color: string;\n    }>;\n}\ninterface Strings {\n    get(key: string, ...args: any[]): string;\n}\ninterface ErrorHandler {\n    handle(error: any, message: string, onClose?: () => void): void;\n}\ninterface MessageBoxApi {\n    show(message: string, buttonText?: string, onClose?: () => void): void;\n    confirm(message: string, okText: string, cancelText: string): Promise<boolean>;\n    destroy(): void;\n}\ninterface ReplayManager {\n    loadList(includeTemp?: boolean): Promise<ReplayMeta[]>;\n    importReplay(file: File): Promise<void>;\n    loadSerializedReplay(replay: ReplayMeta): Promise<string | Blob>;\n    keepReplay(id: string, name: string): Promise<void>;\n    deleteReplay(replay: ReplayMeta): Promise<void>;\n    loadReplay(replay: ReplayMeta): Promise<any>;\n}\ninterface JsxRenderer {\n    render(jsx: any): any[];\n}\ninterface UiScene {\n    viewport: any;\n    add(object: any): void;\n    remove(object: any): void;\n}\ninterface Rules {\n    getMultiplayerColors(): Map<number, any>;\n}\ninterface RootController {\n    goToScreen(screenType: ScreenType, params?: any): void;\n}\ninterface GameOpts {\n    mapName: string;\n    mapTitle?: string;\n    mapDigest: string;\n    mapOfficial: boolean;\n    humanPlayers: Array<{\n        name: string;\n        countryId: number;\n        colorId: number;\n    }>;\n}\ninterface ReplayHeader {\n    gameId: string;\n    gameTimestamp?: number;\n    engineVersion: string;\n    modHash: string;\n    gameOptsSerialized: string;\n}\nexport class ReplaySelScreen extends MainMenuScreen {\n    declare public title: string;\n    private disposables: CompositeDisposable;\n    private availableReplays: ReplayMeta[] = [];\n    private selectedReplay?: ReplayMeta;\n    private form?: any;\n    private fileInput?: HTMLInputElement;\n    private clientVersions?: Record<string, string>;\n    private currentReplayUrl?: string;\n    private replayDetailsTask?: Task<void>;\n    constructor(private engineVersion: string, private engineModHash: string, private activeMod?: string, private oldClientsBaseUrl?: string, private rootController?: RootController, private strings?: Strings, private jsxRenderer?: JsxRenderer, private errorHandler?: ErrorHandler, private messageBoxApi?: MessageBoxApi, private replayManager?: ReplayManager, private uiScene?: UiScene, private rules?: Rules, private sentry?: any) {\n        super();\n        this.title = this.strings?.get(\"GUI:Replays\") || \"Replays\";\n        this.disposables = new CompositeDisposable();\n        this.handleSelectReplay = this.handleSelectReplay.bind(this);\n    }\n    private handleSelectReplay = (replay: ReplayMeta, doubleClick?: boolean): void => {\n        const isNewSelection = this.selectedReplay?.id !== replay.id;\n        this.selectedReplay = replay;\n        if (isNewSelection) {\n            this.updateSidebarButtons();\n        }\n        this.form?.applyOptions((options: any) => {\n            options.selectedReplay = replay;\n            options.selectedReplayDetails = undefined;\n        });\n        if (doubleClick) {\n            this.loadSelectedReplay();\n        }\n        else {\n            this.loadReplayDetails(replay);\n        }\n    };\n    async onEnter(): Promise<void> {\n        this.availableReplays = [];\n        this.controller?.toggleMainVideo(false);\n        this.initForm();\n        try {\n            this.availableReplays = await this.replayManager!.loadList(true);\n        }\n        catch (error: any) {\n            if (!(error instanceof IOError) &&\n                !(error instanceof FileNotFoundError) &&\n                !(error instanceof StorageQuotaError)) {\n                this.sentry?.captureException(new Error(`Failed to load replay list (${error.name ?? error.message})`, { cause: error }));\n            }\n            this.handleError(error, this.strings!.get(\"GUI:ReplayListError\"));\n            return;\n        }\n        this.selectedReplay = this.availableReplays[0];\n        this.form?.applyOptions((options: any) => {\n            options.replays = this.availableReplays;\n            options.selectedReplay = this.selectedReplay;\n        });\n        if (this.selectedReplay !== undefined) {\n            this.loadReplayDetails(this.selectedReplay);\n        }\n        this.initSidebar();\n        this.initFileInput();\n    }\n    private initForm(): void {\n        this.controller?.setMainComponent(this.jsxRenderer!.render(jsx(HtmlView, {\n            innerRef: (ref: any) => (this.form = ref),\n            component: ReplaySel,\n            props: {\n                strings: this.strings,\n                replays: undefined,\n                selectedReplay: undefined,\n                selectedReplayDetails: undefined,\n                onSelectReplay: this.handleSelectReplay\n            }\n        }))[0]);\n    }\n    private initFileInput(): void {\n        const input = this.fileInput = document.createElement(\"input\");\n        input.setAttribute(\"type\", \"file\");\n        input.setAttribute(\"accept\", Replay.extension);\n        input.setAttribute(\"style\", \"display: none\");\n        document.body.appendChild(input);\n        const handleChange = async (): Promise<void> => {\n            const file = this.fileInput?.files?.[0];\n            if (!file)\n                return;\n            try {\n                await this.replayManager!.importReplay(file);\n                this.availableReplays = await this.replayManager!.loadList();\n                this.form?.applyOptions((options: any) => {\n                    options.replays = this.availableReplays;\n                });\n            }\n            catch (error: any) {\n                let message: string;\n                if (error instanceof StorageQuotaError) {\n                    message = this.strings!.get(\"ts:storage_quota_exceeded\");\n                }\n                else if (error instanceof ReplayStorageError) {\n                    message = this.strings!.get(\"GUI:SaveReplayError\");\n                }\n                else {\n                    message = this.strings!.get(\"GUI:ImportReplayError\");\n                }\n                this.errorHandler!.handle(error, message, () => { });\n            }\n        };\n        input.addEventListener(\"change\", handleChange);\n        this.disposables.add(() => {\n            if (this.fileInput) {\n                document.body.removeChild(this.fileInput);\n                this.fileInput.removeEventListener(\"change\", handleChange);\n                this.fileInput = undefined;\n            }\n        });\n    }\n    private initSidebar(): void {\n        this.updateSidebarButtons();\n        this.controller?.showSidebarButtons();\n    }\n    private updateSidebarButtons(): void {\n        const replayMeta = this.getSelectedReplayMeta();\n        this.controller?.setSidebarButtons([\n            {\n                label: this.strings!.get(\"GUI:LoadReplay\"),\n                disabled: !this.selectedReplay,\n                onClick: () => {\n                    this.loadSelectedReplay();\n                }\n            },\n            {\n                label: this.strings!.get(replayMeta?.keep ? \"GUI:RenameReplay\" : \"GUI:KeepReplay\"),\n                tooltip: replayMeta?.keep ? undefined : this.strings!.get(\"STT:KeepReplay\"),\n                disabled: !this.selectedReplay,\n                onClick: () => {\n                    this.showKeepReplayBox(replayMeta!.name, (name: string) => {\n                        this.replayManager!.keepReplay(replayMeta!.id, name)\n                            .then(async () => {\n                            this.availableReplays = await this.replayManager!.loadList();\n                            this.selectedReplay = this.getSelectedReplayMeta();\n                            this.form?.applyOptions((options: any) => {\n                                options.replays = this.availableReplays;\n                            });\n                            this.updateSidebarButtons();\n                        })\n                            .catch((error: any) => {\n                            let message: string;\n                            if (error instanceof ReplayExistsError) {\n                                message = this.strings!.get(\"GUI:ReplayExistsError\");\n                            }\n                            else {\n                                message = this.strings!.get(\"GUI:SaveReplayError\");\n                            }\n                            this.errorHandler!.handle(error, message, () => { });\n                        });\n                    });\n                }\n            },\n            {\n                label: this.strings!.get(\"GUI:ImportReplay\"),\n                tooltip: this.strings!.get(\"STT:ImportReplay\"),\n                onClick: () => {\n                    if (this.fileInput?.click !== undefined) {\n                        this.fileInput.click();\n                    }\n                    else {\n                        const event = document.createEvent(\"Event\");\n                        event.initEvent(\"click\", true, true);\n                        this.fileInput?.dispatchEvent(event);\n                    }\n                }\n            },\n            {\n                label: this.strings!.get(\"GUI:ExportReplay\"),\n                tooltip: this.strings!.get(\"STT:ExportReplay\"),\n                disabled: !this.selectedReplay,\n                onClick: () => {\n                    this.exportCurrentReplay().catch((error: any) => this.errorHandler!.handle(error, this.strings!.get(\"GUI:ReplayError\"), () => { }));\n                }\n            },\n            {\n                label: this.strings!.get(\"GUI:DeleteReplay\"),\n                disabled: !this.selectedReplay,\n                onClick: async () => {\n                    const replayMeta = this.getSelectedReplayMeta();\n                    if (!replayMeta)\n                        return;\n                    const confirmed = await this.messageBoxApi!.confirm(this.strings!.get(\"GUI:ConfirmDeleteReplay\", replayMeta.name), this.strings!.get(\"GUI:Ok\"), this.strings!.get(\"GUI:Cancel\"));\n                    if (confirmed) {\n                        try {\n                            await this.replayManager!.deleteReplay(replayMeta);\n                        }\n                        catch (error: any) {\n                            const message = error instanceof StorageQuotaError\n                                ? this.strings!.get(\"ts:storage_quota_exceeded\")\n                                : this.strings!.get(\"GUI:DeleteReplayError\");\n                            this.errorHandler!.handle(error, message, () => { });\n                            return;\n                        }\n                        this.selectedReplay = undefined;\n                        this.availableReplays = await this.replayManager!.loadList();\n                        this.form?.applyOptions((options: any) => {\n                            options.replays = this.availableReplays;\n                            options.selectedReplay = undefined;\n                            options.selectedReplayDetails = undefined;\n                        });\n                        this.updateSidebarButtons();\n                    }\n                }\n            },\n            {\n                label: this.strings!.get(\"GUI:Back\"),\n                isBottom: true,\n                onClick: () => {\n                    this.controller?.popScreen();\n                }\n            }\n        ]);\n    }\n    private async loadSelectedReplay(): Promise<void> {\n        const replay = this.selectedReplay;\n        if (!replay)\n            return;\n        let replayHeader: ReplayHeader;\n        try {\n            const serialized = await this.replayManager!.loadSerializedReplay(replay);\n            replayHeader = await new Replay().parseHeader(serialized);\n        }\n        catch (error: any) {\n            this.errorHandler!.handle(error, this.strings!.get(\"GUI:ReplayError\"), () => { });\n            return;\n        }\n        if (replayHeader.engineVersion !== this.engineVersion) {\n            if (!this.clientVersions && this.oldClientsBaseUrl) {\n                this.messageBoxApi!.show(this.strings!.get(\"GUI:LoadingEx\"));\n                try {\n                    const loader = new ResourceLoader(this.oldClientsBaseUrl);\n                    this.clientVersions = await loader.loadJson(\"versions.json\");\n                }\n                catch (error) {\n                    console.warn(\"Couldn't download client version list\", error);\n                }\n                finally {\n                    this.messageBoxApi!.destroy();\n                }\n            }\n            const clientVersion = this.clientVersions?.[replayHeader.engineVersion];\n            if (clientVersion) {\n                const confirmed = await this.messageBoxApi!.confirm(this.strings!.get(\"GUI:ReplayOpenOldClient\", replayHeader.engineVersion), this.strings!.get(\"TXT_CONTINUE\"), this.strings!.get(\"GUI:Close\"));\n                if (confirmed) {\n                    const modQuery = this.activeMod\n                        ? `?${RouteHelper.modQueryStringName}=${this.activeMod}`\n                        : \"\";\n                    window.open(`${this.oldClientsBaseUrl}v${clientVersion}/${modQuery}#/replay/${replay.id}`, \"_blank\");\n                }\n            }\n            else {\n                this.messageBoxApi!.show(this.strings!.get(\"GUI:ReplayVersionMismatch\", replayHeader.engineVersion), this.strings!.get(\"GUI:Ok\"));\n            }\n            return;\n        }\n        if (replayHeader.modHash === this.engineModHash) {\n            let loadedReplay: any;\n            try {\n                loadedReplay = await this.replayManager!.loadReplay(replay);\n            }\n            catch (error: any) {\n                this.errorHandler!.handle(error, this.strings!.get(\"GUI:ReplayError\"), () => { });\n                return;\n            }\n            this.rootController!.goToScreen(ScreenType.Replay, {\n                replay: loadedReplay\n            });\n        }\n        else {\n            this.messageBoxApi!.show(this.strings!.get(\"GUI:ReplayModMismatch\"), this.strings!.get(\"GUI:Ok\"));\n        }\n    }\n    private showKeepReplayBox(defaultName: string, onSubmit: (name: string) => void): void {\n        const [component] = this.jsxRenderer!.render(jsx(HtmlView, {\n            component: KeepReplayBox,\n            props: {\n                defaultName,\n                strings: this.strings,\n                onSubmit: (name: string) => {\n                    onSubmit(name);\n                    component.destroy();\n                },\n                onDismiss: () => {\n                    component.destroy();\n                },\n                viewport: this.uiScene!.viewport\n            }\n        }));\n        this.uiScene!.add(component);\n        this.disposables.add(component, () => this.uiScene!.remove(component));\n    }\n    private async exportCurrentReplay(): Promise<void> {\n        const replay = this.getSelectedReplayMeta();\n        if (!replay) {\n            throw new Error(\"No replay selected\");\n        }\n        const serialized = await this.replayManager!.loadSerializedReplay(replay);\n        if (this.currentReplayUrl) {\n            URL.revokeObjectURL(this.currentReplayUrl);\n        }\n        const blob = new Blob([serialized], { type: \"application/octet-stream\" });\n        this.currentReplayUrl = URL.createObjectURL(blob);\n        const link = document.createElement(\"a\");\n        link.setAttribute(\"href\", this.currentReplayUrl);\n        link.setAttribute(\"download\", replay.name + Replay.extension);\n        document.body.appendChild(link);\n        link.click();\n        document.body.removeChild(link);\n    }\n    private getSelectedReplayMeta(): ReplayMeta | undefined {\n        if (!this.selectedReplay)\n            return undefined;\n        return this.availableReplays.find(replay => replay.id === this.selectedReplay!.id);\n    }\n    async onLeave(): Promise<void> {\n        if (this.currentReplayUrl) {\n            URL.revokeObjectURL(this.currentReplayUrl);\n        }\n        this.clientVersions = undefined;\n        this.availableReplays.length = 0;\n        this.form = undefined;\n        this.messageBoxApi?.destroy();\n        this.disposables.dispose();\n        this.replayDetailsTask?.cancel();\n        this.replayDetailsTask = undefined;\n        this.controller?.setMainComponent();\n        await this.controller?.hideSidebarButtons();\n    }\n    private loadReplayDetails(replay: ReplayMeta): void {\n        this.replayDetailsTask?.cancel();\n        this.replayDetailsTask = new Task(async (cancellationToken) => {\n            const serialized = await this.replayManager!.loadSerializedReplay(replay);\n            const header = await new Replay().parseHeader(serialized);\n            cancellationToken.throwIfCancelled();\n            let gameOpts: GameOpts | undefined;\n            let durationSeconds: number | undefined;\n            if (header.engineVersion === this.engineVersion) {\n                const replayInstance = new Replay();\n                const content = typeof serialized === \"string\"\n                    ? serialized\n                    : await serialized.text();\n                replayInstance.unserialize(content, replay);\n                cancellationToken.throwIfCancelled();\n                gameOpts = replayInstance.gameOpts;\n                durationSeconds = Math.floor(replayInstance.endTick / GameSpeed.BASE_TICKS_PER_SECOND);\n            }\n            else {\n                try {\n                    gameOpts = new Parser().parseOptions(header.gameOptsSerialized);\n                }\n                catch (error) {\n                    console.warn(\"Replay couldn't be parsed\", error);\n                }\n            }\n            let players: Array<{\n                name: string;\n                color: string;\n            }> | undefined;\n            if (gameOpts) {\n                const randomGen = GameOptRandomGen.factory(header.gameId, header.gameTimestamp);\n                const generatedColors = randomGen.generateColors(gameOpts as any);\n                const availableColors = this.getAvailablePlayerColors();\n                players = gameOpts.humanPlayers\n                    .filter(player => player.countryId !== OBS_COUNTRY_ID)\n                    .map(player => ({\n                    name: player.name,\n                    color: availableColors[generatedColors.get(player) ?? player.colorId]\n                }));\n            }\n            const details: ReplayDetails = {\n                gameId: header.gameId,\n                gameTimestamp: header.gameTimestamp,\n                engineVersion: header.engineVersion,\n                durationSeconds,\n                mapName: gameOpts?.mapTitle,\n                players\n            };\n            this.form?.applyOptions((options: any) => {\n                options.selectedReplayDetails = details;\n            });\n        });\n        this.replayDetailsTask.start().catch((error: any) => {\n            if (!(error instanceof OperationCanceledError)) {\n                console.error(error);\n            }\n        });\n    }\n    private getAvailablePlayerColors(): string[] {\n        return [...this.rules!.getMultiplayerColors().values()].map((color: any) => color.asHexString());\n    }\n    private handleError(error: any, message: string): void {\n        this.errorHandler!.handle(error, message, () => {\n            this.rootController!.goToScreen(ScreenType.MainMenuRoot);\n        });\n    }\n}\n"
  },
  {
    "path": "src/gui/screen/replay/StorageWarning.tsx",
    "content": "import React, { useState, useEffect } from 'react';\ninterface StorageEstimate {\n    quota?: number;\n    usage?: number;\n}\ninterface StorageWarningProps {\n    strings: {\n        get(key: string, ...args: any[]): string;\n    };\n}\nfunction formatMB(bytes: number): number {\n    return Math.ceil(bytes / 1024 / 1024);\n}\nexport const StorageWarning: React.FC<StorageWarningProps> = ({ strings }) => {\n    const [estimate, setEstimate] = useState<StorageEstimate>();\n    useEffect(() => {\n        if (navigator.storage?.estimate) {\n            navigator.storage\n                .estimate()\n                .then((estimate) => setEstimate(estimate))\n                .catch((error) => console.warn(\"Couldn't get storage estimate\", [error]));\n        }\n    }, []);\n    if (estimate?.quota &&\n        estimate.usage &&\n        estimate.quota - estimate.usage < 1048576) {\n        return (<div className=\"storage-warning\">\n        {strings.get(\"ts:storage_quota_warning\", formatMB(estimate.usage), formatMB(estimate.quota))}\n      </div>);\n    }\n    return null;\n};\n"
  },
  {
    "path": "src/main.tsx",
    "content": "import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport './setupThreeGlobal';\nimport App from './App.tsx';\nimport { MixEntry } from './data/MixEntry';\nimport { Crc32 } from './data/Crc32';\nimport { binaryStringToUint8Array } from './util/string';\nconsole.log(\"--- Hashing Test Start (with debug logging) ---\");\nMixEntry.hashFilename(\"ART.INI\", true);\nconsole.log(\"---\");\nMixEntry.hashFilename(\"A\", true);\nconsole.log(\"---\");\nMixEntry.hashFilename(\"RULES.INI\", true);\nMixEntry.hashFilename(\"ABCDE\", true);\nconsole.log(\"---\");\nMixEntry.hashFilename(\"RA2.\", true);\nconsole.log(\"--- Standard CRC32 Test (for Crc32 class itself) ---\");\nconst testData = binaryStringToUint8Array(\"123456789\");\nconst crcDirect = Crc32.calculateCrc(testData);\nconst knownStandardCRC32 = 0xCBF43926;\nconsole.log(`CRC32 for \"123456789\": ${crcDirect} (0x${crcDirect.toString(16).toUpperCase()})`);\nconsole.log(`Expected Standard CRC32: ${knownStandardCRC32} (0x${knownStandardCRC32.toString(16).toUpperCase()})`);\nif (crcDirect === knownStandardCRC32) {\n    console.log(\"Crc32.calculateCrc matches known standard CRC32 value for \\\"123456789\\\"!\");\n}\nelse {\n    console.error(\"Crc32.calculateCrc MISMATCH against known standard!\");\n}\nconsole.log(\"--- Hashing Test End ---\");\nimport { registerBuiltInBot } from './game/ai/thirdpartbot/builtIn/BuiltInBotAdapter';\n\n// Register built-in third-party bots\nregisterBuiltInBot();\n\ncreateRoot(document.getElementById('root')!).render(<React.StrictMode>\n    <App />\n  </React.StrictMode>);\n"
  },
  {
    "path": "src/network/HttpRequest.ts",
    "content": "import { OperationCanceledError, CancellationToken } from \"@puzzl/core/lib/async/cancellation\";\nexport class DownloadError extends Error {\n    public statusCode?: number;\n    constructor(message: string, options?: ErrorOptions, statusCode?: number) {\n        super(message, options);\n        this.name = \"DownloadError\";\n        this.statusCode = statusCode;\n    }\n}\ninterface FetchOptions {\n    body?: BodyInit | null;\n    method?: string;\n    headers?: HeadersInit;\n    onProgress?: (loadedDelta: number, totalLength?: number) => void;\n    allowHtmlMimeType?: boolean;\n}\ninterface FetchAndParseOptions {\n    url: string;\n    type: 'text' | 'binary' | 'json';\n}\nexport class HttpRequest {\n    async fetchText(url: string, cancellationToken?: CancellationToken, options?: FetchOptions): Promise<string> {\n        return await this.fetchAndParse({ url, type: \"text\" }, cancellationToken, options) as string;\n    }\n    async fetchBinary(url: string, cancellationToken?: CancellationToken, options?: FetchOptions): Promise<ArrayBuffer> {\n        const result = await this.fetchAndParse({ url, type: \"binary\" }, cancellationToken, options);\n        return result as ArrayBuffer;\n    }\n    async fetchJson(url: string, cancellationToken?: CancellationToken, options?: FetchOptions): Promise<any> {\n        return await this.fetchAndParse({ url, type: \"json\" }, cancellationToken, options);\n    }\n    async fetchHtml(url: string, cancellationToken?: CancellationToken, options?: FetchOptions): Promise<string> {\n        return await this.fetchAndParse({ url, type: \"text\" }, cancellationToken, {\n            ...options,\n            allowHtmlMimeType: true,\n        }) as string;\n    }\n    private async fetchAndParse(request: FetchAndParseOptions, cancellationToken?: CancellationToken, options?: FetchOptions): Promise<ArrayBuffer | string | any> {\n        const rawData = await this.fetchRaw(request.url, cancellationToken, options);\n        return this.parseResult(request.type, rawData);\n    }\n    async fetchRaw(url: string, cancellationToken?: CancellationToken, options?: FetchOptions): Promise<ArrayBuffer> {\n        const abortController = new AbortController();\n        cancellationToken?.register(() => {\n            abortController.abort();\n        });\n        let response: Response;\n        try {\n            response = await fetch(url, {\n                signal: abortController.signal,\n                body: options?.body,\n                method: options?.method,\n                headers: options?.headers,\n            });\n        }\n        catch (error: any) {\n            if (cancellationToken &&\n                (error.name === 'AbortError' ||\n                    (error instanceof DOMException && error.code === DOMException.ABORT_ERR) ||\n                    cancellationToken.isCancelled())) {\n                throw new OperationCanceledError(cancellationToken);\n            }\n            console.error('Fetch raw failed:', error);\n            throw new DownloadError(`Fetch failed with error: ${error.name}: ${error.message}`, { cause: error });\n        }\n        if (!response.ok) {\n            throw new DownloadError(`Fetch failed with status ${response.status}: ${response.statusText}`, undefined, response.status);\n        }\n        const contentType = response.headers.get(\"Content-Type\");\n        if (contentType && contentType.includes(\"text/html\") && !options?.allowHtmlMimeType) {\n            throw new DownloadError(`Fetch failed with invalid mime type \"${contentType}\" (HTTP status ${response.status})`);\n        }\n        if (!response.body) {\n            throw new DownloadError(\"Response has no body.\");\n        }\n        const reader = response.body.getReader();\n        const contentLength = Number(response.headers.get('Content-Length') || 0);\n        const chunks: Uint8Array[] = [];\n        let receivedLength = 0;\n        try {\n            while (true) {\n                cancellationToken?.throwIfCancelled();\n                const { done, value } = await reader.read();\n                if (cancellationToken?.isCancelled()) {\n                    reader.cancel('Download cancelled by user');\n                    throw new OperationCanceledError(cancellationToken);\n                }\n                if (done) {\n                    break;\n                }\n                chunks.push(value!);\n                receivedLength += value!.length;\n                options?.onProgress?.(value!.length, contentLength);\n            }\n        }\n        catch (error: any) {\n            if (error.name === 'AbortError' || error instanceof OperationCanceledError) {\n                throw error;\n            }\n            console.error('Error during response body reading:', error);\n            throw new DownloadError(`Failed to read response body: ${error.message}`, { cause: error });\n        }\n        const completeBuffer = new Uint8Array(receivedLength);\n        let position = 0;\n        for (const chunk of chunks) {\n            completeBuffer.set(chunk, position);\n            position += chunk.length;\n        }\n        return completeBuffer.buffer;\n    }\n    parseResult(type: 'text' | 'binary' | 'json', data: ArrayBuffer): ArrayBuffer | string | any {\n        if (type === \"binary\") {\n            return data;\n        }\n        const text = new TextDecoder(\"utf-8\").decode(data);\n        if (type === \"json\") {\n            try {\n                return JSON.parse(text);\n            }\n            catch (e: any) {\n                throw new Error(`Failed to parse JSON: ${e.message}. Content: ${text.substring(0, 100)}`);\n            }\n        }\n        return text;\n    }\n}\n"
  },
  {
    "path": "src/network/IrcConnection.ts",
    "content": "export class IrcConnection {\n    static SocketError = class SocketError extends Error {\n        constructor(message: string) {\n            super(message);\n            this.name = 'SocketError';\n        }\n    };\n    static NoReplyError = class NoReplyError extends Error {\n        constructor(message: string) {\n            super(message);\n            this.name = 'NoReplyError';\n        }\n    };\n    static ConnectError = class ConnectError extends Error {\n        constructor(message: string) {\n            super(message);\n            this.name = 'ConnectError';\n        }\n    };\n}\n"
  },
  {
    "path": "src/network/WolConnection.ts",
    "content": "export class WolConnection {\n    public static readonly MAX_ROOM_DESC_LEN = 64;\n}\n"
  },
  {
    "path": "src/network/WolGameReport.ts",
    "content": "import { Base64 } from \"@/util/Base64\";\nexport enum WolGameReportResult {\n    Win = 0,\n    Loss = 1,\n    Draw = 2\n}\nexport interface WolGameReportPlayer {\n    name: string;\n    resultType: WolGameReportResult;\n    [key: string]: any;\n}\nexport interface WolGameReportData {\n    gameId: string;\n    players: WolGameReportPlayer[];\n    duration: number;\n    [key: string]: any;\n}\nexport class WolGameReport {\n    public gameId: string;\n    public players: WolGameReportPlayer[];\n    public duration: number;\n    [key: string]: any;\n    constructor(encoded: string) {\n        const data: WolGameReportData = JSON.parse(Base64.decode(encoded));\n        this.gameId = data.gameId;\n        this.players = data.players;\n        this.duration = data.duration;\n        Object.assign(this, data);\n    }\n}\n"
  },
  {
    "path": "src/network/chat/ChatMessage.ts",
    "content": "export enum ChatRecipientType {\n    Channel = 0,\n    Page = 1,\n    Whisper = 2\n}\n"
  },
  {
    "path": "src/network/gameopt/FileNameEncoder.ts",
    "content": "import { Base64 } from '@/util/Base64';\nimport { binaryStringToUtf16, utf16ToBinaryString } from '@/util/string';\nexport class FileNameEncoder {\n    encode(fileName: string): string {\n        if (fileName.match(/^[a-z0-9-_]+\\.[a-z]{3}$/i)) {\n            return fileName;\n        }\n        return Base64.encode(utf16ToBinaryString(fileName));\n    }\n    decode(encodedFileName: string): string {\n        if (encodedFileName.match(/\\.[a-z]{3}$/i)) {\n            return encodedFileName;\n        }\n        return binaryStringToUtf16(Base64.decode(encodedFileName));\n    }\n}\n"
  },
  {
    "path": "src/network/gameopt/LoadInfoParser.ts",
    "content": "export interface LoadInfo {\n    name: string;\n    status: number;\n    loadPercent: number;\n    ping: number;\n    lagAllowanceMillis: number;\n}\nexport class LoadInfoParser {\n    parse(data: string): LoadInfo[] {\n        const result: LoadInfo[] = [];\n        const parts = data.split(',');\n        for (let i = 0; i < parts.length / 5; ++i) {\n            const playerInfo: LoadInfo = {\n                name: parts[5 * i],\n                status: Number(parts[5 * i + 1]),\n                loadPercent: Number(parts[5 * i + 2]),\n                ping: Number(parts[5 * i + 3]),\n                lagAllowanceMillis: Number(parts[5 * i + 4])\n            };\n            result.push(playerInfo);\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/network/gameopt/MapNameLegacyEncoder.ts",
    "content": "export class MapNameLegacyEncoder {\n    encode(mapName: string): string {\n        const bytes: number[] = [];\n        let extraIndex = 0;\n        mapName.split('').forEach((char, index) => {\n            const code = char.charCodeAt(0) << (2 * index - 7 * extraIndex);\n            const byte1 = code & 127;\n            const byte2 = (code >> 7) & 127;\n            const byte3 = (code >> 14) & 127;\n            if (byte3) {\n                extraIndex++;\n            }\n            bytes.push(byte1, byte2);\n            if (byte3) {\n                bytes.push(byte3);\n            }\n        });\n        bytes.push(0, 0);\n        if (mapName.length >= 2) {\n            bytes.push(0);\n        }\n        const xorBytes = bytes.map(byte => byte ^ 128);\n        return xorBytes.map(byte => String.fromCharCode(byte)).join('');\n    }\n    decode(encodedMapName: string): string {\n        let bytes = encodedMapName.split('').map(char => char.charCodeAt(0));\n        bytes = bytes.map(byte => byte ^ 128);\n        while (bytes.length > 0 && bytes[bytes.length - 1] === 0) {\n            bytes.pop();\n        }\n        const result: number[] = [];\n        let extraCount = 0;\n        let charIndex = 0;\n        while (bytes.length > 0) {\n            const currentPos = result.length;\n            const byte1 = bytes.shift()!;\n            const byte2 = bytes.shift()!;\n            let byte3 = 0;\n            let hasExtra = false;\n            if ((bytes.length > 0 && [1, 2, 3].includes(bytes[0])) || currentPos > extraCount + 3) {\n                byte3 = bytes.shift()!;\n                extraCount = currentPos;\n                hasExtra = true;\n            }\n            const combined = ((byte3 << 14) | (byte2 << 7) | byte1) >> (2 * currentPos - 7 * extraCount);\n            result.push(combined & 127);\n            if (hasExtra) {\n                charIndex++;\n            }\n        }\n        return result.map(code => String.fromCharCode(code)).join('');\n    }\n}\n"
  },
  {
    "path": "src/network/gameopt/Parser.ts",
    "content": "import { DataStream } from '@/data/DataStream';\nimport { MapNameLegacyEncoder } from './MapNameLegacyEncoder';\nimport { SlotType, SlotInfo, PingInfo } from './SlotInfo';\nimport { GameOpts, AiDifficulty, HumanPlayerInfo, AiPlayerInfo } from '@/game/gameopts/GameOpts';\nimport { FileNameEncoder } from './FileNameEncoder';\nimport { Base64 } from '@/util/Base64';\nimport { binaryStringToUtf16, uint8ArrayToBinaryString } from '@/util/string';\nexport class Parser {\n    parseOptions(optionsString: string): GameOpts {\n        const gameOpts: Partial<GameOpts> = {};\n        const [gameOptsPart, playersPart, , aiPart] = optionsString.split(':');\n        const parts = gameOptsPart.split(',');\n        parts.shift();\n        parts.shift();\n        gameOpts.gameSpeed = 6 - Number(parts.shift());\n        gameOpts.credits = Number(parts.shift());\n        gameOpts.unitCount = Number(parts.shift());\n        gameOpts.shortGame = Boolean(Number(parts.shift()));\n        gameOpts.superWeapons = Boolean(Number(parts.shift()));\n        gameOpts.buildOffAlly = Boolean(Number(parts.shift()));\n        gameOpts.mcvRepacks = Boolean(Number(parts.shift()));\n        gameOpts.cratesAppear = Boolean(Number(parts.shift()));\n        gameOpts.gameMode = Number(parts.shift());\n        gameOpts.hostTeams = Boolean(Number(parts.shift()));\n        const mapTitlePart = parts.shift()!;\n        gameOpts.mapTitle = Base64.isBase64(mapTitlePart)\n            ? binaryStringToUtf16(Base64.decode(mapTitlePart))\n            : new MapNameLegacyEncoder().decode(mapTitlePart);\n        gameOpts.maxSlots = Number(parts.shift());\n        gameOpts.mapOfficial = Boolean(Number(parts.shift()));\n        gameOpts.mapSizeBytes = Number(parts.shift());\n        gameOpts.mapName = new FileNameEncoder().decode(parts.shift()!);\n        gameOpts.mapDigest = parts.shift();\n        gameOpts.destroyableBridges = Boolean(Number(parts.shift() ?? '1'));\n        gameOpts.multiEngineer = Boolean(Number(parts.shift() ?? '0'));\n        gameOpts.noDogEngiKills = Boolean(Number(parts.shift() ?? '0'));\n        gameOpts.unknown = parts.length ? parts.join(',') : undefined;\n        gameOpts.humanPlayers = this.parsePlayerOpts(playersPart);\n        gameOpts.aiPlayers = this.parseAiOpts(aiPart?.slice(0, -1));\n        return gameOpts as GameOpts;\n    }\n    parsePlayerOpts(playersString: string): HumanPlayerInfo[] {\n        const parts = playersString.split(',');\n        if (parts.length % 8 !== 0) {\n            throw new Error(`Couldn't parse gameopt: unexpected players data length ${parts.length}`);\n        }\n        const players: HumanPlayerInfo[] = [];\n        const playerCount = Math.floor(parts.length / 8);\n        for (let i = 0; i < playerCount; i++) {\n            const player: HumanPlayerInfo = {\n                name: parts[i * 8],\n                countryId: Number(parts[i * 8 + 1]),\n                colorId: Number(parts[i * 8 + 2]),\n                startPos: Number(parts[i * 8 + 3]),\n                teamId: Number(parts[i * 8 + 4])\n            };\n            players.push(player);\n        }\n        return players;\n    }\n    parseAiOpts(aiString?: string): (AiPlayerInfo | undefined)[] {\n        const aiPlayers: (AiPlayerInfo | undefined)[] = [];\n        if (!aiString) {\n            return aiPlayers;\n        }\n        const parts = aiString.split(',');\n        if (parts.length % 5 !== 0) {\n            throw new Error(`Couldn't parse gameopt: unexpected ai data length ${parts.length}`);\n        }\n        const aiCount = Math.floor(parts.length / 5);\n        for (let i = 0; i < aiCount; i++) {\n            const aiPlayer: AiPlayerInfo = {\n                difficulty: Number(parts[i * 5]),\n                countryId: Number(parts[i * 5 + 1]),\n                colorId: Number(parts[i * 5 + 2]),\n                startPos: Number(parts[i * 5 + 3]),\n                teamId: Number(parts[i * 5 + 4])\n            };\n            aiPlayers.push(aiPlayer.countryId !== -1 ? aiPlayer : undefined);\n        }\n        return aiPlayers;\n    }\n    parseTopic(topicString: string): any {\n        const parts = topicString.split(',');\n        if (parts.length < 6) {\n            return undefined;\n        }\n        const gameId = parts[0];\n        const modHash = Number(parts[1]);\n        const maxPlayers = gameId.charAt(2);\n        const aiPlayers = parts[2];\n        const observers = parts[3];\n        const observable = parts[4];\n        const mapName = new FileNameEncoder().decode(parts[5]);\n        return {\n            description: parts[6] ? binaryStringToUtf16(Base64.decode(parts[6])) : '',\n            modHash: modHash,\n            modName: parts[7] ? binaryStringToUtf16(Base64.decode(parts[7])) : undefined,\n            aiPlayers: Number(aiPlayers),\n            maxPlayers: Number(maxPlayers),\n            observers: Number(observers),\n            observable: Boolean(Number(observable)),\n            mapName: mapName\n        };\n    }\n    parsePingData(pingString: string): PingInfo[] {\n        const parts = pingString.split(',').slice(1);\n        if (parts.length % 2 !== 0) {\n            throw new Error(`Couldn't parse gameopt: unexpected ping data length ${parts.length}`);\n        }\n        const pings: PingInfo[] = [];\n        const pairCount = Math.floor(parts.length / 2);\n        for (let i = 0; i < pairCount; i++) {\n            pings.push({\n                playerName: parts[i * 2],\n                ping: Number(parts[i * 2 + 1])\n            });\n        }\n        return pings;\n    }\n    parseSlotData(slotString: string): SlotInfo[] {\n        const slots: SlotInfo[] = [];\n        const slotParts = slotString.slice(1, -1).split(',');\n        for (const slotPart of slotParts) {\n            const slot: SlotInfo = { type: SlotType.Closed };\n            if (slotPart === '@Closed@') {\n                slot.type = SlotType.Closed;\n            }\n            else if (slotPart === '@Open@') {\n                slot.type = SlotType.Open;\n            }\n            else if (slotPart === '@OpenObserver@') {\n                slot.type = SlotType.OpenObserver;\n            }\n            else if (slotPart === '@EasyAI@') {\n                slot.type = SlotType.Ai;\n                slot.difficulty = AiDifficulty.Easy;\n            }\n            else {\n                slot.type = SlotType.Player;\n                slot.name = slotPart;\n            }\n            slots.push(slot);\n        }\n        return slots;\n    }\n    parsePlayerActions(data: Uint8Array): Array<{\n        id: number;\n        params: Uint8Array;\n    }> {\n        const stream = new DataStream(data);\n        const actionCount = stream.readUint8();\n        const actions: Array<{\n            id: number;\n            params: Uint8Array;\n        }> = [];\n        for (let i = 0; i < actionCount; i++) {\n            const id = stream.readUint8();\n            const paramLength = stream.readUint16();\n            const params = paramLength > 0 ? stream.readUint8Array(paramLength) : new Uint8Array();\n            actions.push({ id, params });\n        }\n        return actions;\n    }\n    parseAllPlayerActions(stream: DataStream): Map<number, Array<{\n        id: number;\n        params: Uint8Array;\n    }>> {\n        const playerCount = stream.readUint8();\n        const allActions = new Map<number, Array<{\n            id: number;\n            params: Uint8Array;\n        }>>();\n        for (let i = 0; i < playerCount; i++) {\n            const playerId = stream.readUint8();\n            const dataLength = stream.readUint16();\n            const data = dataLength > 0 ? stream.readUint8Array(dataLength) : new Uint8Array();\n            const actions = this.parsePlayerActions(data);\n            allActions.set(playerId, actions);\n        }\n        return allActions;\n    }\n    parseMapData(data: Uint8Array): string {\n        return uint8ArrayToBinaryString(data);\n    }\n}\n"
  },
  {
    "path": "src/network/gameopt/Serializer.ts",
    "content": "import { DataStream } from '@/data/DataStream';\nimport { SlotType, SlotInfo, PingInfo } from './SlotInfo';\nimport { GameOpts, AiDifficulty, HumanPlayerInfo, AiPlayerInfo } from '@/game/gameopts/GameOpts';\nimport { MapNameLegacyEncoder } from './MapNameLegacyEncoder';\nimport { FileNameEncoder } from './FileNameEncoder';\nimport { Base64 } from '@/util/Base64';\nimport { utf16ToBinaryString, binaryStringToUint8Array } from '@/util/string';\nexport class Serializer {\n    static readonly MAX_ACTION_PAYLOAD_SIZE = 65536;\n    serializeOptions(gameOpts: GameOpts, useLegacyMapName = false): string {\n        const gameMode = gameOpts.gameMode;\n        const mapTitle = useLegacyMapName\n            ? new MapNameLegacyEncoder().encode(gameOpts.mapTitle)\n            : Base64.encode(utf16ToBinaryString(gameOpts.mapTitle));\n        const mapName = new FileNameEncoder().encode(gameOpts.mapName);\n        const optionsParts = [\n            '0',\n            '0',\n            6 - gameOpts.gameSpeed,\n            gameOpts.credits,\n            gameOpts.unitCount,\n            Number(gameOpts.shortGame),\n            Number(gameOpts.superWeapons),\n            Number(gameOpts.buildOffAlly),\n            Number(gameOpts.mcvRepacks),\n            Number(gameOpts.cratesAppear),\n            gameMode,\n            Number(gameOpts.hostTeams ?? false),\n            mapTitle,\n            gameOpts.maxSlots,\n            Number(gameOpts.mapOfficial),\n            gameOpts.mapSizeBytes,\n            mapName,\n            gameOpts.mapDigest,\n            Number(gameOpts.destroyableBridges),\n            Number(gameOpts.multiEngineer),\n            Number(gameOpts.noDogEngiKills),\n            ...(gameOpts.unknown ? [gameOpts.unknown] : [])\n        ].join(',');\n        const playersPart = gameOpts.humanPlayers\n            .map(player => `${player.name},${player.countryId},${player.colorId},${player.startPos},${player.teamId},0,0,0`)\n            .join(',');\n        const aiPart = this.serializeAiOpts(gameOpts.aiPlayers);\n        return `${optionsParts}:${playersPart}:@:${aiPart},`;\n    }\n    serializeAiOpts(aiPlayers: (AiPlayerInfo | undefined)[]): string {\n        return aiPlayers\n            .map(ai => ai\n            ? `${ai.difficulty},${ai.countryId},${ai.colorId},${ai.startPos},${ai.teamId}`\n            : '0,-1,-1,-1,-1')\n            .join(',');\n    }\n    serializePingData(pings: PingInfo[]): string {\n        return pings.length + ',' + pings.map(ping => `${ping.playerName},${ping.ping}`).join(',');\n    }\n    serializeSlotData(slots: SlotInfo[]): string {\n        const slotStrings = slots.map(slot => {\n            if (slot.type === SlotType.Closed) {\n                return '@Closed@';\n            }\n            if (slot.type === SlotType.Open) {\n                return '@Open@';\n            }\n            if (slot.type === SlotType.OpenObserver) {\n                return '@OpenObserver@';\n            }\n            if (slot.type === SlotType.Ai) {\n                return '@EasyAI@';\n            }\n            else if (slot.type === SlotType.Player) {\n                return slot.name;\n            }\n            throw new Error(`Unexpected slot info with type ${SlotType[slot.type]}`);\n        });\n        return slotStrings.join(',') + ',';\n    }\n    serializeLoadInfo(loadInfo: Array<{\n        name: string;\n        status: number;\n        loadPercent: number;\n        ping: number;\n        lagAllowanceMillis: number;\n    }>): string {\n        return loadInfo\n            .map(info => [\n            info.name,\n            info.status,\n            info.loadPercent,\n            info.ping,\n            info.lagAllowanceMillis\n        ].join(','))\n            .join(',');\n    }\n    serializePlayerActions(actions: Array<{\n        id: number;\n        params: Uint8Array;\n    }>): Uint8Array {\n        const stream = new DataStream();\n        stream.writeUint8(actions.length);\n        for (const { id, params } of actions) {\n            stream.writeUint8(id);\n            stream.writeUint16(params.byteLength);\n            if (params.byteLength > 0) {\n                if (params.byteLength > Serializer.MAX_ACTION_PAYLOAD_SIZE - stream.position) {\n                    console.error(`Action #${id} payload exceeds max data size`, params);\n                    throw new RangeError('Maximum payload data size exceeded');\n                }\n                stream.writeUint8Array(params);\n            }\n        }\n        return stream.toUint8Array();\n    }\n    serializeAllPlayerActions(stream: DataStream, allActions: Map<number, Array<{\n        id: number;\n        params: Uint8Array;\n    }>>): void {\n        stream.writeUint8(allActions.size);\n        for (const [playerId, actions] of allActions) {\n            stream.writeUint8(playerId);\n            const serializedActions = this.serializePlayerActions(actions);\n            stream.writeUint16(serializedActions.byteLength);\n            if (serializedActions.byteLength > 0) {\n                if (serializedActions.byteLength > Serializer.MAX_ACTION_PAYLOAD_SIZE) {\n                    console.error(`Player #${playerId} actions payload exceeds max data size`, actions);\n                    throw new RangeError('Maximum payload data size exceeded');\n                }\n                stream.writeUint8Array(serializedActions);\n            }\n        }\n    }\n    serializeMapData(mapData: string): Uint8Array {\n        return binaryStringToUint8Array(mapData);\n    }\n}\n"
  },
  {
    "path": "src/network/gameopt/SlotInfo.ts",
    "content": "export enum SlotType {\n    Closed = 0,\n    Open = 1,\n    OpenObserver = 2,\n    Player = 3,\n    Ai = 4\n}\nexport interface SlotInfo {\n    type: SlotType;\n    name?: string;\n    difficulty?: number;\n    customBotId?: string;\n}\nexport interface PingInfo {\n    playerName: string;\n    ping: number;\n}\n"
  },
  {
    "path": "src/network/gamestate/PlayerConnectionStatus.ts",
    "content": "export enum PlayerConnectionStatus {\n    Connected = 'Connected',\n    Disconnected = 'Disconnected',\n    Lagging = 'Lagging'\n}\n"
  },
  {
    "path": "src/network/gamestate/Replay.ts",
    "content": "import { DataStream } from '@/data/DataStream';\n\n/** Binary magic bytes: \"RA2R\" */\nconst REPLAY_MAGIC = 0x52324152;\n/** Current replay format version */\nconst REPLAY_FORMAT_VERSION = 1;\n\nexport interface ActionRecord {\n    tick: number;\n    playerId: number;\n    actionType: number;\n    data: Uint8Array;\n}\n\nexport const enum ReplayEventType {\n    Chat = 1,\n    Taunt = 2,\n}\n\nexport interface ReplayEventRecord {\n    tick: number;\n    type: ReplayEventType;\n    playerId: number;\n    payload: string;\n}\n\nexport interface HashCheckpoint {\n    tick: number;\n    hash: number;\n}\n\nexport interface ReplayHeader {\n    gameId: string;\n    gameTimestamp: number;\n    engineVersion: string;\n    modHash: string;\n    gameOptsSerialized: string;\n}\n\nexport class Replay {\n    public static readonly extension = '.ra2replay';\n\n    public name?: string;\n    public timestamp: number = 0;\n    public gameId: string = '';\n    public gameTimestamp: number = 0;\n    public gameOpts: any;\n    public engineVersion: string = '';\n    public modHash: string = '';\n    public finishedTick: number = 0;\n\n    public actionRecords: ActionRecord[] = [];\n    public eventRecords: ReplayEventRecord[] = [];\n    public hashCheckpoints: HashCheckpoint[] = [];\n\n    get endTick(): number {\n        return this.finishedTick;\n    }\n\n    public static sanitizeFileName(filename: string): string {\n        return filename.replace(/[<>:\"/\\\\|?*]/g, '_');\n    }\n\n    finish(currentTick: number): void {\n        this.finishedTick = currentTick;\n    }\n\n    serialize(): string {\n        const ds = new DataStream();\n\n        // Header\n        ds.writeUint32(REPLAY_MAGIC);\n        ds.writeUint16(REPLAY_FORMAT_VERSION);\n        ds.writeUtf8WithLen(this.engineVersion);\n        ds.writeUtf8WithLen(this.modHash);\n        ds.writeUtf8WithLen(this.gameId);\n        ds.writeUint32(this.gameTimestamp);\n        ds.writeUint32(this.finishedTick);\n        ds.writeFloat64(this.timestamp);\n\n        // GameOpts as JSON\n        const gameOptsJson = JSON.stringify(this.toSerializableValue(this.gameOpts));\n        ds.writeUtf8WithLen(gameOptsJson);\n\n        // Replay name\n        ds.writeUtf8WithLen(this.name ?? '');\n\n        // Action records\n        ds.writeUint32(this.actionRecords.length);\n        for (const record of this.actionRecords) {\n            ds.writeUint32(record.tick);\n            ds.writeUint8(record.playerId);\n            ds.writeUint8(record.actionType);\n            ds.writeUint16(record.data.length);\n            ds.writeUint8Array(record.data);\n        }\n\n        // Event records\n        ds.writeUint32(this.eventRecords.length);\n        for (const event of this.eventRecords) {\n            ds.writeUint32(event.tick);\n            ds.writeUint8(event.type);\n            ds.writeUint8(event.playerId);\n            ds.writeUtf8WithLen(event.payload);\n        }\n\n        // Hash checkpoints\n        ds.writeUint32(this.hashCheckpoints.length);\n        for (const cp of this.hashCheckpoints) {\n            ds.writeUint32(cp.tick);\n            ds.writeUint32(cp.hash);\n        }\n\n        // Convert to base64 string for storage\n        const bytes = ds.toUint8Array();\n        return this.uint8ArrayToBase64(bytes);\n    }\n\n    unserialize(data: string, meta?: { name?: string; timestamp?: number }): void {\n        const bytes = this.base64ToUint8Array(data);\n        const ds = new DataStream(bytes.buffer as ArrayBuffer, bytes.byteOffset);\n\n        // Header\n        const magic = ds.readUint32();\n        if (magic !== REPLAY_MAGIC) {\n            throw new Error('Invalid replay file: bad magic');\n        }\n        const version = ds.readUint16();\n        if (version > REPLAY_FORMAT_VERSION) {\n            throw new Error(`Unsupported replay version: ${version}`);\n        }\n\n        this.engineVersion = ds.readUtf8WithLen();\n        this.modHash = ds.readUtf8WithLen();\n        this.gameId = ds.readUtf8WithLen();\n        this.gameTimestamp = ds.readUint32();\n        this.finishedTick = ds.readUint32();\n        this.timestamp = ds.readFloat64();\n\n        // GameOpts\n        const gameOptsJson = ds.readUtf8WithLen();\n        this.gameOpts = JSON.parse(gameOptsJson);\n\n        // Name (from file or embedded)\n        const embeddedName = ds.readUtf8WithLen();\n        this.name = meta?.name ?? embeddedName;\n        if (meta?.timestamp !== undefined) {\n            this.timestamp = meta.timestamp;\n        }\n\n        // Action records\n        const actionCount = ds.readUint32();\n        this.actionRecords = [];\n        for (let i = 0; i < actionCount; i++) {\n            const tick = ds.readUint32();\n            const playerId = ds.readUint8();\n            const actionType = ds.readUint8();\n            const dataLength = ds.readUint16();\n            const actionData = ds.readUint8Array(dataLength);\n            this.actionRecords.push({ tick, playerId, actionType, data: actionData });\n        }\n\n        // Event records\n        const eventCount = ds.readUint32();\n        this.eventRecords = [];\n        for (let i = 0; i < eventCount; i++) {\n            const tick = ds.readUint32();\n            const type = ds.readUint8() as ReplayEventType;\n            const playerId = ds.readUint8();\n            const payload = ds.readUtf8WithLen();\n            this.eventRecords.push({ tick, type, playerId, payload });\n        }\n\n        // Hash checkpoints\n        const cpCount = ds.readUint32();\n        this.hashCheckpoints = [];\n        for (let i = 0; i < cpCount; i++) {\n            const tick = ds.readUint32();\n            const hash = ds.readUint32();\n            this.hashCheckpoints.push({ tick, hash });\n        }\n    }\n\n    async parseHeader(data: string | Blob): Promise<ReplayHeader> {\n        const serialized = typeof data === 'string'\n            ? data\n            : await data.text();\n        const bytes = this.base64ToUint8Array(serialized);\n        const ds = new DataStream(bytes.buffer as ArrayBuffer, bytes.byteOffset);\n\n        const magic = ds.readUint32();\n        if (magic !== REPLAY_MAGIC) {\n            throw new Error('Invalid replay file: bad magic');\n        }\n        const version = ds.readUint16();\n        if (version > REPLAY_FORMAT_VERSION) {\n            throw new Error(`Unsupported replay version: ${version}`);\n        }\n\n        const engineVersion = ds.readUtf8WithLen();\n        const modHash = ds.readUtf8WithLen();\n        const gameId = ds.readUtf8WithLen();\n        const gameTimestamp = ds.readUint32();\n        ds.readUint32();\n        ds.readFloat64();\n        const gameOptsSerialized = ds.readUtf8WithLen();\n\n        return {\n            gameId,\n            gameTimestamp,\n            engineVersion,\n            modHash,\n            gameOptsSerialized,\n        };\n    }\n\n    private uint8ArrayToBase64(bytes: Uint8Array): string {\n        let binary = '';\n        for (let i = 0; i < bytes.length; i++) {\n            binary += String.fromCharCode(bytes[i]);\n        }\n        return btoa(binary);\n    }\n\n    private toSerializableValue(value: any, seen: WeakSet<object> = new WeakSet()): any {\n        if (value === null || value === undefined) {\n            return value;\n        }\n        if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n            return value;\n        }\n        if (Array.isArray(value)) {\n            return value.map((item) => this.toSerializableValue(item, seen));\n        }\n        if (typeof value !== 'object') {\n            return undefined;\n        }\n        if (value instanceof Uint8Array) {\n            return Array.from(value);\n        }\n        if (seen.has(value)) {\n            return undefined;\n        }\n        seen.add(value);\n        const result: Record<string, any> = {};\n        for (const [key, entry] of Object.entries(value)) {\n            const serialized = this.toSerializableValue(entry, seen);\n            if (serialized !== undefined) {\n                result[key] = serialized;\n            }\n        }\n        seen.delete(value);\n        return result;\n    }\n\n    private base64ToUint8Array(base64: string): Uint8Array {\n        const binary = atob(base64);\n        const bytes = new Uint8Array(binary.length);\n        for (let i = 0; i < binary.length; i++) {\n            bytes[i] = binary.charCodeAt(i);\n        }\n        return bytes;\n    }\n}\n"
  },
  {
    "path": "src/network/gamestate/ReplayRecorder.ts",
    "content": "import { Replay, ReplayEventType } from './Replay';\n\nexport class ReplayRecorder {\n    private checkpointInterval = 300; // ~20 seconds at 15 ticks/sec\n\n    constructor(\n        private readonly game: any,\n        private readonly replay: Replay,\n    ) {}\n\n    recordActions(tick: number, actions: any[]): void {\n        for (const action of actions) {\n            let serialized: Uint8Array | undefined;\n            try {\n                serialized = action.serialize?.();\n            }\n            catch (error) {\n                console.warn('[ReplayRecorder] Failed to serialize action for replay recording', {\n                    tick,\n                    actionType: action?.actionType,\n                    action,\n                    error,\n                });\n                continue;\n            }\n            if (!serialized || serialized.length === 0) {\n                continue;\n            }\n            this.replay.actionRecords.push({\n                tick,\n                playerId: action.player?.index ?? 0,\n                actionType: action.actionType,\n                data: serialized,\n            });\n        }\n\n        // Periodic hash checkpoints\n        if (tick > 0 && tick % this.checkpointInterval === 0 &&\n            (this.replay.hashCheckpoints.length === 0 ||\n             this.replay.hashCheckpoints[this.replay.hashCheckpoints.length - 1].tick !== tick)) {\n            this.replay.hashCheckpoints.push({\n                tick,\n                hash: this.game.getHash(),\n            });\n        }\n    }\n\n    recordChatMessage(tick: number, playerName: string, message: string): void {\n        const playerId = this.resolvePlayerId(playerName);\n        this.replay.eventRecords.push({\n            tick,\n            type: ReplayEventType.Chat,\n            playerId,\n            payload: message,\n        });\n    }\n\n    recordTaunt(tick: number, playerName: string, tauntNo: number): void {\n        const playerId = this.resolvePlayerId(playerName);\n        this.replay.eventRecords.push({\n            tick,\n            type: ReplayEventType.Taunt,\n            playerId,\n            payload: String(tauntNo),\n        });\n    }\n\n    private resolvePlayerId(playerName: string): number {\n        try {\n            const player = this.game.getPlayerByName(playerName);\n            return player?.index ?? 0;\n        } catch {\n            return 0;\n        }\n    }\n}\n"
  },
  {
    "path": "src/network/gamestate/ReplayTurnManager.ts",
    "content": "import { ActionRecord, ReplayEventRecord, ReplayEventType, HashCheckpoint } from './Replay';\nimport { ChatMessageReplayEvent } from './replay/ChatMessageReplayEvent';\nimport { TauntReplayEvent } from './replay/TauntReplayEvent';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { EventDispatcher } from '@/util/event';\n\nexport class ReplayTurnManager {\n    private gameTurnMillis = 1000 / GameSpeed.BASE_TICKS_PER_SECOND;\n    private errorState = false;\n    private gameSpeedChanged = false;\n    private finished = false;\n\n    private actionsByTick: Map<number, ActionRecord[]>;\n    private eventsByTick: Map<number, ReplayEventRecord[]>;\n    private hashByTick: Map<number, number>;\n\n    public readonly onReplayEvent = new EventDispatcher<this, any>();\n    public readonly onActionsSent = new EventDispatcher<this, void>();\n    public readonly onFinished = new EventDispatcher<this, void>();\n\n    private readonly onGameSpeedChanged = () => {\n        this.gameSpeedChanged = true;\n    };\n\n    constructor(\n        private readonly game: any,\n        private readonly replay: any,\n        private readonly actionFactory: any,\n        private readonly actionLogger?: { debug(message: string): void },\n    ) {\n        this.actionsByTick = this.buildTickMap<ActionRecord>(replay.actionRecords ?? [], r => r.tick);\n        this.eventsByTick = this.buildTickMap<ReplayEventRecord>(replay.eventRecords ?? [], r => r.tick);\n        this.hashByTick = new Map<number, number>();\n        for (const cp of (replay.hashCheckpoints ?? []) as HashCheckpoint[]) {\n            this.hashByTick.set(cp.tick, cp.hash);\n        }\n    }\n\n    init(): void {\n        this.game.desiredSpeed.onChange.subscribe(this.onGameSpeedChanged);\n        this.computeGameTurn(this.game.speed.value);\n    }\n\n    private computeGameTurn(speed: number): void {\n        this.gameTurnMillis = 1000 / (speed * GameSpeed.BASE_TICKS_PER_SECOND);\n    }\n\n    setErrorState(): void {\n        this.errorState = true;\n    }\n\n    getErrorState(): boolean {\n        return this.errorState;\n    }\n\n    getTurnMillis(): number {\n        return this.gameTurnMillis;\n    }\n\n    isFinished(): boolean {\n        return this.finished;\n    }\n\n    doGameTurn(_timestamp: number): boolean {\n        if (this.errorState || this.finished) {\n            return false;\n        }\n\n        const tick = this.game.currentTick;\n\n        // Inject actions for this tick\n        const records = this.actionsByTick.get(tick);\n        if (records) {\n            for (const record of records) {\n                try {\n                    const action = this.actionFactory.create(record.actionType);\n                    action.unserialize(record.data);\n                    action.player = this.game.getPlayer(record.playerId);\n                    action.process();\n\n                    const printable = action.print?.();\n                    if (printable) {\n                        this.actionLogger?.debug(`[replay](${action.player?.name})@${tick}: ${printable}`);\n                    }\n                } catch (error) {\n                    console.warn(`[ReplayTurnManager] Failed to replay action at tick ${tick}:`, error);\n                }\n            }\n            this.onActionsSent.dispatch(this);\n        }\n\n        // Advance game state\n        this.game.update();\n\n        // Dispatch replay events for this tick\n        const events = this.eventsByTick.get(tick);\n        if (events) {\n            for (const event of events) {\n                if (event.type === ReplayEventType.Chat) {\n                    this.onReplayEvent.dispatch(this, new ChatMessageReplayEvent({\n                        playerId: event.playerId,\n                        message: event.payload,\n                    }));\n                } else if (event.type === ReplayEventType.Taunt) {\n                    this.onReplayEvent.dispatch(this, new TauntReplayEvent({\n                        playerId: event.playerId,\n                        tauntNo: parseInt(event.payload, 10),\n                    }));\n                }\n            }\n        }\n\n        // Verify hash checkpoint\n        const expectedHash = this.hashByTick.get(tick);\n        if (expectedHash !== undefined) {\n            const actualHash = this.game.getHash();\n            if (actualHash !== expectedHash) {\n                console.warn(`[ReplayTurnManager] Desync detected at tick ${tick}: expected=${expectedHash}, actual=${actualHash}`);\n            }\n        }\n\n        // Handle game speed changes\n        if (this.gameSpeedChanged) {\n            this.game.speed.value = this.game.desiredSpeed.value;\n            this.computeGameTurn(this.game.speed.value);\n            this.gameSpeedChanged = false;\n        }\n\n        // Check if replay has ended\n        if (this.replay.finishedTick && tick >= this.replay.finishedTick) {\n            this.finished = true;\n            this.onFinished.dispatch(this, undefined);\n            return false;\n        }\n\n        return true;\n    }\n\n    dispose(): void {\n        this.game.desiredSpeed.onChange.unsubscribe(this.onGameSpeedChanged);\n    }\n\n    private buildTickMap<T>(records: T[], getKey: (r: T) => number): Map<number, T[]> {\n        const map = new Map<number, T[]>();\n        for (const record of records) {\n            const key = getKey(record);\n            let list = map.get(key);\n            if (!list) {\n                list = [];\n                map.set(key, list);\n            }\n            list.push(record);\n        }\n        return map;\n    }\n}\n"
  },
  {
    "path": "src/network/gamestate/SoloPlayTurnManager.ts",
    "content": "import { NoAction } from '@/game/action/NoAction';\nimport { GameStatus } from '@/game/Game';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { EventDispatcher } from '@/util/event';\n\nexport class SoloPlayTurnManager {\n    private gameTurnMillis = 1000 / GameSpeed.BASE_TICKS_PER_SECOND;\n    private errorState = false;\n    private gameSpeedChanged = false;\n    public readonly onActionsSent = new EventDispatcher<this, void>();\n\n    private readonly onGameSpeedChanged = () => {\n        this.gameSpeedChanged = true;\n    };\n\n    constructor(\n        private readonly game: any,\n        private readonly currentPlayer: any,\n        private readonly inputActions: { dequeueAll(): any[] },\n        private readonly actionLogger?: { debug(message: string): void },\n        private readonly replayRecorder?: { recordActions?(tick: number, actions: any[]): void }\n    ) { }\n\n    init(): void {\n        this.game.desiredSpeed.onChange.subscribe(this.onGameSpeedChanged);\n        this.computeGameTurn(this.game.speed.value);\n    }\n\n    private computeGameTurn(speed: number): void {\n        this.gameTurnMillis = 1000 / (speed * GameSpeed.BASE_TICKS_PER_SECOND);\n    }\n\n    setErrorState(): void {\n        this.errorState = true;\n    }\n\n    getErrorState(): boolean {\n        return this.errorState;\n    }\n\n    getTurnMillis(): number {\n        return this.gameTurnMillis;\n    }\n\n    doGameTurn(_timestamp: number): boolean {\n        if (this.errorState) {\n            return false;\n        }\n\n        if (this.game.status !== GameStatus.Ended) {\n            let actions = this.inputActions.dequeueAll();\n            if (actions.length) {\n                this.replayRecorder?.recordActions?.(this.game.currentTick, actions);\n                this.onActionsSent.dispatch(this);\n            } else {\n                actions = [new NoAction()];\n            }\n            this.processActions(actions);\n        }\n\n        this.game.update();\n\n        if (this.gameSpeedChanged) {\n            this.game.speed.value = this.game.desiredSpeed.value;\n            this.computeGameTurn(this.game.speed.value);\n            this.gameSpeedChanged = false;\n        }\n\n        return true;\n    }\n\n    private processActions(actions: any[]): void {\n        actions.forEach((action) => {\n            action.player = this.currentPlayer;\n            action.process();\n            const printable = action.print?.();\n            if (printable) {\n                this.actionLogger?.debug(`(${action.player.name})@${this.game.currentTick}: ${printable}`);\n            }\n        });\n    }\n\n    dispose(): void {\n        this.game.desiredSpeed.onChange.unsubscribe(this.onGameSpeedChanged);\n    }\n}\n"
  },
  {
    "path": "src/network/gamestate/replay/ChatMessageReplayEvent.ts",
    "content": "export class ChatMessageReplayEvent {\n    constructor(public readonly payload: { playerId: number; message: string }) {}\n}\n"
  },
  {
    "path": "src/network/gamestate/replay/TauntReplayEvent.ts",
    "content": "export class TauntReplayEvent {\n    constructor(public readonly payload: { playerId: number; tauntNo: number }) {}\n}\n"
  },
  {
    "path": "src/network/gservConfig.ts",
    "content": "export const RECIPIENT_ALL = \"#all\";\nexport const RECIPIENT_TEAM = \"#team\";\n"
  },
  {
    "path": "src/network/ladder/LadderHead.ts",
    "content": "export class LadderHead {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/network/ladder/PagedResponse.ts",
    "content": "export class PagedResponse {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/network/ladder/PlayerLadderRung.ts",
    "content": "export class PlayerLadderRung {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/network/ladder/PlayerProfile.ts",
    "content": "export class PlayerProfile {\n    constructor() {\n    }\n}\n"
  },
  {
    "path": "src/network/ladder/PlayerRankType.ts",
    "content": "export enum PlayerRankType {\n    None = 0,\n    Private = 1,\n    Corporal = 2,\n    Sergeant = 3,\n    Lieutenant = 4,\n    Major = 5,\n    Colonel = 6,\n    BrigGeneral = 7,\n    General = 8,\n    FiveStarGeneral = 9,\n    CommanderInChief = 10\n}\n"
  },
  {
    "path": "src/network/ladder/WLadderService.ts",
    "content": "import { LadderType, CURRENT_SEASON, PREV_SEASON } from './wladderConfig';\nimport { HttpRequest } from '../HttpRequest';\nexport class WLadderService {\n    private url: string;\n    private wolConfig: any;\n    static CURRENT_SEASON = CURRENT_SEASON;\n    static PREV_SEASON = PREV_SEASON;\n    constructor(wolConfig: any) {\n        this.wolConfig = wolConfig;\n    }\n    setUrl(url: string): void {\n        this.url = url;\n    }\n    getUrl(): string {\n        return this.url;\n    }\n    async getSeasons(options: any): Promise<any> {\n        if (!this.url)\n            throw new Error(\"No ladder URL is set\");\n        const sku = this.wolConfig.getClientSku();\n        return await new HttpRequest().fetchJson(this.url + \"/\" + sku, options);\n    }\n    async getSeason(season: string, locale: string, options: any): Promise<any> {\n        if (!this.url)\n            throw new Error(\"No ladder URL is set\");\n        const sku = this.wolConfig.getClientSku();\n        return await new HttpRequest().fetchJson(this.url + `/${sku}/${season}?locale=${locale}`, options);\n    }\n    async listSearch(players: any[], options: any, ladderType: LadderType = LadderType.Solo1v1, season: string = CURRENT_SEASON, locale?: string): Promise<any> {\n        if (!this.url)\n            throw new Error(\"No ladder URL is set\");\n        const sku = this.wolConfig.getClientSku();\n        return await new HttpRequest().fetchJson(this.url + `/${sku}/${ladderType}/${season}/listsearch`, options, {\n            method: \"POST\",\n            body: JSON.stringify({ players, locale })\n        });\n    }\n    async rungSearch(start: number, count: number, ladderType: LadderType, season: string, ladderId: string, options: any): Promise<any> {\n        if (!this.url)\n            throw new Error(\"No ladder URL is set\");\n        const sku = this.wolConfig.getClientSku();\n        return await new HttpRequest().fetchJson(this.url + `/${sku}/${ladderType}/${season}/rungsearch`, options, {\n            method: \"POST\",\n            body: JSON.stringify({ ladderId, start, count })\n        });\n    }\n}\n"
  },
  {
    "path": "src/network/ladder/wladderConfig.ts",
    "content": "export enum LadderType {\n    Solo1v1 = \"1v1\",\n    Random2v2 = \"2v2-random\"\n}\nexport enum LadderQueueType {\n    Solo1v1 = \"1v1\",\n    Team2v2 = \"2v2\"\n}\nexport const CURRENT_SEASON = \"current\";\nexport const PREV_SEASON = \"prev\";\nexport const MAX_LIST_SEARCH_COUNT = 50;\nexport const teamSizes = new Map([\n    [LadderQueueType.Solo1v1, 1],\n    [LadderQueueType.Team2v2, 2]\n]);\nexport function getLadderTypeForQueueType(queueType: LadderQueueType): LadderType {\n    switch (queueType) {\n        case LadderQueueType.Solo1v1:\n            return LadderType.Solo1v1;\n        case LadderQueueType.Team2v2:\n            return LadderType.Random2v2;\n        default:\n            throw new Error(`Unhandled queue type \"${queueType}\"`);\n    }\n}\n"
  },
  {
    "path": "src/network/lan/LanLockstepTurnManager.ts",
    "content": "import { GameStatus } from '@/game/Game';\nimport { GameSpeed } from '@/game/GameSpeed';\nimport { ActionType } from '@/game/action/ActionType';\nimport { NoAction } from '@/game/action/NoAction';\nimport { Parser } from '@/network/gameopt/Parser';\nimport { Serializer } from '@/network/gameopt/Serializer';\nimport { LanMatchSession, LanResolvedTurn } from '@/network/lan/LanMatchSession';\nimport { EventDispatcher } from '@/util/event';\n\nexport class LanLockstepTurnManager {\n    private readonly serializer = new Serializer();\n    private readonly parser = new Parser();\n    private readonly submittedTicks = new Set<number>();\n    private gameTurnMillis = 1000 / GameSpeed.BASE_TICKS_PER_SECOND;\n    private errorState = false;\n    private passiveMode = false;\n    private lagState = false;\n    private matchDisposed = false;\n\n    public readonly onActionsSent = new EventDispatcher<this, string>();\n    public readonly onActionsReceived = new EventDispatcher<this, string>();\n    public readonly onLagStateChange = new EventDispatcher<this, boolean>();\n\n    constructor(\n        private readonly game: any,\n        private readonly localPlayer: any,\n        private readonly inputActions: { dequeueAll(): any[] },\n        private readonly actionFactory: any,\n        private readonly matchSession: LanMatchSession,\n        private readonly actionLogger?: { debug(message: string): void },\n        private readonly lockstepLogger?: { debug?(message: string): void; warn?(message: string): void },\n        private readonly replayRecorder?: { recordActions?(tick: number, actions: any[]): void }\n    ) { }\n\n    init(): void {\n        this.computeGameTurn(this.game.speed.value);\n        this.matchSession.onActionsReceived.subscribe(this.handleActionsReceived);\n    }\n\n    getTurnMillis(): number {\n        return this.gameTurnMillis;\n    }\n\n    setPassiveMode(passive: boolean): void {\n        this.passiveMode = passive;\n    }\n\n    setErrorState(): void {\n        this.errorState = true;\n    }\n\n    getErrorState(): boolean {\n        return this.errorState;\n    }\n\n    doGameTurn(_timestamp: number): boolean {\n        if (this.errorState) {\n            return false;\n        }\n\n        const tick = this.game.currentTick;\n        if (this.game.status !== GameStatus.Ended) {\n            const localTurnId = this.submitLocalTurn(tick);\n            if (localTurnId) {\n                this.onActionsSent.dispatch(this, localTurnId);\n            }\n\n            const resolvedTurn = this.matchSession.tryConsumeTurn(tick);\n            if (!resolvedTurn) {\n                this.updateLagState(true, tick);\n                return false;\n            }\n\n            this.updateLagState(false, tick);\n            const processedActions = this.processResolvedTurn(tick, resolvedTurn);\n            if (processedActions.length) {\n                this.replayRecorder?.recordActions?.(tick, processedActions);\n            }\n            if (tick > 0 && tick % 300 === 0) {\n                this.lockstepLogger?.debug?.(`[lan] tick=${tick} hash=${this.game.getHash()} peers=${resolvedTurn.batches.map((batch) => batch.peerId).join(',')}`);\n            }\n        }\n\n        this.game.update();\n        return true;\n    }\n\n    dispose(): void {\n        this.matchSession.onActionsReceived.unsubscribe(this.handleActionsReceived);\n        if (!this.matchDisposed) {\n            this.matchSession.dispose();\n            this.matchDisposed = true;\n        }\n    }\n\n    private readonly handleActionsReceived = (turnId: string) => {\n        this.onActionsReceived.dispatch(this, turnId);\n    };\n\n    private computeGameTurn(speed: number): void {\n        this.gameTurnMillis = 1000 / (speed * GameSpeed.BASE_TICKS_PER_SECOND);\n    }\n\n    private submitLocalTurn(tick: number): string | undefined {\n        if (this.submittedTicks.has(tick)) {\n            return undefined;\n        }\n        let actions = this.inputActions.dequeueAll();\n        if (!actions.length) {\n            actions = [new NoAction()];\n        }\n\n        const actionData = this.serializer.serializePlayerActions(actions.map((action: any) => ({\n            id: action.actionType,\n            params: action.serialize?.() ?? new Uint8Array(),\n        })));\n\n        this.submittedTicks.add(tick);\n        return this.matchSession.submitLocalTurn(tick, actionData);\n    }\n\n    private processResolvedTurn(tick: number, resolvedTurn: LanResolvedTurn): any[] {\n        const processedActions: any[] = [];\n\n        resolvedTurn.batches.forEach((batch) => {\n            const assignment = this.matchSession.getHumanAssignment(batch.peerId);\n            if (!assignment) {\n                return;\n            }\n\n            const player = this.game.getPlayerByName(assignment.name);\n            const actionRecords = this.parser.parsePlayerActions(batch.actionData);\n            actionRecords.forEach((record) => {\n                const action = this.actionFactory.create(record.id);\n                action.unserialize?.(record.params);\n                action.player = player;\n                action.process();\n                processedActions.push(action);\n\n                const printable = action.print?.();\n                if (printable) {\n                    this.actionLogger?.debug(`(${action.player.name})@${tick}: ${printable}`);\n                }\n            });\n        });\n\n        resolvedTurn.dropPeerIds.forEach((peerId) => {\n            const assignment = this.matchSession.getHumanAssignment(peerId);\n            if (!assignment) {\n                return;\n            }\n\n            const player = this.game.getPlayerByName(assignment.name);\n            const action = this.actionFactory.create(ActionType.DropPlayer);\n            action.unserialize?.(new Uint8Array());\n            action.player = player;\n            action.process();\n            processedActions.push(action);\n            this.actionLogger?.debug(`(${player.name})@${tick}: [drop] peer disconnected`);\n        });\n\n        return processedActions;\n    }\n\n    private updateLagState(nextLagState: boolean, tick: number): void {\n        if (this.lagState === nextLagState) {\n            return;\n        }\n        this.lagState = nextLagState;\n        this.onLagStateChange.dispatch(this, nextLagState);\n        if (nextLagState) {\n            this.lockstepLogger?.warn?.(`[lan] waiting for turn ${tick}${this.passiveMode ? ' (passive)' : ''}`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/network/lan/LanMatchSession.ts",
    "content": "import { LanMeshAppMessage, LanMeshSession, LanMeshSnapshot } from '@/network/lan/LanMeshSession';\nimport { LanHumanAssignment, LanLaunchDescriptor } from '@/network/lan/LanRoomSession';\nimport { EventDispatcher } from '@/util/event';\nimport { base64StringToUint8Array, uint8ArrayToBase64String } from '@/util/string';\n\ninterface LanMatchPeerIdentity {\n    id: string;\n    name: string;\n}\n\ninterface LanMatchSnapshotMember {\n    id: string;\n    isSelf: boolean;\n    status: 'self' | 'known' | 'connected' | 'connecting';\n}\n\ninterface LanMatchTransportSnapshot {\n    members: LanMatchSnapshotMember[];\n}\n\ninterface LanMatchTransportMessage {\n    from: LanMatchPeerIdentity;\n    payload: unknown;\n    timestamp: number;\n}\n\nexport interface LanMatchTransport {\n    getSelf(): LanMatchPeerIdentity;\n    getSnapshot(): LanMatchTransportSnapshot;\n    broadcastAppMessage(payload: unknown, excludedPeerId?: string): void;\n    leaveRoom?(): void;\n    onSnapshotChange: {\n        subscribe(listener: (snapshot: LanMatchTransportSnapshot, source: LanMeshSession) => void): void;\n        unsubscribe(listener: (snapshot: LanMatchTransportSnapshot, source: LanMeshSession) => void): void;\n    };\n    onAppMessage: {\n        subscribe(listener: (entry: LanMatchTransportMessage, source: LanMeshSession) => void): void;\n        unsubscribe(listener: (entry: LanMatchTransportMessage, source: LanMeshSession) => void): void;\n    };\n}\n\ninterface LanGameTurnMessage {\n    type: 'lan-game-turn';\n    gameId: string;\n    tick: number;\n    fromPeerId: string;\n    turnId: string;\n    actionData: string;\n    dropPeerIds: string[];\n}\n\ninterface LanGameLoadProgressMessage {\n    type: 'lan-game-load-progress';\n    gameId: string;\n    fromPeerId: string;\n    loadPercent: number;\n}\n\nexport interface LanMatchTurnBatch {\n    tick: number;\n    peerId: string;\n    turnId: string;\n    actionData: Uint8Array;\n    dropPeerIds: string[];\n    receivedAt: number;\n}\n\nexport interface LanResolvedTurn {\n    tick: number;\n    controlPeerId: string;\n    dropPeerIds: string[];\n    batches: LanMatchTurnBatch[];\n}\n\nexport interface LanMatchSnapshotState {\n    gameId: string;\n    localPeerId: string;\n    controlPeerId: string;\n    activePeerIds: string[];\n    suspectedDropPeerIds: string[];\n    bufferedTicks: number[];\n    batchPeerIdsByTick: Record<number, string[]>;\n    pendingLocalTicks: number[];\n    allPeersLoaded: boolean;\n    loadPercentByPeerId: Record<string, number>;\n    transportMembers: LanMatchSnapshotMember[];\n}\n\nfunction sortAssignments(assignments: LanHumanAssignment[]): LanHumanAssignment[] {\n    return assignments\n        .slice()\n        .sort((left, right) => left.slotIndex - right.slotIndex || left.name.localeCompare(right.name, 'zh-Hans-CN'));\n}\n\nfunction cloneBatch(batch: LanMatchTurnBatch): LanMatchTurnBatch {\n    return {\n        tick: batch.tick,\n        peerId: batch.peerId,\n        turnId: batch.turnId,\n        actionData: new Uint8Array(batch.actionData),\n        dropPeerIds: [...batch.dropPeerIds],\n        receivedAt: batch.receivedAt,\n    };\n}\n\nfunction cloneLaunchDescriptor(descriptor: LanLaunchDescriptor): LanLaunchDescriptor {\n    return {\n        ...descriptor,\n        humanAssignments: descriptor.humanAssignments.map((assignment) => ({ ...assignment })),\n        mapTransferStateByPeerId: Object.fromEntries(\n            Object.entries(descriptor.mapTransferStateByPeerId).map(([peerId, transferState]) => [peerId, { ...transferState }])\n        ),\n    };\n}\n\nfunction arePeerListsEqual(left: string[], right: string[]): boolean {\n    if (left.length !== right.length) {\n        return false;\n    }\n    return left.every((peerId, index) => peerId === right[index]);\n}\n\nfunction logLanMatch(event: string, details: Record<string, unknown>): void {\n    console.log(`[lan-match] ${event}`, details);\n}\n\nexport class LanMatchSession {\n    private readonly descriptor: LanLaunchDescriptor;\n    private readonly orderedAssignments: LanHumanAssignment[];\n    private readonly assignmentByPeerId = new Map<string, LanHumanAssignment>();\n    private readonly activePeerIds = new Set<string>();\n    private readonly suspectedDropPeerIds = new Set<string>();\n    private readonly turnBatchesByTick = new Map<number, Map<string, LanMatchTurnBatch>>();\n    private readonly localTurnIdByTick = new Map<number, string>();\n    private readonly loadPercentByPeerId = new Map<string, number>();\n\n    private lastSnapshot: LanMatchTransportSnapshot;\n    private localTurnCounter = 0;\n    private disposed = false;\n    private roomLeft = false;\n\n    public readonly onSnapshotChange = new EventDispatcher<this, LanMatchSnapshotState>();\n    public readonly onActionsReceived = new EventDispatcher<this, string>();\n\n    constructor(\n        private readonly transport: LanMatchTransport,\n        descriptor: LanLaunchDescriptor\n    ) {\n        this.descriptor = cloneLaunchDescriptor(descriptor);\n        this.orderedAssignments = sortAssignments(this.descriptor.humanAssignments);\n        this.orderedAssignments.forEach((assignment) => {\n            this.assignmentByPeerId.set(assignment.peerId, { ...assignment });\n            this.activePeerIds.add(assignment.peerId);\n            this.loadPercentByPeerId.set(assignment.peerId, 0);\n        });\n        this.lastSnapshot = this.transport.getSnapshot();\n        this.handleSnapshotChange = this.handleSnapshotChange.bind(this);\n        this.handleAppMessage = this.handleAppMessage.bind(this);\n        this.transport.onSnapshotChange.subscribe(this.handleSnapshotChange);\n        this.transport.onAppMessage.subscribe(this.handleAppMessage);\n        this.handleSnapshotChange(this.lastSnapshot, this.transport as LanMeshSession);\n    }\n\n    dispose(): void {\n        if (this.disposed) {\n            return;\n        }\n        this.disposed = true;\n        this.transport.onSnapshotChange.unsubscribe(this.handleSnapshotChange);\n        this.transport.onAppMessage.unsubscribe(this.handleAppMessage);\n    }\n\n    leaveRoom(): void {\n        if (this.roomLeft) {\n            return;\n        }\n        this.roomLeft = true;\n        this.transport.leaveRoom?.();\n    }\n\n    getLaunchDescriptor(): LanLaunchDescriptor {\n        return cloneLaunchDescriptor(this.descriptor);\n    }\n\n    getHumanAssignment(peerId: string): LanHumanAssignment | undefined {\n        const assignment = this.assignmentByPeerId.get(peerId);\n        return assignment ? { ...assignment } : undefined;\n    }\n\n    getSnapshot(): LanMatchSnapshotState {\n        return this.createSnapshot();\n    }\n\n    reportLoadProgress(percent: number): void {\n        const localPeerId = this.transport.getSelf().id;\n        const nextPercent = Math.max(0, Math.min(100, Math.floor(percent)));\n        const currentPercent = this.loadPercentByPeerId.get(localPeerId) ?? 0;\n        if (nextPercent <= currentPercent) {\n            return;\n        }\n        this.loadPercentByPeerId.set(localPeerId, nextPercent);\n        this.transport.broadcastAppMessage({\n            type: 'lan-game-load-progress',\n            gameId: this.descriptor.gameId,\n            fromPeerId: localPeerId,\n            loadPercent: nextPercent,\n        } satisfies LanGameLoadProgressMessage);\n        this.dispatchSnapshot();\n    }\n\n    areAllPlayersLoaded(): boolean {\n        return this.getOrderedActivePeerIds().every((peerId) => (this.loadPercentByPeerId.get(peerId) ?? 0) >= 100);\n    }\n\n    submitLocalTurn(tick: number, actionData: Uint8Array): string {\n        const existingTurnId = this.localTurnIdByTick.get(tick);\n        if (existingTurnId) {\n            return existingTurnId;\n        }\n\n        const localPeerId = this.transport.getSelf().id;\n        const turnId = `${localPeerId}:${tick}:${++this.localTurnCounter}`;\n        const dropPeerIds = this.getControlPeerId() === localPeerId\n            ? this.getSortedPeerIds(this.suspectedDropPeerIds)\n            : [];\n        const batch: LanMatchTurnBatch = {\n            tick,\n            peerId: localPeerId,\n            turnId,\n            actionData: new Uint8Array(actionData),\n            dropPeerIds,\n            receivedAt: Date.now(),\n        };\n\n        this.localTurnIdByTick.set(tick, turnId);\n        this.storeBatch(batch);\n        logLanMatch('submit-local-turn', {\n            localPeerId,\n            tick,\n            turnId,\n            controlPeerId: this.getControlPeerId(),\n            dropPeerIds,\n            activePeerIds: this.getOrderedActivePeerIds(),\n        });\n        this.transport.broadcastAppMessage({\n            type: 'lan-game-turn',\n            gameId: this.descriptor.gameId,\n            tick,\n            fromPeerId: localPeerId,\n            turnId,\n            actionData: uint8ArrayToBase64String(actionData),\n            dropPeerIds,\n        } satisfies LanGameTurnMessage);\n\n        return turnId;\n    }\n\n    tryConsumeTurn(tick: number): LanResolvedTurn | undefined {\n        const tickBatches = this.turnBatchesByTick.get(tick);\n        if (!tickBatches) {\n            return undefined;\n        }\n\n        const controlPeerId = this.getControlPeerId();\n        const controlBatch = tickBatches.get(controlPeerId);\n        if (!controlBatch) {\n            return undefined;\n        }\n\n        const dropPeerIds = controlBatch.dropPeerIds.filter((peerId) => this.activePeerIds.has(peerId));\n        const expectedPeerIds = this.getOrderedActivePeerIds().filter((peerId) => !dropPeerIds.includes(peerId));\n        if (expectedPeerIds.some((peerId) => !tickBatches.has(peerId))) {\n            return undefined;\n        }\n\n        const resolvedBatches = expectedPeerIds\n            .map((peerId) => tickBatches.get(peerId))\n            .filter((batch): batch is LanMatchTurnBatch => Boolean(batch))\n            .map(cloneBatch);\n\n        this.turnBatchesByTick.delete(tick);\n        this.commitDrops(dropPeerIds);\n        logLanMatch('resolve-turn', {\n            localPeerId: this.transport.getSelf().id,\n            tick,\n            controlPeerId,\n            dropPeerIds,\n            peerIds: resolvedBatches.map((batch) => batch.peerId),\n        });\n\n        const localTurnId = this.localTurnIdByTick.get(tick);\n        if (localTurnId) {\n            this.localTurnIdByTick.delete(tick);\n            this.onActionsReceived.dispatch(this, localTurnId);\n        }\n\n        this.dispatchSnapshot();\n        return {\n            tick,\n            controlPeerId,\n            dropPeerIds: [...dropPeerIds],\n            batches: resolvedBatches,\n        };\n    }\n\n    private handleSnapshotChange(snapshot: LanMatchTransportSnapshot, _source: LanMeshSession): void {\n        this.lastSnapshot = snapshot;\n        const connectedPeerIds = new Set(\n            snapshot.members\n                .filter((member) => member.isSelf || member.status === 'connected')\n                .map((member) => member.id)\n        );\n\n        this.getOrderedActivePeerIds().forEach((peerId) => {\n            if (!connectedPeerIds.has(peerId)) {\n                this.suspectedDropPeerIds.add(peerId);\n            }\n        });\n\n        this.refreshLocalControlTurns();\n        this.dispatchSnapshot();\n    }\n\n    private handleAppMessage(entry: LanMeshAppMessage, _source: LanMeshSession): void {\n        const payload = entry.payload;\n        if (!payload || typeof payload !== 'object') {\n            return;\n        }\n\n        const message = payload as LanGameTurnMessage | LanGameLoadProgressMessage;\n        if (message.gameId !== this.descriptor.gameId) {\n            return;\n        }\n        if (message.fromPeerId !== entry.from.id || !this.assignmentByPeerId.has(message.fromPeerId)) {\n            return;\n        }\n\n        if (message.type === 'lan-game-load-progress') {\n            const currentPercent = this.loadPercentByPeerId.get(message.fromPeerId) ?? 0;\n            if (message.loadPercent > currentPercent) {\n                this.loadPercentByPeerId.set(message.fromPeerId, Math.min(100, Math.floor(message.loadPercent)));\n                this.dispatchSnapshot();\n            }\n            return;\n        }\n\n        if (message.type !== 'lan-game-turn') {\n            return;\n        }\n\n        this.storeBatch({\n            tick: message.tick,\n            peerId: message.fromPeerId,\n            turnId: message.turnId,\n            actionData: base64StringToUint8Array(message.actionData),\n            dropPeerIds: this.getSortedPeerIds(new Set((message.dropPeerIds ?? []).filter((peerId) => this.activePeerIds.has(peerId)))),\n            receivedAt: entry.timestamp,\n        });\n        logLanMatch('receive-turn', {\n            localPeerId: this.transport.getSelf().id,\n            fromPeerId: message.fromPeerId,\n            tick: message.tick,\n            turnId: message.turnId,\n            dropPeerIds: message.dropPeerIds ?? [],\n        });\n    }\n\n    private storeBatch(batch: LanMatchTurnBatch): void {\n        if (!this.activePeerIds.has(batch.peerId)) {\n            return;\n        }\n\n        let tickBatches = this.turnBatchesByTick.get(batch.tick);\n        if (!tickBatches) {\n            tickBatches = new Map<string, LanMatchTurnBatch>();\n            this.turnBatchesByTick.set(batch.tick, tickBatches);\n        }\n        const existingBatch = tickBatches.get(batch.peerId);\n        if (existingBatch) {\n            if (existingBatch.turnId === batch.turnId &&\n                !arePeerListsEqual(existingBatch.dropPeerIds, batch.dropPeerIds)) {\n                tickBatches.set(batch.peerId, cloneBatch(batch));\n                this.dispatchSnapshot();\n            }\n            return;\n        }\n\n        tickBatches.set(batch.peerId, cloneBatch(batch));\n        this.suspectedDropPeerIds.delete(batch.peerId);\n        this.dispatchSnapshot();\n    }\n\n    private refreshLocalControlTurns(): void {\n        const localPeerId = this.transport.getSelf().id;\n        if (this.getControlPeerId() !== localPeerId) {\n            return;\n        }\n\n        const nextDropPeerIds = this.getSortedPeerIds(this.suspectedDropPeerIds);\n        this.localTurnIdByTick.forEach((turnId, tick) => {\n            const tickBatches = this.turnBatchesByTick.get(tick);\n            const localBatch = tickBatches?.get(localPeerId);\n            if (!tickBatches || !localBatch || arePeerListsEqual(localBatch.dropPeerIds, nextDropPeerIds)) {\n                return;\n            }\n\n            const updatedBatch: LanMatchTurnBatch = {\n                ...localBatch,\n                dropPeerIds: [...nextDropPeerIds],\n            };\n            tickBatches.set(localPeerId, updatedBatch);\n            logLanMatch('refresh-control-turn', {\n                localPeerId,\n                tick,\n                turnId,\n                dropPeerIds: updatedBatch.dropPeerIds,\n            });\n            this.transport.broadcastAppMessage({\n                type: 'lan-game-turn',\n                gameId: this.descriptor.gameId,\n                tick,\n                fromPeerId: localPeerId,\n                turnId,\n                actionData: uint8ArrayToBase64String(updatedBatch.actionData),\n                dropPeerIds: updatedBatch.dropPeerIds,\n            } satisfies LanGameTurnMessage);\n        });\n    }\n\n    private commitDrops(dropPeerIds: string[]): void {\n        if (!dropPeerIds.length) {\n            return;\n        }\n\n        dropPeerIds.forEach((peerId) => {\n            this.activePeerIds.delete(peerId);\n            this.suspectedDropPeerIds.delete(peerId);\n        });\n\n        Array.from(this.turnBatchesByTick.entries()).forEach(([tick, tickBatches]) => {\n            dropPeerIds.forEach((peerId) => tickBatches.delete(peerId));\n            if (!tickBatches.size) {\n                this.turnBatchesByTick.delete(tick);\n            }\n        });\n    }\n\n    private getControlPeerId(): string {\n        const orderedActivePeerIds = this.getOrderedActivePeerIds();\n        const availableControlPeers = orderedActivePeerIds.filter((peerId) => !this.suspectedDropPeerIds.has(peerId));\n        return availableControlPeers[0] ?? orderedActivePeerIds[0] ?? this.transport.getSelf().id;\n    }\n\n    private getOrderedActivePeerIds(): string[] {\n        return this.orderedAssignments\n            .map((assignment) => assignment.peerId)\n            .filter((peerId) => this.activePeerIds.has(peerId));\n    }\n\n    private getSortedPeerIds(peerIds: Set<string>): string[] {\n        const orderedPeerIds = this.getOrderedActivePeerIds();\n        return orderedPeerIds.filter((peerId) => peerIds.has(peerId));\n    }\n\n    private createSnapshot(): LanMatchSnapshotState {\n        const batchPeerIdsByTick = Object.fromEntries(\n            Array.from(this.turnBatchesByTick.entries())\n                .sort(([left], [right]) => left - right)\n                .map(([tick, tickBatches]) => [\n                    tick,\n                    Array.from(tickBatches.keys()).sort((left, right) => {\n                        const orderedPeerIds = this.getOrderedActivePeerIds();\n                        return orderedPeerIds.indexOf(left) - orderedPeerIds.indexOf(right);\n                    }),\n                ])\n        );\n        const orderedActivePeerIds = this.getOrderedActivePeerIds();\n        return {\n            gameId: this.descriptor.gameId,\n            localPeerId: this.transport.getSelf().id,\n            controlPeerId: this.getControlPeerId(),\n            activePeerIds: orderedActivePeerIds,\n            suspectedDropPeerIds: this.getSortedPeerIds(this.suspectedDropPeerIds),\n            bufferedTicks: Array.from(this.turnBatchesByTick.keys()).sort((left, right) => left - right),\n            batchPeerIdsByTick,\n            pendingLocalTicks: Array.from(this.localTurnIdByTick.keys()).sort((left, right) => left - right),\n            allPeersLoaded: orderedActivePeerIds.every((peerId) => (this.loadPercentByPeerId.get(peerId) ?? 0) >= 100),\n            loadPercentByPeerId: Object.fromEntries(\n                this.orderedAssignments.map((assignment) => [assignment.peerId, this.loadPercentByPeerId.get(assignment.peerId) ?? 0])\n            ),\n            transportMembers: this.lastSnapshot.members.map((member) => ({ ...member })),\n        };\n    }\n\n    private dispatchSnapshot(): void {\n        this.onSnapshotChange.dispatch(this, this.createSnapshot());\n    }\n}\n"
  },
  {
    "path": "src/network/lan/LanMeshSession.ts",
    "content": "import { EventDispatcher } from '@/util/event';\nimport {\n    decodeLanQrPacket,\n    encodeLanQrPacket,\n    LanInvitePacket,\n    LanJoinResponsePacket,\n    LanPeerIdentity,\n} from '@/network/lan/LanQrPayload';\nimport { formatSdpCandidateSummary, getSdpCandidateWarning, summarizeSdpCandidates } from '@/network/lan/SdpCandidateDiagnostics';\n\ntype ControlEnvelope =\n    | {\n        type: 'hello';\n        roomId: string;\n        self: LanPeerIdentity;\n        members: LanPeerIdentity[];\n    }\n    | {\n        type: 'room-sync';\n        roomId: string;\n        members: LanPeerIdentity[];\n    }\n    | {\n        type: 'member-join';\n        roomId: string;\n        member: LanPeerIdentity;\n    }\n    | {\n        type: 'member-leave';\n        roomId: string;\n        peerId: string;\n        reason: 'left' | 'disconnect';\n    }\n    | {\n        type: 'mesh-connect-request';\n        roomId: string;\n        target: LanPeerIdentity;\n    }\n    | {\n        type: 'relay-signal';\n        roomId: string;\n        source: LanPeerIdentity;\n        targetPeerId: string;\n        signalType: 'offer' | 'answer';\n        description: RTCSessionDescriptionInit;\n    }\n    | {\n        type: 'chat';\n        roomId: string;\n        from: LanPeerIdentity;\n        text: string;\n        timestamp: number;\n    }\n    | {\n        type: 'app-message';\n        roomId: string;\n        from: LanPeerIdentity;\n        payload: unknown;\n    };\n\ntype LinkRole = 'inviter' | 'joiner' | 'mesh-offerer' | 'mesh-answerer';\ntype LinkStatus = 'connecting' | 'connected' | 'closed';\n\ninterface LinkContext {\n    key: string;\n    peer?: LanPeerIdentity;\n    pc: RTCPeerConnection;\n    channel?: RTCDataChannel;\n    role: LinkRole;\n    status: LinkStatus;\n}\n\ninterface PendingInvite {\n    inviteId: string;\n    context: LinkContext;\n}\n\ninterface ActiveQrPayload {\n    kind: 'invite' | 'join-response';\n    text: string;\n    title: string;\n    description: string;\n}\n\nexport interface LanMemberSnapshot extends LanPeerIdentity {\n    isSelf: boolean;\n    isDirect: boolean;\n    status: 'self' | 'known' | 'connected' | 'connecting';\n}\n\nexport interface LanMeshSnapshot {\n    self: LanPeerIdentity;\n    roomId?: string;\n    isInRoom: boolean;\n    roomReady: boolean;\n    directPeerCount: number;\n    members: LanMemberSnapshot[];\n    activeQrPayloadText: string;\n    activeQrPayloadKind?: 'invite' | 'join-response';\n    activeQrPayloadTitle?: string;\n    activeQrPayloadDescription?: string;\n}\n\nexport interface LanMeshLogEntry {\n    level: 'info' | 'warn' | 'error';\n    text: string;\n    timestamp: number;\n}\n\nexport interface LanMeshChatEntry {\n    from: LanPeerIdentity;\n    text: string;\n    timestamp: number;\n}\n\nexport interface LanMeshAppMessage {\n    from: LanPeerIdentity;\n    payload: unknown;\n    timestamp: number;\n}\n\nconst ICE_GATHER_TIMEOUT_MILLIS = 10000;\n\nfunction generateId(): string {\n    if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n        return crypto.randomUUID();\n    }\n    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {\n        const random = (Math.random() * 16) | 0;\n        const value = char === 'x' ? random : (random & 0x3) | 0x8;\n        return value.toString(16);\n    });\n}\n\nfunction generateShortCode(): string {\n    return generateId().replace(/-/g, '').slice(0, 6).toUpperCase();\n}\n\nfunction createPeerConnection(): RTCPeerConnection {\n    if (typeof RTCPeerConnection === 'undefined') {\n        throw new Error('当前浏览器不支持 WebRTC。');\n    }\n    return new RTCPeerConnection({\n        iceServers: [],\n    });\n}\n\nexport class LanMeshSession {\n    private readonly self: LanPeerIdentity = {\n        id: generateId(),\n        name: `玩家-${generateShortCode()}`,\n    };\n    private roomId?: string;\n    private readonly members = new Map<string, LanPeerIdentity>();\n    private readonly linksByKey = new Map<string, LinkContext>();\n    private readonly directLinks = new Map<string, LinkContext>();\n    private pendingInvite?: PendingInvite;\n    private activeQrPayload?: ActiveQrPayload;\n\n    public readonly onSnapshotChange = new EventDispatcher<this, LanMeshSnapshot>();\n    public readonly onLog = new EventDispatcher<this, LanMeshLogEntry>();\n    public readonly onChat = new EventDispatcher<this, LanMeshChatEntry>();\n    public readonly onAppMessage = new EventDispatcher<this, LanMeshAppMessage>();\n\n    constructor() {\n        this.members.set(this.self.id, this.self);\n    }\n\n    getSnapshot(): LanMeshSnapshot {\n        return this.createSnapshot();\n    }\n\n    getSelf(): LanPeerIdentity {\n        return { ...this.self };\n    }\n\n    ensureLocalRoom(): LanMeshSnapshot {\n        this.ensureRoom();\n        this.dispatchSnapshot();\n        return this.createSnapshot();\n    }\n\n    updateSelfName(name: string): void {\n        const trimmed = name.trim();\n        if (!trimmed || trimmed === this.self.name) {\n            return;\n        }\n        this.self.name = trimmed.slice(0, 24);\n        this.members.set(this.self.id, { ...this.self });\n        if (this.isInRoom()) {\n            this.broadcastRoomSync();\n        }\n        this.dispatchSnapshot();\n    }\n\n    async createRoomInvite(): Promise<void> {\n        this.ensureRoom();\n        this.disposePendingInvite();\n\n        const context = this.createOutgoingLink(undefined, 'inviter');\n        const inviteId = generateId();\n        this.pendingInvite = {\n            inviteId,\n            context,\n        };\n\n        this.log('info', '正在生成邀请二维码...');\n        await context.pc.setLocalDescription(await context.pc.createOffer());\n        await this.waitForIceGatheringComplete(context.pc);\n        this.logLinkDiagnostics(context, '邀请 Offer');\n\n        const packet: LanInvitePacket = {\n            version: 1,\n            kind: 'invite',\n            roomId: this.roomId!,\n            inviteId,\n            inviter: { ...this.self },\n            description: context.pc.localDescription!,\n        };\n\n        this.activeQrPayload = {\n            kind: 'invite',\n            text: await encodeLanQrPacket(packet),\n            title: '邀请二维码',\n            description: '让新玩家扫描这张二维码，加入当前房间。',\n        };\n        this.log('info', '邀请二维码已生成，等待对方回传加入响应。');\n        this.dispatchSnapshot();\n    }\n\n    async importPayload(payloadText: string): Promise<void> {\n        const packet = await decodeLanQrPacket(payloadText);\n\n        if (packet.kind === 'invite') {\n            await this.acceptInvite(packet);\n            return;\n        }\n\n        await this.acceptJoinResponse(packet);\n    }\n\n    async sendChat(text: string): Promise<void> {\n        const normalizedText = text.trim();\n        if (!normalizedText) {\n            return;\n        }\n        if (!this.directLinks.size) {\n            throw new Error('当前还没有直连玩家，无法发送房间消息。');\n        }\n\n        const envelope: ControlEnvelope = {\n            type: 'chat',\n            roomId: this.roomId!,\n            from: { ...this.self },\n            text: normalizedText,\n            timestamp: Date.now(),\n        };\n        this.broadcastEnvelope(envelope);\n        this.onChat.dispatch(this, {\n            from: { ...this.self },\n            text: normalizedText,\n            timestamp: envelope.timestamp,\n        });\n    }\n\n    broadcastAppMessage(payload: unknown, excludedPeerId?: string): void {\n        if (!this.roomId) {\n            throw new Error('当前还没有局域网房间。');\n        }\n        const envelope: ControlEnvelope = {\n            type: 'app-message',\n            roomId: this.roomId,\n            from: { ...this.self },\n            payload,\n        };\n        this.broadcastEnvelope(envelope, excludedPeerId);\n    }\n\n    sendAppMessage(peerId: string, payload: unknown): void {\n        if (!this.roomId) {\n            throw new Error('当前还没有局域网房间。');\n        }\n        this.sendDirectEnvelope(peerId, {\n            type: 'app-message',\n            roomId: this.roomId,\n            from: { ...this.self },\n            payload,\n        });\n    }\n\n    leaveRoom(): void {\n        if (this.isInRoom()) {\n            this.broadcastEnvelope({\n                type: 'member-leave',\n                roomId: this.roomId!,\n                peerId: this.self.id,\n                reason: 'left',\n            });\n        }\n        this.reset();\n    }\n\n    reset(): void {\n        this.disposePendingInvite();\n        Array.from(this.linksByKey.values()).forEach((context) => this.disposeLink(context));\n        this.linksByKey.clear();\n        this.directLinks.clear();\n        this.roomId = undefined;\n        this.members.clear();\n        this.members.set(this.self.id, { ...this.self });\n        this.activeQrPayload = undefined;\n        this.dispatchSnapshot();\n    }\n\n    private isInRoom(): boolean {\n        return Boolean(this.roomId);\n    }\n\n    private ensureRoom(): void {\n        if (!this.roomId) {\n            this.roomId = generateShortCode();\n            this.members.set(this.self.id, { ...this.self });\n            this.log('info', `已创建局域网房间 ${this.roomId}。`);\n        }\n    }\n\n    private createSnapshot(): LanMeshSnapshot {\n        const members = Array.from(this.members.values())\n            .map((member) => {\n                const directLink = this.directLinks.get(member.id);\n                return {\n                    ...member,\n                    isSelf: member.id === this.self.id,\n                    isDirect: member.id === this.self.id || Boolean(directLink),\n                    status: member.id === this.self.id\n                        ? 'self'\n                        : !directLink\n                            ? 'known'\n                            : directLink.status === 'connected'\n                                ? 'connected'\n                                : 'connecting',\n                } satisfies LanMemberSnapshot;\n            })\n            .sort((left, right) => {\n                if (left.isSelf) {\n                    return -1;\n                }\n                if (right.isSelf) {\n                    return 1;\n                }\n                return left.name.localeCompare(right.name, 'zh-Hans-CN');\n            });\n\n        return {\n            self: { ...this.self },\n            roomId: this.roomId,\n            isInRoom: this.isInRoom(),\n            roomReady: this.directLinks.size > 0,\n            directPeerCount: Array.from(this.directLinks.values()).filter((context) => context.status === 'connected').length,\n            members,\n            activeQrPayloadText: this.activeQrPayload?.text ?? '',\n            activeQrPayloadKind: this.activeQrPayload?.kind,\n            activeQrPayloadTitle: this.activeQrPayload?.title,\n            activeQrPayloadDescription: this.activeQrPayload?.description,\n        };\n    }\n\n    private dispatchSnapshot(): void {\n        this.onSnapshotChange.dispatch(this, this.createSnapshot());\n    }\n\n    private createOutgoingLink(peer: LanPeerIdentity | undefined, role: LinkRole): LinkContext {\n        const context: LinkContext = {\n            key: generateId(),\n            peer,\n            pc: createPeerConnection(),\n            role,\n            status: 'connecting',\n        };\n        this.linksByKey.set(context.key, context);\n        if (peer) {\n            this.directLinks.set(peer.id, context);\n        }\n        this.bindPeerEvents(context);\n        this.attachDataChannel(context, context.pc.createDataChannel('ra2-lan-room', {\n            ordered: true,\n        }));\n        this.dispatchSnapshot();\n        return context;\n    }\n\n    private createIncomingLink(peer: LanPeerIdentity, role: LinkRole): LinkContext {\n        const context: LinkContext = {\n            key: generateId(),\n            peer,\n            pc: createPeerConnection(),\n            role,\n            status: 'connecting',\n        };\n        this.linksByKey.set(context.key, context);\n        this.directLinks.set(peer.id, context);\n        this.bindPeerEvents(context);\n        context.pc.ondatachannel = (event) => {\n            if (!this.linksByKey.has(context.key)) {\n                return;\n            }\n            this.attachDataChannel(context, event.channel);\n        };\n        this.dispatchSnapshot();\n        return context;\n    }\n\n    private bindPeerEvents(context: LinkContext): void {\n        const { pc } = context;\n\n        pc.addEventListener('connectionstatechange', () => {\n            if (!this.linksByKey.has(context.key)) {\n                return;\n            }\n            if (pc.connectionState === 'failed' || pc.connectionState === 'closed' || pc.connectionState === 'disconnected') {\n                this.handleLinkClosed(context, pc.connectionState === 'closed' ? 'left' : 'disconnect');\n                return;\n            }\n            this.dispatchSnapshot();\n        });\n        pc.addEventListener('icecandidateerror', (event) => {\n            if (!this.linksByKey.has(context.key)) {\n                return;\n            }\n            const address = 'address' in event && typeof event.address === 'string' ? ` ${event.address}` : '';\n            this.log('warn', `${context.peer?.name ?? '未知玩家'} 的 ICE 候选采集报错${address}：${event.errorText || 'unknown error'}。`);\n        });\n    }\n\n    private attachDataChannel(context: LinkContext, channel: RTCDataChannel): void {\n        context.channel = channel;\n        channel.binaryType = 'arraybuffer';\n\n        channel.addEventListener('open', () => {\n            if (!this.linksByKey.has(context.key)) {\n                return;\n            }\n            context.status = 'connected';\n            if (context.peer) {\n                this.members.set(context.peer.id, { ...context.peer });\n            }\n            this.handleLinkOpened(context);\n            this.dispatchSnapshot();\n        });\n\n        channel.addEventListener('close', () => {\n            if (!this.linksByKey.has(context.key)) {\n                return;\n            }\n            this.handleLinkClosed(context, 'disconnect');\n        });\n\n        channel.addEventListener('error', () => {\n            if (!this.linksByKey.has(context.key)) {\n                return;\n            }\n            this.log('error', `${context.peer?.name ?? '未知玩家'} 的数据通道发生错误。`);\n        });\n\n        channel.addEventListener('message', (event) => {\n            if (!this.linksByKey.has(context.key)) {\n                return;\n            }\n            this.handleChannelMessage(context, event.data);\n        });\n    }\n\n    private async acceptInvite(packet: LanInvitePacket): Promise<void> {\n        if (this.roomId && this.members.size > 1) {\n            throw new Error('你已经在一个局域网房间里，无法再扫描其他房间邀请码。');\n        }\n\n        this.reset();\n        this.roomId = packet.roomId;\n        this.members.set(this.self.id, { ...this.self });\n        this.members.set(packet.inviter.id, packet.inviter);\n\n        const context = this.createIncomingLink(packet.inviter, 'joiner');\n        this.log('info', `正在加入房间 ${packet.roomId}，等待生成响应二维码...`);\n\n        await context.pc.setRemoteDescription(packet.description);\n        await context.pc.setLocalDescription(await context.pc.createAnswer());\n        await this.waitForIceGatheringComplete(context.pc);\n        this.logLinkDiagnostics(context, '加入 Answer');\n\n        const response: LanJoinResponsePacket = {\n            version: 1,\n            kind: 'join-response',\n            roomId: packet.roomId,\n            inviteId: packet.inviteId,\n            inviterPeerId: packet.inviter.id,\n            joiner: { ...this.self },\n            description: context.pc.localDescription!,\n        };\n\n        this.activeQrPayload = {\n            kind: 'join-response',\n            text: await encodeLanQrPacket(response),\n            title: '加入响应二维码',\n            description: `让 ${packet.inviter.name} 扫描这张二维码，完成你的入房。`,\n        };\n        this.dispatchSnapshot();\n    }\n\n    private async acceptJoinResponse(packet: LanJoinResponsePacket): Promise<void> {\n        if (!this.pendingInvite) {\n            throw new Error('当前没有等待中的邀请二维码。');\n        }\n        if (packet.inviterPeerId !== this.self.id || packet.inviteId !== this.pendingInvite.inviteId) {\n            throw new Error('这个加入响应不属于当前邀请二维码。');\n        }\n\n        const { context } = this.pendingInvite;\n        context.peer = packet.joiner;\n        this.directLinks.set(packet.joiner.id, context);\n        this.members.set(packet.joiner.id, packet.joiner);\n        this.log('info', `正在接入 ${packet.joiner.name}...`);\n        await context.pc.setRemoteDescription(packet.description);\n        this.pendingInvite = undefined;\n        this.activeQrPayload = undefined;\n        this.dispatchSnapshot();\n    }\n\n    private handleLinkOpened(context: LinkContext): void {\n        if (!context.peer || !this.roomId) {\n            return;\n        }\n\n        this.sendDirectEnvelope(context.peer.id, {\n            type: 'hello',\n            roomId: this.roomId,\n            self: { ...this.self },\n            members: this.getMemberList(),\n        });\n\n        if (context.role === 'inviter') {\n            this.log('info', `${context.peer.name} 已加入房间，正在补齐与其他成员的直连。`);\n            this.broadcastRoomSync();\n            Array.from(this.directLinks.values())\n                .filter((link) => link.peer && link.peer.id !== context.peer!.id && link.status === 'connected')\n                .forEach((link) => {\n                    this.sendDirectEnvelope(link.peer!.id, {\n                        type: 'member-join',\n                        roomId: this.roomId!,\n                        member: context.peer!,\n                    });\n                    this.sendDirectEnvelope(link.peer!.id, {\n                        type: 'mesh-connect-request',\n                        roomId: this.roomId!,\n                        target: context.peer!,\n                    });\n                });\n        }\n\n        if (context.role === 'joiner') {\n            this.activeQrPayload = undefined;\n            this.log('info', '已接入房间，等待其他成员自动补齐直连。');\n        }\n\n        if (context.role === 'mesh-offerer' || context.role === 'mesh-answerer') {\n            this.log('info', `已和 ${context.peer.name} 建立直连。`);\n            this.broadcastRoomSync();\n        }\n    }\n\n    private handleLinkClosed(context: LinkContext, reason: 'left' | 'disconnect'): void {\n        if (!this.linksByKey.has(context.key)) {\n            return;\n        }\n\n        this.linksByKey.delete(context.key);\n        context.status = 'closed';\n\n        if (context.peer) {\n            this.directLinks.delete(context.peer.id);\n            if (this.members.delete(context.peer.id)) {\n                this.log(reason === 'left' ? 'info' : 'warn', `${context.peer.name} 已离开房间。`);\n            }\n        }\n\n        this.disposeLink(context);\n        this.broadcastRoomSync();\n        this.dispatchSnapshot();\n    }\n\n    private handleChannelMessage(context: LinkContext, data: string | ArrayBuffer | Blob): void {\n        if (typeof data === 'string') {\n            this.handleEnvelopeText(context, data);\n            return;\n        }\n        if (data instanceof ArrayBuffer) {\n            this.handleEnvelopeText(context, new TextDecoder().decode(new Uint8Array(data)));\n            return;\n        }\n        if (typeof Blob !== 'undefined' && data instanceof Blob) {\n            data.text().then((text) => this.handleEnvelopeText(context, text)).catch((error) => {\n                this.log('warn', `读取联机消息失败：${(error as Error).message}`);\n            });\n        }\n    }\n\n    private handleEnvelopeText(context: LinkContext, text: string): void {\n        let payload: ControlEnvelope | undefined;\n        try {\n            payload = JSON.parse(text) as ControlEnvelope;\n        }\n        catch {\n            payload = undefined;\n        }\n\n        if (!payload || typeof payload !== 'object') {\n            this.log('warn', '收到了无法识别的房间消息。');\n            return;\n        }\n\n        switch (payload.type) {\n            case 'hello':\n                this.mergeMembers(payload.self, ...payload.members);\n                this.dispatchSnapshot();\n                return;\n            case 'room-sync':\n                this.mergeMembers(...payload.members);\n                this.dispatchSnapshot();\n                return;\n            case 'member-join':\n                this.members.set(payload.member.id, payload.member);\n                this.log('info', `${payload.member.name} 已进入房间。`);\n                this.dispatchSnapshot();\n                return;\n            case 'member-leave':\n                if (payload.peerId !== this.self.id) {\n                    this.removePeer(payload.peerId, payload.reason);\n                }\n                return;\n            case 'mesh-connect-request':\n                this.handleMeshConnectRequest(context, payload.target).catch((error) => {\n                    this.log('warn', `为 ${payload.target.name} 发起直连失败：${(error as Error).message}`);\n                });\n                return;\n            case 'relay-signal':\n                this.handleRelaySignal(context, payload).catch((error) => {\n                    this.log('warn', `处理转发信令失败：${(error as Error).message}`);\n                });\n                return;\n            case 'chat':\n                this.onChat.dispatch(this, {\n                    from: payload.from,\n                    text: payload.text,\n                    timestamp: payload.timestamp,\n                });\n                return;\n            case 'app-message':\n                this.onAppMessage.dispatch(this, {\n                    from: payload.from,\n                    payload: payload.payload,\n                    timestamp: Date.now(),\n                });\n                return;\n            default:\n                this.log('warn', '收到了未知类型的联机控制消息。');\n        }\n    }\n\n    private async handleMeshConnectRequest(relayContext: LinkContext, target: LanPeerIdentity): Promise<void> {\n        if (!relayContext.peer || target.id === this.self.id || this.directLinks.has(target.id)) {\n            return;\n        }\n\n        this.members.set(target.id, target);\n        const context = this.createOutgoingLink(target, 'mesh-offerer');\n        await context.pc.setLocalDescription(await context.pc.createOffer());\n        await this.waitForIceGatheringComplete(context.pc);\n        this.logLinkDiagnostics(context, `对 ${target.name} 的 mesh Offer`);\n\n        this.sendDirectEnvelope(relayContext.peer.id, {\n            type: 'relay-signal',\n            roomId: this.roomId!,\n            source: { ...this.self },\n            targetPeerId: target.id,\n            signalType: 'offer',\n            description: context.pc.localDescription!,\n        });\n    }\n\n    private async handleRelaySignal(relayContext: LinkContext, payload: Extract<ControlEnvelope, { type: 'relay-signal' }>): Promise<void> {\n        if (!relayContext.peer) {\n            return;\n        }\n\n        if (payload.targetPeerId !== this.self.id) {\n            this.sendDirectEnvelope(payload.targetPeerId, payload);\n            return;\n        }\n\n        this.members.set(payload.source.id, payload.source);\n\n        if (payload.signalType === 'offer') {\n            if (this.directLinks.has(payload.source.id)) {\n                return;\n            }\n            const context = this.createIncomingLink(payload.source, 'mesh-answerer');\n            await context.pc.setRemoteDescription(payload.description);\n            await context.pc.setLocalDescription(await context.pc.createAnswer());\n            await this.waitForIceGatheringComplete(context.pc);\n            this.logLinkDiagnostics(context, `对 ${payload.source.name} 的 mesh Answer`);\n\n            this.sendDirectEnvelope(relayContext.peer.id, {\n                type: 'relay-signal',\n                roomId: this.roomId!,\n                source: { ...this.self },\n                targetPeerId: payload.source.id,\n                signalType: 'answer',\n                description: context.pc.localDescription!,\n            });\n            return;\n        }\n\n        const existingLink = this.directLinks.get(payload.source.id);\n        if (!existingLink) {\n            throw new Error(`没有找到 ${payload.source.name} 的待完成直连。`);\n        }\n        await existingLink.pc.setRemoteDescription(payload.description);\n    }\n\n    private mergeMembers(...members: LanPeerIdentity[]): void {\n        members.forEach((member) => {\n            this.members.set(member.id, member);\n        });\n    }\n\n    private removePeer(peerId: string, reason: 'left' | 'disconnect'): void {\n        const member = this.members.get(peerId);\n        this.members.delete(peerId);\n        const link = this.directLinks.get(peerId);\n        if (link) {\n            this.linksByKey.delete(link.key);\n            this.directLinks.delete(peerId);\n            this.disposeLink(link);\n        }\n        if (member) {\n            this.log(reason === 'left' ? 'info' : 'warn', `${member.name} 已离开房间。`);\n        }\n        this.dispatchSnapshot();\n    }\n\n    private getMemberList(): LanPeerIdentity[] {\n        return Array.from(this.members.values()).map((member) => ({ ...member }));\n    }\n\n    private broadcastRoomSync(): void {\n        if (!this.roomId || !this.directLinks.size) {\n            this.dispatchSnapshot();\n            return;\n        }\n        this.broadcastEnvelope({\n            type: 'room-sync',\n            roomId: this.roomId,\n            members: this.getMemberList(),\n        });\n    }\n\n    private broadcastEnvelope(envelope: ControlEnvelope, excludedPeerId?: string): void {\n        Array.from(this.directLinks.values())\n            .filter((context) => context.peer && context.peer.id !== excludedPeerId && context.status === 'connected')\n            .forEach((context) => {\n                try {\n                    this.safeSend(context, envelope);\n                }\n                catch (error) {\n                    this.log('warn', `向 ${context.peer?.name ?? '未知玩家'} 发送联机消息失败：${(error as Error).message}`);\n                    this.handleLinkClosed(context, 'disconnect');\n                }\n            });\n    }\n\n    private sendDirectEnvelope(peerId: string, envelope: ControlEnvelope): void {\n        const context = this.directLinks.get(peerId);\n        if (!context) {\n            throw new Error(`没有到 ${peerId} 的直连通道。`);\n        }\n        try {\n            this.safeSend(context, envelope);\n        }\n        catch (error) {\n            this.log('warn', `向 ${context.peer?.name ?? peerId} 发送联机消息失败：${(error as Error).message}`);\n            this.handleLinkClosed(context, 'disconnect');\n            throw error;\n        }\n    }\n\n    private safeSend(context: LinkContext, envelope: ControlEnvelope): void {\n        if (!context.channel || context.channel.readyState !== 'open') {\n            throw new Error(`和 ${context.peer?.name ?? '未知玩家'} 的数据通道尚未打开。`);\n        }\n        context.channel.send(JSON.stringify(envelope));\n    }\n\n    private async waitForIceGatheringComplete(pc: RTCPeerConnection): Promise<void> {\n        if (pc.iceGatheringState === 'complete') {\n            return;\n        }\n\n        await new Promise<void>((resolve, reject) => {\n            const timeoutId = window.setTimeout(() => {\n                cleanup();\n                reject(new Error('ICE 候选收集超时，请稍后重试。'));\n            }, ICE_GATHER_TIMEOUT_MILLIS);\n\n            const handleChange = () => {\n                if (pc.iceGatheringState === 'complete') {\n                    cleanup();\n                    resolve();\n                }\n            };\n\n            const cleanup = () => {\n                clearTimeout(timeoutId);\n                pc.removeEventListener('icegatheringstatechange', handleChange);\n            };\n\n            pc.addEventListener('icegatheringstatechange', handleChange);\n        });\n    }\n\n    private disposePendingInvite(): void {\n        if (!this.pendingInvite) {\n            return;\n        }\n        this.disposeLink(this.pendingInvite.context);\n        this.linksByKey.delete(this.pendingInvite.context.key);\n        this.pendingInvite = undefined;\n    }\n\n    private disposeLink(context: LinkContext): void {\n        try {\n            context.channel?.close();\n        }\n        catch {\n        }\n        try {\n            context.pc.close();\n        }\n        catch {\n        }\n    }\n\n    private log(level: LanMeshLogEntry['level'], text: string): void {\n        this.onLog.dispatch(this, {\n            level,\n            text,\n            timestamp: Date.now(),\n        });\n    }\n\n    private logLinkDiagnostics(context: LinkContext, label: string): void {\n        const summary = summarizeSdpCandidates(context.pc.localDescription);\n        this.log('info', `${label} 候选情况：${formatSdpCandidateSummary(summary)}。`);\n        const warning = getSdpCandidateWarning(summary);\n        if (warning) {\n            this.log('warn', warning);\n        }\n    }\n}\n"
  },
  {
    "path": "src/network/lan/LanQrPayload.ts",
    "content": "export interface LanPeerIdentity {\n    id: string;\n    name: string;\n}\n\nexport interface LanInvitePacket {\n    version: 1;\n    kind: 'invite';\n    roomId: string;\n    inviteId: string;\n    inviter: LanPeerIdentity;\n    description: RTCSessionDescriptionInit;\n}\n\nexport interface LanJoinResponsePacket {\n    version: 1;\n    kind: 'join-response';\n    roomId: string;\n    inviteId: string;\n    inviterPeerId: string;\n    joiner: LanPeerIdentity;\n    description: RTCSessionDescriptionInit;\n}\n\nexport type LanQrPacket = LanInvitePacket | LanJoinResponsePacket;\n\nconst PREFIX = 'ra2lan';\nconst JSON_PREFIX = `${PREFIX}:json:`;\nconst GZIP_PREFIX = `${PREFIX}:gzip:`;\n\nfunction bytesToBase64Url(bytes: Uint8Array): string {\n    let binary = '';\n    bytes.forEach((value) => {\n        binary += String.fromCharCode(value);\n    });\n    return btoa(binary)\n        .replace(/\\+/g, '-')\n        .replace(/\\//g, '_')\n        .replace(/=+$/g, '');\n}\n\nfunction base64UrlToBytes(value: string): Uint8Array {\n    const normalized = value\n        .replace(/-/g, '+')\n        .replace(/_/g, '/')\n        .padEnd(Math.ceil(value.length / 4) * 4, '=');\n    const binary = atob(normalized);\n    const bytes = new Uint8Array(binary.length);\n    for (let index = 0; index < binary.length; index += 1) {\n        bytes[index] = binary.charCodeAt(index);\n    }\n    return bytes;\n}\n\nfunction supportsCompressionStreams(): boolean {\n    return typeof CompressionStream !== 'undefined' &&\n        typeof DecompressionStream !== 'undefined';\n}\n\nfunction toArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n    return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n}\n\nasync function gzipBytes(bytes: Uint8Array): Promise<Uint8Array> {\n    const stream = new CompressionStream('gzip');\n    const writer = stream.writable.getWriter();\n    await writer.write(toArrayBuffer(bytes));\n    await writer.close();\n    return new Uint8Array(await new Response(stream.readable).arrayBuffer());\n}\n\nasync function gunzipBytes(bytes: Uint8Array): Promise<Uint8Array> {\n    const stream = new DecompressionStream('gzip');\n    const writer = stream.writable.getWriter();\n    await writer.write(toArrayBuffer(bytes));\n    await writer.close();\n    return new Uint8Array(await new Response(stream.readable).arrayBuffer());\n}\n\nfunction parsePacket(jsonText: string): LanQrPacket {\n    let parsed: unknown;\n    try {\n        parsed = JSON.parse(jsonText);\n    }\n    catch {\n        throw new Error('二维码内容不是合法 JSON。');\n    }\n\n    if (!parsed || typeof parsed !== 'object') {\n        throw new Error('二维码内容格式不正确。');\n    }\n\n    const candidate = parsed as Partial<LanQrPacket>;\n    if (candidate.version !== 1) {\n        throw new Error(`不支持的二维码版本：${candidate.version ?? 'unknown'}。`);\n    }\n    if (candidate.kind !== 'invite' && candidate.kind !== 'join-response') {\n        throw new Error('二维码内容类型无法识别。');\n    }\n    return candidate as LanQrPacket;\n}\n\nexport async function encodeLanQrPacket(packet: LanQrPacket): Promise<string> {\n    const jsonText = JSON.stringify(packet);\n    return `${JSON_PREFIX}${jsonText}`;\n}\n\nexport async function decodeLanQrPacket(payloadText: string): Promise<LanQrPacket> {\n    const normalized = payloadText.trim();\n\n    if (normalized.startsWith(GZIP_PREFIX)) {\n        if (!supportsCompressionStreams()) {\n            throw new Error('当前浏览器不支持压缩二维码内容。');\n        }\n        const bytes = base64UrlToBytes(normalized.slice(GZIP_PREFIX.length));\n        const uncompressed = await gunzipBytes(bytes);\n        return parsePacket(new TextDecoder().decode(uncompressed));\n    }\n\n    if (normalized.startsWith(JSON_PREFIX)) {\n        return parsePacket(normalized.slice(JSON_PREFIX.length));\n    }\n\n    throw new Error('这不是 redalert2 局域网联机二维码内容。');\n}\n"
  },
  {
    "path": "src/network/lan/LanRoomSession.ts",
    "content": "import { VirtualFile } from '@/data/vfs/VirtualFile';\nimport { MapDigest } from '@/engine/MapDigest';\nimport { GameOpts } from '@/game/gameopts/GameOpts';\nimport { NO_TEAM_ID, OBS_COUNTRY_ID, RANDOM_COLOR_ID, RANDOM_COUNTRY_ID, RANDOM_START_POS } from '@/game/gameopts/constants';\nimport { SlotInfo, SlotType as NetSlotType } from '@/network/gameopt/SlotInfo';\nimport { LanPeerIdentity } from '@/network/lan/LanQrPayload';\nimport { LanMeshAppMessage, LanMeshSession, LanMeshSnapshot } from '@/network/lan/LanMeshSession';\nimport { EventDispatcher } from '@/util/event';\nimport { base64StringToUint8Array, uint8ArrayToBase64String } from '@/util/string';\n\ninterface GameMode {\n    id: number;\n    mpDialogSettings: any;\n}\n\ninterface GameModes {\n    getById(id: number): GameMode;\n}\n\ninterface MapDirectory {\n    containsEntry(entryName: string): Promise<boolean>;\n    writeFile(file: VirtualFile): Promise<void>;\n}\n\ninterface MapList {\n    addFromMapFile(file: VirtualFile): void;\n}\n\ninterface MapFileLoader {\n    load(mapName: string): Promise<any>;\n}\n\nexport interface LanHumanAssignment {\n    peerId: string;\n    slotIndex: number;\n    name: string;\n}\n\nexport interface LanMapTransferPeerState {\n    status: 'idle' | 'pending' | 'sending' | 'receiving' | 'complete' | 'error';\n    receivedBytes?: number;\n    totalBytes?: number;\n    error?: string;\n    updatedAt: number;\n}\n\nexport interface LanRoomState {\n    version: 1;\n    hostPeerId: string;\n    memberOrder: string[];\n    humanAssignments: LanHumanAssignment[];\n    gameOpts: GameOpts;\n    slotsInfo: SlotInfo[];\n    readyStateByPeerId: Record<string, boolean>;\n    mapTransferStateByPeerId: Record<string, LanMapTransferPeerState>;\n}\n\nexport interface LanLaunchDescriptor {\n    kind: 'lan';\n    roomId: string;\n    gameId: string;\n    timestamp: number;\n    hostPeerId: string;\n    localPeerId: string;\n    localPlayerName: string;\n    gameOpts: GameOpts;\n    humanAssignments: LanHumanAssignment[];\n    mapTransferStateByPeerId: Record<string, LanMapTransferPeerState>;\n    returnRoute: {\n        screenType: number;\n        params?: any;\n    };\n}\n\nexport interface LanRoomMemberSnapshot {\n    peerId: string;\n    name: string;\n    isSelf: boolean;\n    isHost: boolean;\n    isConnected: boolean;\n    slotIndex?: number;\n    ready: boolean;\n    mapTransfer: LanMapTransferPeerState;\n}\n\nexport interface LanRoomSnapshot {\n    self: LanPeerIdentity;\n    mesh: LanMeshSnapshot;\n    isRoomActive: boolean;\n    isHost: boolean;\n    hostPeerId?: string;\n    roomState?: LanRoomState;\n    members: LanRoomMemberSnapshot[];\n    localMapFileReady: boolean;\n    canInvite: boolean;\n    canStart: boolean;\n    launchDescriptor?: LanLaunchDescriptor;\n}\n\ntype LanRoomMessage =\n    | {\n        type: 'state-sync';\n        state: LanRoomState;\n    }\n    | {\n        type: 'slot-request';\n        peerId: string;\n        slotIndex: number;\n        countryId: number;\n        colorId: number;\n        startPos: number;\n        teamId: number;\n    }\n    | {\n        type: 'ready';\n        peerId: string;\n        ready: boolean;\n    }\n    | {\n        type: 'map-offer';\n        peerId: string;\n        filename: string;\n        digest: string;\n        sizeBytes: number;\n        totalChunks: number;\n    }\n    | {\n        type: 'map-chunk';\n        peerId: string;\n        digest: string;\n        index: number;\n        totalChunks: number;\n        data: string;\n    }\n    | {\n        type: 'map-complete';\n        peerId: string;\n        digest: string;\n        ok: boolean;\n        error?: string;\n    }\n    | {\n        type: 'start-game';\n        descriptor: LanLaunchDescriptor;\n    }\n    | {\n        type: 'host-handover';\n        hostPeerId: string;\n    };\n\ninterface IncomingMapTransfer {\n    filename: string;\n    digest: string;\n    totalChunks: number;\n    sizeBytes: number;\n    chunks: string[];\n}\n\nconst MAP_CHUNK_SIZE = 12 * 1024;\n\nfunction cloneHumanPlayer(player: any) {\n    return {\n        name: player.name,\n        countryId: player.countryId,\n        colorId: player.colorId,\n        startPos: player.startPos,\n        teamId: player.teamId,\n    };\n}\n\nfunction cloneAiPlayer(ai: any) {\n    return ai\n        ? {\n            difficulty: ai.difficulty,\n            countryId: ai.countryId,\n            colorId: ai.colorId,\n            startPos: ai.startPos,\n            teamId: ai.teamId,\n        }\n        : undefined;\n}\n\nfunction cloneGameOpts(gameOpts: GameOpts): GameOpts {\n    return {\n        gameMode: gameOpts.gameMode,\n        gameSpeed: gameOpts.gameSpeed,\n        credits: gameOpts.credits,\n        unitCount: gameOpts.unitCount,\n        shortGame: gameOpts.shortGame,\n        superWeapons: gameOpts.superWeapons,\n        buildOffAlly: gameOpts.buildOffAlly,\n        mcvRepacks: gameOpts.mcvRepacks,\n        cratesAppear: gameOpts.cratesAppear,\n        hostTeams: gameOpts.hostTeams,\n        destroyableBridges: gameOpts.destroyableBridges,\n        multiEngineer: gameOpts.multiEngineer,\n        noDogEngiKills: gameOpts.noDogEngiKills,\n        mapName: gameOpts.mapName,\n        mapTitle: gameOpts.mapTitle,\n        mapDigest: gameOpts.mapDigest,\n        mapSizeBytes: gameOpts.mapSizeBytes,\n        maxSlots: gameOpts.maxSlots,\n        mapOfficial: gameOpts.mapOfficial,\n        humanPlayers: gameOpts.humanPlayers.map(cloneHumanPlayer),\n        aiPlayers: gameOpts.aiPlayers.map(cloneAiPlayer),\n        unknown: gameOpts.unknown,\n    };\n}\n\nfunction cloneSlotsInfo(slotsInfo: SlotInfo[]): SlotInfo[] {\n    return slotsInfo.map((slot) => ({\n        type: slot.type,\n        name: slot.name,\n        difficulty: slot.difficulty,\n    }));\n}\n\nfunction cloneMapTransferState(state: Record<string, LanMapTransferPeerState>): Record<string, LanMapTransferPeerState> {\n    const cloned: Record<string, LanMapTransferPeerState> = {};\n    Object.entries(state).forEach(([peerId, value]) => {\n        cloned[peerId] = { ...value };\n    });\n    return cloned;\n}\n\nfunction cloneRoomState(state: LanRoomState): LanRoomState {\n    return {\n        version: 1,\n        hostPeerId: state.hostPeerId,\n        memberOrder: [...state.memberOrder],\n        humanAssignments: state.humanAssignments.map((assignment) => ({ ...assignment })),\n        gameOpts: cloneGameOpts(state.gameOpts),\n        slotsInfo: cloneSlotsInfo(state.slotsInfo),\n        readyStateByPeerId: { ...state.readyStateByPeerId },\n        mapTransferStateByPeerId: cloneMapTransferState(state.mapTransferStateByPeerId),\n    };\n}\n\nfunction createTransferState(status: LanMapTransferPeerState['status'], totalBytes?: number, receivedBytes?: number, error?: string): LanMapTransferPeerState {\n    return {\n        status,\n        totalBytes,\n        receivedBytes,\n        error,\n        updatedAt: Date.now(),\n    };\n}\n\nfunction getTransferStatePriority(status: LanMapTransferPeerState['status']): number {\n    switch (status) {\n        case 'idle':\n            return 0;\n        case 'pending':\n            return 1;\n        case 'sending':\n            return 1;\n        case 'receiving':\n            return 2;\n        case 'error':\n            return 3;\n        case 'complete':\n            return 4;\n        default:\n            return 0;\n    }\n}\n\nfunction generateId(): string {\n    if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n        return crypto.randomUUID();\n    }\n    return `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;\n}\n\nfunction createDefaultHumanPlayer(name: string, mustAlly: boolean) {\n    return {\n        name,\n        countryId: RANDOM_COUNTRY_ID,\n        colorId: RANDOM_COLOR_ID,\n        startPos: RANDOM_START_POS,\n        teamId: mustAlly ? 0 : NO_TEAM_ID,\n    };\n}\n\nexport class LanRoomSession {\n    private roomState?: LanRoomState;\n    private currentCustomMapFile?: VirtualFile;\n    private incomingTransfers = new Map<string, IncomingMapTransfer>();\n    private lastMeshSnapshot: LanMeshSnapshot;\n    private launchDescriptor?: LanLaunchDescriptor;\n    private disposed = false;\n\n    public readonly onSnapshotChange = new EventDispatcher<this, LanRoomSnapshot>();\n    public readonly onLog = new EventDispatcher<this, { level: 'info' | 'warn' | 'error'; text: string; timestamp: number }>();\n    public readonly onLaunch = new EventDispatcher<this, LanLaunchDescriptor>();\n\n    constructor(\n        private readonly meshSession: LanMeshSession,\n        private readonly gameModes: GameModes,\n        private readonly mapFileLoader: MapFileLoader,\n        private readonly mapDir?: MapDirectory,\n        private readonly mapList?: MapList\n    ) {\n        this.lastMeshSnapshot = meshSession.getSnapshot();\n        this.handleMeshSnapshot = this.handleMeshSnapshot.bind(this);\n        this.handleAppMessage = this.handleAppMessage.bind(this);\n        this.meshSession.onSnapshotChange.subscribe(this.handleMeshSnapshot);\n        this.meshSession.onAppMessage.subscribe(this.handleAppMessage);\n    }\n\n    dispose(): void {\n        if (this.disposed) {\n            return;\n        }\n        this.disposed = true;\n        this.meshSession.onSnapshotChange.unsubscribe(this.handleMeshSnapshot);\n        this.meshSession.onAppMessage.unsubscribe(this.handleAppMessage);\n    }\n\n    getSnapshot(): LanRoomSnapshot {\n        return this.createSnapshot();\n    }\n\n    getResolvedCustomMapFile(): VirtualFile | undefined {\n        return this.currentCustomMapFile;\n    }\n\n    startHosting(snapshot: { gameOpts: GameOpts; slotsInfo: SlotInfo[]; currentMapFile?: any }): void {\n        const meshSnapshot = this.meshSession.ensureLocalRoom();\n        this.lastMeshSnapshot = meshSnapshot;\n        const self = meshSnapshot.self;\n        const state: LanRoomState = {\n            version: 1,\n            hostPeerId: self.id,\n            memberOrder: [self.id],\n            humanAssignments: [{ peerId: self.id, slotIndex: 0, name: self.name }],\n            gameOpts: cloneGameOpts(snapshot.gameOpts),\n            slotsInfo: cloneSlotsInfo(snapshot.slotsInfo),\n            readyStateByPeerId: { [self.id]: false },\n            mapTransferStateByPeerId: {\n                [self.id]: snapshot.gameOpts.mapOfficial\n                    ? createTransferState('complete', snapshot.gameOpts.mapSizeBytes, snapshot.gameOpts.mapSizeBytes)\n                    : createTransferState('complete', snapshot.gameOpts.mapSizeBytes, snapshot.gameOpts.mapSizeBytes),\n            },\n        };\n        this.currentCustomMapFile = snapshot.gameOpts.mapOfficial || !snapshot.currentMapFile\n            ? undefined\n            : VirtualFile.fromBytes(snapshot.currentMapFile.getBytes(), snapshot.gameOpts.mapName);\n        this.roomState = state;\n        this.reconcileRoomStateWithMesh();\n        this.broadcastStateSync();\n        this.dispatchSnapshot();\n    }\n\n    applyHostPregameSnapshot(snapshot: { gameOpts: GameOpts; slotsInfo: SlotInfo[]; currentMapFile?: any }): void {\n        if (!this.roomState || !this.isHost()) {\n            return;\n        }\n        this.roomState.gameOpts = cloneGameOpts(snapshot.gameOpts);\n        this.roomState.slotsInfo = cloneSlotsInfo(snapshot.slotsInfo);\n        this.currentCustomMapFile = snapshot.gameOpts.mapOfficial || !snapshot.currentMapFile\n            ? undefined\n            : VirtualFile.fromBytes(snapshot.currentMapFile.getBytes(), snapshot.gameOpts.mapName);\n        this.reconcileRoomStateWithMesh();\n        this.broadcastStateSync();\n        this.scheduleCustomMapTransfers();\n        this.dispatchSnapshot();\n    }\n\n    async setReady(ready: boolean): Promise<void> {\n        const self = this.meshSession.getSelf();\n        if (!this.roomState) {\n            return;\n        }\n        if (this.isHost()) {\n            this.roomState.readyStateByPeerId[self.id] = ready;\n            this.broadcastStateSync();\n            this.dispatchSnapshot();\n            return;\n        }\n        this.meshSession.sendAppMessage(this.roomState.hostPeerId, {\n            type: 'ready',\n            peerId: self.id,\n            ready,\n        } satisfies LanRoomMessage);\n    }\n\n    async requestSlotConfig(slotIndex: number, config: { countryId: number; colorId: number; startPos: number; teamId: number }): Promise<void> {\n        if (!this.roomState || this.isHost()) {\n            return;\n        }\n        const self = this.meshSession.getSelf();\n        this.meshSession.sendAppMessage(this.roomState.hostPeerId, {\n            type: 'slot-request',\n            peerId: self.id,\n            slotIndex,\n            ...config,\n        } satisfies LanRoomMessage);\n    }\n\n    leaveRoom(): void {\n        if (this.roomState && this.isHost()) {\n            const nextHostPeerId = this.findNextHostPeerId(this.roomState.hostPeerId);\n            if (nextHostPeerId) {\n                this.meshSession.broadcastAppMessage({\n                    type: 'host-handover',\n                    hostPeerId: nextHostPeerId,\n                } satisfies LanRoomMessage);\n            }\n        }\n        this.roomState = undefined;\n        this.currentCustomMapFile = undefined;\n        this.incomingTransfers.clear();\n        this.launchDescriptor = undefined;\n        this.dispatchSnapshot();\n    }\n\n    startGame(returnRoute: { screenType: number; params?: any }): LanLaunchDescriptor {\n        if (!this.roomState || !this.isHost()) {\n            throw new Error('只有房主可以开始游戏。');\n        }\n        if (!this.canStart()) {\n            throw new Error('当前房间还不能开始游戏。');\n        }\n        const self = this.meshSession.getSelf();\n        const descriptor: LanLaunchDescriptor = {\n            kind: 'lan',\n            roomId: this.lastMeshSnapshot.roomId ?? '',\n            gameId: generateId(),\n            timestamp: Date.now(),\n            hostPeerId: this.roomState.hostPeerId,\n            localPeerId: self.id,\n            localPlayerName: self.name,\n            gameOpts: cloneGameOpts(this.roomState.gameOpts),\n            humanAssignments: this.roomState.humanAssignments.map((assignment) => ({ ...assignment })),\n            mapTransferStateByPeerId: cloneMapTransferState(this.roomState.mapTransferStateByPeerId),\n            returnRoute,\n        };\n        this.launchDescriptor = descriptor;\n        this.meshSession.broadcastAppMessage({\n            type: 'start-game',\n            descriptor,\n        } satisfies LanRoomMessage);\n        this.onLaunch.dispatch(this, descriptor);\n        this.dispatchSnapshot();\n        return descriptor;\n    }\n\n    private handleMeshSnapshot(snapshot: LanMeshSnapshot): void {\n        this.lastMeshSnapshot = snapshot;\n        if (!snapshot.isInRoom) {\n            this.roomState = undefined;\n            this.currentCustomMapFile = undefined;\n            this.incomingTransfers.clear();\n            this.launchDescriptor = undefined;\n            this.dispatchSnapshot();\n            return;\n        }\n        if (!this.roomState) {\n            this.dispatchSnapshot();\n            return;\n        }\n\n        const previousHostPeerId = this.roomState.hostPeerId;\n        this.reconcileRoomStateWithMesh();\n\n        if (previousHostPeerId !== this.roomState.hostPeerId && this.isHost()) {\n            this.log('info', '房主已迁移到当前客户端。');\n            this.broadcastStateSync();\n            this.scheduleCustomMapTransfers();\n        }\n\n        if (this.isHost()) {\n            this.broadcastStateSync();\n            this.scheduleCustomMapTransfers();\n        }\n        this.dispatchSnapshot();\n    }\n\n    private handleAppMessage(entry: LanMeshAppMessage, _sender: LanMeshSession): void {\n        const payload = entry.payload;\n        if (!payload || typeof payload !== 'object') {\n            return;\n        }\n        const message = payload as LanRoomMessage;\n        switch (message.type) {\n            case 'state-sync':\n                this.handleStateSync(entry.from, message);\n                return;\n            case 'slot-request':\n                this.handleSlotRequest(entry.from, message);\n                return;\n            case 'ready':\n                this.handleReady(entry.from, message);\n                return;\n            case 'map-offer':\n                this.handleMapOffer(entry.from, message);\n                return;\n            case 'map-chunk':\n                void this.handleMapChunk(entry.from, message);\n                return;\n            case 'map-complete':\n                this.handleMapComplete(entry.from, message);\n                return;\n            case 'start-game':\n                this.handleStartGame(entry.from, message);\n                return;\n            case 'host-handover':\n                this.handleHostHandover(message);\n                return;\n            default:\n                return;\n        }\n    }\n\n    private handleStateSync(from: LanPeerIdentity, message: Extract<LanRoomMessage, { type: 'state-sync' }>): void {\n        if (this.isHost() && from.id !== this.roomState?.hostPeerId) {\n            return;\n        }\n        const selfPeerId = this.meshSession.getSelf().id;\n        const previousLocalTransferState = this.roomState?.mapTransferStateByPeerId[selfPeerId];\n        this.roomState = cloneRoomState(message.state);\n        this.reconcileRoomStateWithMesh();\n        if (!this.isHost() && previousLocalTransferState) {\n            const nextRemoteTransferState = this.roomState.mapTransferStateByPeerId[selfPeerId];\n            if (!nextRemoteTransferState ||\n                getTransferStatePriority(previousLocalTransferState.status) > getTransferStatePriority(nextRemoteTransferState.status) ||\n                (previousLocalTransferState.status === nextRemoteTransferState.status &&\n                    previousLocalTransferState.updatedAt > nextRemoteTransferState.updatedAt)) {\n                this.roomState.mapTransferStateByPeerId[selfPeerId] = { ...previousLocalTransferState };\n            }\n        }\n        void this.ensureLocalCustomMapIfNeeded();\n        this.dispatchSnapshot();\n    }\n\n    private handleSlotRequest(from: LanPeerIdentity, message: Extract<LanRoomMessage, { type: 'slot-request' }>): void {\n        if (!this.roomState || !this.isHost() || message.peerId !== from.id) {\n            return;\n        }\n        const assignment = this.roomState.humanAssignments.find((candidate) => candidate.peerId === from.id);\n        if (!assignment || assignment.slotIndex !== message.slotIndex) {\n            return;\n        }\n        const slotInfo = this.roomState.slotsInfo[assignment.slotIndex];\n        if (!slotInfo || slotInfo.type !== NetSlotType.Player) {\n            return;\n        }\n        const human = this.roomState.gameOpts.humanPlayers.find((player) => player.name === assignment.name);\n        if (!human) {\n            return;\n        }\n        human.countryId = message.countryId;\n        human.colorId = message.colorId;\n        human.startPos = message.startPos;\n        human.teamId = message.teamId;\n        this.broadcastStateSync();\n        this.dispatchSnapshot();\n    }\n\n    private handleReady(from: LanPeerIdentity, message: Extract<LanRoomMessage, { type: 'ready' }>): void {\n        if (!this.roomState || !this.isHost() || message.peerId !== from.id) {\n            return;\n        }\n        this.roomState.readyStateByPeerId[from.id] = message.ready;\n        this.broadcastStateSync();\n        this.dispatchSnapshot();\n    }\n\n    private handleMapOffer(from: LanPeerIdentity, message: Extract<LanRoomMessage, { type: 'map-offer' }>): void {\n        if (this.isHost() || !this.roomState || message.peerId !== this.meshSession.getSelf().id) {\n            return;\n        }\n        this.incomingTransfers.set(from.id, {\n            filename: message.filename,\n            digest: message.digest,\n            totalChunks: message.totalChunks,\n            sizeBytes: message.sizeBytes,\n            chunks: new Array(message.totalChunks),\n        });\n        this.updateLocalMapTransferState('receiving', message.sizeBytes, 0);\n        this.dispatchSnapshot();\n    }\n\n    private async handleMapChunk(from: LanPeerIdentity, message: Extract<LanRoomMessage, { type: 'map-chunk' }>): Promise<void> {\n        if (this.isHost() || !this.roomState || message.peerId !== this.meshSession.getSelf().id) {\n            return;\n        }\n        const transfer = this.incomingTransfers.get(from.id);\n        if (!transfer || transfer.digest !== message.digest) {\n            return;\n        }\n        transfer.chunks[message.index] = message.data;\n        const receivedCount = transfer.chunks.filter(Boolean).length;\n        const receivedBytes = Math.min(transfer.sizeBytes, receivedCount * MAP_CHUNK_SIZE);\n        this.updateLocalMapTransferState('receiving', transfer.sizeBytes, receivedBytes);\n        this.dispatchSnapshot();\n\n        if (receivedCount !== transfer.totalChunks) {\n            return;\n        }\n\n        try {\n            const bytes = base64StringToUint8Array(transfer.chunks.join(''));\n            const file = VirtualFile.fromBytes(bytes, transfer.filename);\n            if (MapDigest.compute(file) !== transfer.digest) {\n                throw new Error('接收到的自定义地图摘要不匹配。');\n            }\n            this.currentCustomMapFile = file;\n            await this.persistCustomMap(file);\n            this.incomingTransfers.delete(from.id);\n            this.updateLocalMapTransferState('complete', transfer.sizeBytes, transfer.sizeBytes);\n            this.meshSession.sendAppMessage(this.roomState.hostPeerId, {\n                type: 'map-complete',\n                peerId: this.meshSession.getSelf().id,\n                digest: transfer.digest,\n                ok: true,\n            } satisfies LanRoomMessage);\n            this.dispatchSnapshot();\n        }\n        catch (error) {\n            const errorText = (error as Error).message;\n            this.updateLocalMapTransferState('error', transfer.sizeBytes, undefined, errorText);\n            this.meshSession.sendAppMessage(this.roomState.hostPeerId, {\n                type: 'map-complete',\n                peerId: this.meshSession.getSelf().id,\n                digest: transfer.digest,\n                ok: false,\n                error: errorText,\n            } satisfies LanRoomMessage);\n            this.dispatchSnapshot();\n        }\n    }\n\n    private handleMapComplete(from: LanPeerIdentity, message: Extract<LanRoomMessage, { type: 'map-complete' }>): void {\n        if (!this.roomState || !this.isHost() || message.peerId !== from.id) {\n            return;\n        }\n        this.roomState.mapTransferStateByPeerId[from.id] = message.ok\n            ? createTransferState('complete', this.roomState.gameOpts.mapSizeBytes, this.roomState.gameOpts.mapSizeBytes)\n            : createTransferState('error', this.roomState.gameOpts.mapSizeBytes, undefined, message.error);\n        this.broadcastStateSync();\n        this.dispatchSnapshot();\n    }\n\n    private handleStartGame(_from: LanPeerIdentity, message: Extract<LanRoomMessage, { type: 'start-game' }>): void {\n        const descriptor = {\n            ...message.descriptor,\n            localPeerId: this.meshSession.getSelf().id,\n            localPlayerName: this.meshSession.getSelf().name,\n            returnRoute: message.descriptor.returnRoute,\n        };\n        this.launchDescriptor = descriptor;\n        this.onLaunch.dispatch(this, descriptor);\n        this.dispatchSnapshot();\n    }\n\n    private handleHostHandover(message: Extract<LanRoomMessage, { type: 'host-handover' }>): void {\n        if (!this.roomState) {\n            return;\n        }\n        this.roomState.hostPeerId = message.hostPeerId;\n        this.reconcileRoomStateWithMesh();\n        if (this.isHost()) {\n            this.broadcastStateSync();\n            this.scheduleCustomMapTransfers();\n        }\n        this.dispatchSnapshot();\n    }\n\n    private reconcileRoomStateWithMesh(): void {\n        if (!this.roomState) {\n            return;\n        }\n        const activeMembers = this.lastMeshSnapshot.members.map((member) => ({\n            id: member.id,\n            name: member.name,\n        }));\n        const activeIds = new Set(activeMembers.map((member) => member.id));\n\n        this.roomState.memberOrder = this.roomState.memberOrder.filter((peerId) => activeIds.has(peerId));\n        activeMembers.forEach((member) => {\n            if (!this.roomState!.memberOrder.includes(member.id)) {\n                this.roomState!.memberOrder.push(member.id);\n            }\n        });\n\n        if (!activeIds.has(this.roomState.hostPeerId)) {\n            const nextHostPeerId = this.findNextHostPeerId(this.roomState.hostPeerId);\n            if (nextHostPeerId) {\n                this.roomState.hostPeerId = nextHostPeerId;\n            }\n        }\n\n        const readyStateByPeerId: Record<string, boolean> = {};\n        const mapTransferStateByPeerId: Record<string, LanMapTransferPeerState> = {};\n        activeMembers.forEach((member) => {\n            readyStateByPeerId[member.id] = this.roomState!.readyStateByPeerId[member.id] ?? false;\n            mapTransferStateByPeerId[member.id] = this.roomState!.mapTransferStateByPeerId[member.id] ??\n                (this.roomState!.gameOpts.mapOfficial\n                    ? createTransferState('complete', this.roomState!.gameOpts.mapSizeBytes, this.roomState!.gameOpts.mapSizeBytes)\n                    : member.id === this.meshSession.getSelf().id && this.currentCustomMapFile\n                        ? createTransferState('complete', this.roomState!.gameOpts.mapSizeBytes, this.roomState!.gameOpts.mapSizeBytes)\n                        : createTransferState('pending', this.roomState!.gameOpts.mapSizeBytes, 0));\n        });\n        this.roomState.readyStateByPeerId = readyStateByPeerId;\n        this.roomState.mapTransferStateByPeerId = mapTransferStateByPeerId;\n        this.syncHumanAssignments(activeMembers);\n    }\n\n    private syncHumanAssignments(activeMembers: Array<{ id: string; name: string }>): void {\n        if (!this.roomState) {\n            return;\n        }\n        const state = this.roomState;\n        const activeMemberMap = new Map(activeMembers.map((member) => [member.id, member]));\n        const activePeerIds = new Set(activeMembers.map((member) => member.id));\n        const previousAssignments = state.humanAssignments;\n        const departedHumanSlots = new Set(previousAssignments\n            .filter((assignment) => !activePeerIds.has(assignment.peerId))\n            .map((assignment) => assignment.slotIndex));\n        const previousHumanByPeerId = new Map<string, any>();\n        previousAssignments.forEach((assignment) => {\n            const existingHuman = state.gameOpts.humanPlayers.find((player) => player.name === assignment.name);\n            if (existingHuman) {\n                previousHumanByPeerId.set(assignment.peerId, cloneHumanPlayer(existingHuman));\n            }\n        });\n\n        const nextAssignments: LanHumanAssignment[] = [];\n        const takenSlots = new Set<number>();\n        const visibleSlots = this.computeVisibleSlots(state);\n\n        state.memberOrder.forEach((peerId) => {\n            const member = activeMemberMap.get(peerId);\n            if (!member) {\n                return;\n            }\n            const previousAssignment = previousAssignments.find((candidate) => candidate.peerId === peerId);\n            let slotIndex = previousAssignment?.slotIndex;\n            if (slotIndex === undefined || slotIndex < 0 || slotIndex >= visibleSlots || takenSlots.has(slotIndex)) {\n                slotIndex = this.findNextAssignableSlot(state, takenSlots, visibleSlots, departedHumanSlots);\n            }\n            if (slotIndex === undefined) {\n                return;\n            }\n            takenSlots.add(slotIndex);\n            nextAssignments.push({\n                peerId,\n                slotIndex,\n                name: member.name,\n            });\n        });\n\n        const nextHumans = nextAssignments\n            .slice()\n            .sort((left, right) => left.slotIndex - right.slotIndex)\n            .map((assignment) => previousHumanByPeerId.get(assignment.peerId) ?? createDefaultHumanPlayer(assignment.name, this.gameModes.getById(state.gameOpts.gameMode).mpDialogSettings.mustAlly))\n            .map((player: any, index) => ({\n                ...player,\n                name: nextAssignments.slice().sort((left, right) => left.slotIndex - right.slotIndex)[index].name,\n            }));\n\n        const nextSlotsInfo = cloneSlotsInfo(state.slotsInfo);\n        const nextAiPlayers = state.gameOpts.aiPlayers.map(cloneAiPlayer);\n        for (let slotIndex = 0; slotIndex < nextSlotsInfo.length; slotIndex += 1) {\n            if (slotIndex >= visibleSlots) {\n                nextSlotsInfo[slotIndex] = { type: NetSlotType.Closed };\n                nextAiPlayers[slotIndex] = undefined;\n                continue;\n            }\n            const assignment = nextAssignments.find((candidate) => candidate.slotIndex === slotIndex);\n            if (assignment) {\n                nextSlotsInfo[slotIndex] = {\n                    type: NetSlotType.Player,\n                    name: assignment.name,\n                };\n                nextAiPlayers[slotIndex] = undefined;\n                continue;\n            }\n            if (nextSlotsInfo[slotIndex].type === NetSlotType.Player) {\n                nextSlotsInfo[slotIndex] = { type: NetSlotType.Open };\n                nextAiPlayers[slotIndex] = undefined;\n                continue;\n            }\n            if (nextSlotsInfo[slotIndex].type !== NetSlotType.Ai) {\n                nextAiPlayers[slotIndex] = undefined;\n            }\n        }\n\n        state.humanAssignments = nextAssignments;\n        state.gameOpts.humanPlayers = nextHumans;\n        state.gameOpts.aiPlayers = nextAiPlayers;\n        state.slotsInfo = nextSlotsInfo;\n    }\n\n    private computeVisibleSlots(state: LanRoomState): number {\n        const observerActive = state.gameOpts.humanPlayers[0]?.countryId === OBS_COUNTRY_ID;\n        return observerActive ? state.gameOpts.maxSlots + 1 : state.gameOpts.maxSlots;\n    }\n\n    private findNextAssignableSlot(state: LanRoomState, takenSlots: Set<number>, visibleSlots: number, departedHumanSlots: Set<number>): number | undefined {\n        for (let slotIndex = 0; slotIndex < visibleSlots; slotIndex += 1) {\n            if (takenSlots.has(slotIndex)) {\n                continue;\n            }\n            if (state.slotsInfo[slotIndex]?.type === NetSlotType.Open) {\n                return slotIndex;\n            }\n        }\n        for (let slotIndex = 0; slotIndex < visibleSlots; slotIndex += 1) {\n            if (takenSlots.has(slotIndex)) {\n                continue;\n            }\n            if (departedHumanSlots.has(slotIndex)) {\n                return slotIndex;\n            }\n        }\n        for (let slotIndex = 0; slotIndex < visibleSlots; slotIndex += 1) {\n            if (takenSlots.has(slotIndex)) {\n                continue;\n            }\n            if (state.slotsInfo[slotIndex]?.type === NetSlotType.Ai) {\n                return slotIndex;\n            }\n        }\n        for (let slotIndex = 0; slotIndex < visibleSlots; slotIndex += 1) {\n            if (!takenSlots.has(slotIndex) && state.slotsInfo[slotIndex]?.type === NetSlotType.Closed) {\n                return slotIndex;\n            }\n        }\n        return undefined;\n    }\n\n    private findNextHostPeerId(currentHostPeerId: string): string | undefined {\n        if (!this.roomState) {\n            return undefined;\n        }\n        return this.roomState.memberOrder.find((peerId) => peerId !== currentHostPeerId && this.lastMeshSnapshot.members.some((member) => member.id === peerId));\n    }\n\n    private broadcastStateSync(): void {\n        if (!this.roomState) {\n            return;\n        }\n        this.meshSession.broadcastAppMessage({\n            type: 'state-sync',\n            state: cloneRoomState(this.roomState),\n        } satisfies LanRoomMessage);\n    }\n\n    private scheduleCustomMapTransfers(): void {\n        if (!this.roomState || !this.isHost() || this.roomState.gameOpts.mapOfficial || !this.currentCustomMapFile) {\n            return;\n        }\n        this.lastMeshSnapshot.members\n            .filter((member) => !member.isSelf && member.status === 'connected')\n            .forEach((member) => {\n                const transferState = this.roomState!.mapTransferStateByPeerId[member.id];\n                if (transferState?.status === 'complete' || transferState?.status === 'sending') {\n                    return;\n                }\n                void this.sendMapToPeer(member.id);\n            });\n    }\n\n    private async sendMapToPeer(peerId: string): Promise<void> {\n        if (!this.roomState || !this.currentCustomMapFile || !this.isHost()) {\n            return;\n        }\n        const bytes = this.currentCustomMapFile.getBytes();\n        const base64 = uint8ArrayToBase64String(bytes);\n        const chunks: string[] = [];\n        for (let offset = 0; offset < base64.length; offset += MAP_CHUNK_SIZE) {\n            chunks.push(base64.slice(offset, offset + MAP_CHUNK_SIZE));\n        }\n        this.roomState.mapTransferStateByPeerId[peerId] = createTransferState('sending', this.roomState.gameOpts.mapSizeBytes, 0);\n        this.broadcastStateSync();\n        this.meshSession.sendAppMessage(peerId, {\n            type: 'map-offer',\n            peerId,\n            filename: this.roomState.gameOpts.mapName,\n            digest: this.roomState.gameOpts.mapDigest,\n            sizeBytes: this.roomState.gameOpts.mapSizeBytes,\n            totalChunks: chunks.length,\n        } satisfies LanRoomMessage);\n        for (let index = 0; index < chunks.length; index += 1) {\n            this.meshSession.sendAppMessage(peerId, {\n                type: 'map-chunk',\n                peerId,\n                digest: this.roomState.gameOpts.mapDigest,\n                index,\n                totalChunks: chunks.length,\n                data: chunks[index],\n            } satisfies LanRoomMessage);\n            this.roomState.mapTransferStateByPeerId[peerId] = createTransferState(\n                'sending',\n                this.roomState.gameOpts.mapSizeBytes,\n                Math.min(this.roomState.gameOpts.mapSizeBytes, Math.floor(((index + 1) / chunks.length) * this.roomState.gameOpts.mapSizeBytes))\n            );\n            this.broadcastStateSync();\n            await Promise.resolve();\n        }\n    }\n\n    private async ensureLocalCustomMapIfNeeded(): Promise<void> {\n        if (!this.roomState || this.roomState.gameOpts.mapOfficial || this.currentCustomMapFile) {\n            return;\n        }\n        try {\n            const localFile = await this.mapFileLoader.load(this.roomState.gameOpts.mapName);\n            const localVirtualFile = VirtualFile.fromBytes(localFile.getBytes(), this.roomState.gameOpts.mapName);\n            if (MapDigest.compute(localVirtualFile) === this.roomState.gameOpts.mapDigest) {\n                this.currentCustomMapFile = localVirtualFile;\n                this.updateLocalMapTransferState('complete', this.roomState.gameOpts.mapSizeBytes, this.roomState.gameOpts.mapSizeBytes);\n                if (!this.isHost()) {\n                    this.meshSession.sendAppMessage(this.roomState.hostPeerId, {\n                        type: 'map-complete',\n                        peerId: this.meshSession.getSelf().id,\n                        digest: this.roomState.gameOpts.mapDigest,\n                        ok: true,\n                    } satisfies LanRoomMessage);\n                }\n                this.dispatchSnapshot();\n            }\n        }\n        catch {\n        }\n    }\n\n    private updateLocalMapTransferState(status: LanMapTransferPeerState['status'], totalBytes?: number, receivedBytes?: number, error?: string): void {\n        if (!this.roomState) {\n            return;\n        }\n        const selfPeerId = this.meshSession.getSelf().id;\n        this.roomState.mapTransferStateByPeerId[selfPeerId] = createTransferState(status, totalBytes, receivedBytes, error);\n        if (!this.isHost()) {\n            this.dispatchSnapshot();\n        }\n    }\n\n    private async persistCustomMap(file: VirtualFile): Promise<void> {\n        if (!this.mapDir) {\n            return;\n        }\n        if (!(await this.mapDir.containsEntry(file.filename))) {\n            await this.mapDir.writeFile(file);\n            this.mapList?.addFromMapFile(file);\n        }\n    }\n\n    private isHost(): boolean {\n        return this.roomState?.hostPeerId === this.meshSession.getSelf().id;\n    }\n\n    private canStart(): boolean {\n        if (!this.roomState || !this.isHost()) {\n            return false;\n        }\n        if (this.roomState.humanAssignments.length < 2) {\n            return false;\n        }\n        if (this.roomState.humanAssignments.length !== this.lastMeshSnapshot.members.length) {\n            return false;\n        }\n        const connectedMembers = this.lastMeshSnapshot.members.filter((member) => member.isSelf || member.status === 'connected');\n        if (connectedMembers.length !== this.lastMeshSnapshot.members.length) {\n            return false;\n        }\n        if (!this.roomState.gameOpts.mapOfficial) {\n            return this.lastMeshSnapshot.members.every((member) => this.roomState!.mapTransferStateByPeerId[member.id]?.status === 'complete');\n        }\n        return true;\n    }\n\n    private canInvite(): boolean {\n        if (!this.roomState || !this.lastMeshSnapshot.isInRoom) {\n            return false;\n        }\n        const visibleSlots = this.computeVisibleSlots(this.roomState);\n        const occupiedSlots = new Set(this.roomState.humanAssignments.map((assignment) => assignment.slotIndex));\n        for (let slotIndex = 0; slotIndex < visibleSlots; slotIndex += 1) {\n            if (occupiedSlots.has(slotIndex)) {\n                continue;\n            }\n            const slotType = this.roomState.slotsInfo[slotIndex]?.type;\n            if (slotType === NetSlotType.Open || slotType === NetSlotType.OpenObserver) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private createSnapshot(): LanRoomSnapshot {\n        const roomState = this.roomState ? cloneRoomState(this.roomState) : undefined;\n        const hostPeerId = roomState?.hostPeerId;\n        const members = this.lastMeshSnapshot.members.map((member) => {\n            const assignment = roomState?.humanAssignments.find((candidate) => candidate.peerId === member.id);\n            return {\n                peerId: member.id,\n                name: member.name,\n                isSelf: member.isSelf,\n                isHost: hostPeerId === member.id,\n                isConnected: member.isSelf || member.status === 'connected',\n                slotIndex: assignment?.slotIndex,\n                ready: roomState?.readyStateByPeerId[member.id] ?? false,\n                mapTransfer: roomState?.mapTransferStateByPeerId[member.id] ?? createTransferState('idle'),\n            };\n        });\n\n        return {\n            self: this.meshSession.getSelf(),\n            mesh: this.lastMeshSnapshot,\n            isRoomActive: Boolean(roomState),\n            isHost: Boolean(hostPeerId && hostPeerId === this.meshSession.getSelf().id),\n            hostPeerId,\n            roomState,\n            members,\n            localMapFileReady: roomState ? roomState.gameOpts.mapOfficial || Boolean(this.currentCustomMapFile) : false,\n            canInvite: this.canInvite(),\n            canStart: this.canStart(),\n            launchDescriptor: this.launchDescriptor,\n        };\n    }\n\n    private dispatchSnapshot(): void {\n        this.onSnapshotChange.dispatch(this, this.createSnapshot());\n    }\n\n    private log(level: 'info' | 'warn' | 'error', text: string): void {\n        this.onLog.dispatch(this, {\n            level,\n            text,\n            timestamp: Date.now(),\n        });\n    }\n}\n"
  },
  {
    "path": "src/network/lan/ManualSdpLanSession.ts",
    "content": "import { EventDispatcher } from '@/util/event';\nimport { formatSdpCandidateSummary, getSdpCandidateWarning, summarizeSdpCandidates } from './SdpCandidateDiagnostics';\n\nexport type ManualLanRole = 'host' | 'guest';\n\nexport interface ManualLanSnapshot {\n    role?: ManualLanRole;\n    localDescriptionText: string;\n    remoteDescriptionApplied: boolean;\n    connectionState: RTCPeerConnectionState;\n    iceConnectionState: RTCIceConnectionState;\n    iceGatheringState: RTCIceGatheringState;\n    signalingState: RTCSignalingState;\n    channelState: RTCDataChannelState | 'closed';\n}\n\nexport interface ManualLanLogEntry {\n    level: 'info' | 'warn' | 'error';\n    text: string;\n    timestamp: number;\n}\n\nexport interface ManualLanMessage {\n    from: string;\n    text: string;\n    timestamp: number;\n}\n\ninterface ManualLanEnvelope {\n    type: 'hello' | 'chat';\n    role?: ManualLanRole;\n    text?: string;\n    timestamp?: number;\n}\n\nconst ICE_GATHER_TIMEOUT_MILLIS = 10000;\n\nfunction createDefaultSnapshot(role?: ManualLanRole): ManualLanSnapshot {\n    return {\n        role,\n        localDescriptionText: '',\n        remoteDescriptionApplied: false,\n        connectionState: 'closed',\n        iceConnectionState: 'closed',\n        iceGatheringState: 'new',\n        signalingState: 'stable',\n        channelState: 'closed',\n    };\n}\n\nexport class ManualSdpLanSession {\n    private peerConnection?: RTCPeerConnection;\n    private dataChannel?: RTCDataChannel;\n    private snapshot: ManualLanSnapshot = createDefaultSnapshot();\n\n    public readonly onSnapshotChange = new EventDispatcher<this, ManualLanSnapshot>();\n    public readonly onLog = new EventDispatcher<this, ManualLanLogEntry>();\n    public readonly onMessage = new EventDispatcher<this, ManualLanMessage>();\n\n    getSnapshot(): ManualLanSnapshot {\n        return { ...this.snapshot };\n    }\n\n    reset(role?: ManualLanRole): void {\n        this.closePeer();\n        this.snapshot = createDefaultSnapshot(role);\n        this.dispatchSnapshot();\n    }\n\n    async createHostOffer(): Promise<string> {\n        this.reset('host');\n\n        const pc = this.createPeerConnection('host');\n        const channel = pc.createDataChannel('ra2-lan-manual', {\n            ordered: true,\n        });\n        this.attachDataChannel(channel, 'local');\n\n        this.log('info', '正在生成房主 Offer...');\n        await pc.setLocalDescription(await pc.createOffer());\n        await this.waitForIceGatheringComplete(pc);\n        this.logLocalDescriptionDiagnostics('房主 Offer');\n        this.refreshSnapshot();\n        this.log('info', '房主 Offer 已生成，可以复制给加入者。');\n        return this.snapshot.localDescriptionText;\n    }\n\n    async acceptHostOffer(offerText: string): Promise<string> {\n        this.reset('guest');\n\n        const pc = this.createPeerConnection('guest');\n        pc.ondatachannel = (event) => {\n            if (pc !== this.peerConnection) {\n                return;\n            }\n            this.attachDataChannel(event.channel, 'remote');\n        };\n\n        const offer = this.parseDescription(offerText, 'offer');\n        this.log('info', '正在导入房主 Offer...');\n        await pc.setRemoteDescription(offer);\n        this.snapshot.remoteDescriptionApplied = true;\n        this.refreshSnapshot();\n\n        this.log('info', '正在生成加入者 Answer...');\n        await pc.setLocalDescription(await pc.createAnswer());\n        await this.waitForIceGatheringComplete(pc);\n        this.logLocalDescriptionDiagnostics('加入者 Answer');\n        this.refreshSnapshot();\n        this.log('info', '加入者 Answer 已生成，可以复制回房主。');\n        return this.snapshot.localDescriptionText;\n    }\n\n    async acceptGuestAnswer(answerText: string): Promise<void> {\n        if (!this.peerConnection || this.snapshot.role !== 'host') {\n            throw new Error('请先在房主模式下生成 Offer。');\n        }\n\n        const answer = this.parseDescription(answerText, 'answer');\n        this.log('info', '正在导入加入者 Answer...');\n        await this.peerConnection.setRemoteDescription(answer);\n        this.snapshot.remoteDescriptionApplied = true;\n        this.refreshSnapshot();\n        this.log('info', 'Answer 已导入，等待数据通道建立。');\n    }\n\n    sendChat(text: string): void {\n        const normalizedText = text.trim();\n        if (!normalizedText) {\n            return;\n        }\n        if (!this.dataChannel || this.dataChannel.readyState !== 'open') {\n            throw new Error('数据通道尚未建立，暂时无法发送消息。');\n        }\n        this.dataChannel.send(JSON.stringify({\n            type: 'chat',\n            text: normalizedText,\n            timestamp: Date.now(),\n        } satisfies ManualLanEnvelope));\n    }\n\n    dispose(): void {\n        this.reset();\n    }\n\n    private createPeerConnection(role: ManualLanRole): RTCPeerConnection {\n        if (typeof RTCPeerConnection === 'undefined') {\n            throw new Error('当前浏览器不支持 WebRTC。');\n        }\n        const pc = new RTCPeerConnection({\n            iceServers: [],\n        });\n        this.peerConnection = pc;\n        this.snapshot = createDefaultSnapshot(role);\n        this.bindPeerEvents(pc);\n        this.refreshSnapshot();\n        return pc;\n    }\n\n    private bindPeerEvents(pc: RTCPeerConnection): void {\n        pc.addEventListener('connectionstatechange', () => {\n            if (pc !== this.peerConnection) {\n                return;\n            }\n            this.refreshSnapshot();\n            this.log('info', `连接状态已更新为 ${pc.connectionState}。`);\n        });\n        pc.addEventListener('iceconnectionstatechange', () => {\n            if (pc !== this.peerConnection) {\n                return;\n            }\n            this.refreshSnapshot();\n            this.log('info', `ICE 连接状态已更新为 ${pc.iceConnectionState}。`);\n        });\n        pc.addEventListener('icegatheringstatechange', () => {\n            if (pc !== this.peerConnection) {\n                return;\n            }\n            this.refreshSnapshot();\n        });\n        pc.addEventListener('icecandidateerror', (event) => {\n            if (pc !== this.peerConnection) {\n                return;\n            }\n            const address = 'address' in event && typeof event.address === 'string' ? ` ${event.address}` : '';\n            this.log('warn', `ICE 候选采集报错${address}：${event.errorText || 'unknown error'}。`);\n        });\n        pc.addEventListener('signalingstatechange', () => {\n            if (pc !== this.peerConnection) {\n                return;\n            }\n            this.refreshSnapshot();\n        });\n    }\n\n    private attachDataChannel(channel: RTCDataChannel, source: 'local' | 'remote'): void {\n        this.dataChannel = channel;\n        channel.binaryType = 'arraybuffer';\n        this.log('info', `${source === 'local' ? '本地' : '远端'}数据通道已创建。`);\n        channel.addEventListener('open', () => {\n            if (channel !== this.dataChannel) {\n                return;\n            }\n            this.refreshSnapshot();\n            this.log('info', '数据通道已打开，可以开始发送测试消息。');\n            try {\n                this.sendEnvelope({\n                    type: 'hello',\n                    role: this.snapshot.role,\n                    timestamp: Date.now(),\n                });\n            }\n            catch (error) {\n                this.log('warn', `发送握手消息失败：${(error as Error).message}`);\n            }\n        });\n        channel.addEventListener('close', () => {\n            if (channel !== this.dataChannel) {\n                return;\n            }\n            this.refreshSnapshot();\n            this.log('warn', '数据通道已关闭。');\n        });\n        channel.addEventListener('error', () => {\n            if (channel !== this.dataChannel) {\n                return;\n            }\n            this.refreshSnapshot();\n            this.log('error', '数据通道发生错误。');\n        });\n        channel.addEventListener('message', (event) => {\n            if (channel !== this.dataChannel) {\n                return;\n            }\n            this.handleDataChannelMessage(event.data);\n        });\n        this.refreshSnapshot();\n    }\n\n    private handleDataChannelMessage(data: string | ArrayBuffer | Blob): void {\n        if (typeof data === 'string') {\n            this.handleEnvelopeText(data);\n            return;\n        }\n        if (data instanceof ArrayBuffer) {\n            this.handleEnvelopeText(new TextDecoder().decode(new Uint8Array(data)));\n            return;\n        }\n        if (typeof Blob !== 'undefined' && data instanceof Blob) {\n            data.text()\n                .then((text) => this.handleEnvelopeText(text))\n                .catch((error) => this.log('warn', `读取远端消息失败：${(error as Error).message}`));\n        }\n    }\n\n    private handleEnvelopeText(text: string): void {\n        let payload: ManualLanEnvelope | undefined;\n        try {\n            payload = JSON.parse(text) as ManualLanEnvelope;\n        }\n        catch {\n            payload = undefined;\n        }\n\n        if (!payload || typeof payload !== 'object') {\n            this.onMessage.dispatch(this, {\n                from: this.getRemoteLabel(),\n                text,\n                timestamp: Date.now(),\n            });\n            return;\n        }\n\n        if (payload.type === 'hello') {\n            this.log('info', `${this.getRemoteLabel()}已完成握手。`);\n            return;\n        }\n\n        if (payload.type === 'chat' && payload.text) {\n            this.onMessage.dispatch(this, {\n                from: this.getRemoteLabel(),\n                text: payload.text,\n                timestamp: payload.timestamp ?? Date.now(),\n            });\n            return;\n        }\n\n        this.log('warn', '收到了无法识别的数据通道消息。');\n    }\n\n    private sendEnvelope(payload: ManualLanEnvelope): void {\n        if (!this.dataChannel || this.dataChannel.readyState !== 'open') {\n            throw new Error('数据通道尚未打开。');\n        }\n        this.dataChannel.send(JSON.stringify(payload));\n    }\n\n    private parseDescription(text: string, expectedType: RTCSdpType): RTCSessionDescriptionInit {\n        let parsed: unknown;\n        try {\n            parsed = JSON.parse(text.trim());\n        }\n        catch {\n            throw new Error('描述文本不是合法的 JSON。');\n        }\n\n        if (!parsed || typeof parsed !== 'object') {\n            throw new Error('描述文本格式不正确。');\n        }\n\n        const candidate = parsed as RTCSessionDescriptionInit;\n        if (candidate.type !== expectedType) {\n            throw new Error(`需要导入 ${expectedType}，但当前文本类型是 ${candidate.type ?? 'unknown'}。`);\n        }\n        if (!candidate.sdp || typeof candidate.sdp !== 'string') {\n            throw new Error('描述文本缺少 SDP 内容。');\n        }\n        return candidate;\n    }\n\n    private async waitForIceGatheringComplete(pc: RTCPeerConnection): Promise<void> {\n        if (pc.iceGatheringState === 'complete') {\n            return;\n        }\n\n        await new Promise<void>((resolve, reject) => {\n            const timeoutId = window.setTimeout(() => {\n                cleanup();\n                reject(new Error('ICE 候选收集超时，请稍后重试。'));\n            }, ICE_GATHER_TIMEOUT_MILLIS);\n\n            const handleChange = () => {\n                if (pc.iceGatheringState === 'complete') {\n                    cleanup();\n                    resolve();\n                }\n            };\n\n            const cleanup = () => {\n                clearTimeout(timeoutId);\n                pc.removeEventListener('icegatheringstatechange', handleChange);\n            };\n\n            pc.addEventListener('icegatheringstatechange', handleChange);\n        });\n    }\n\n    private closePeer(): void {\n        try {\n            this.dataChannel?.close();\n        }\n        catch {\n        }\n        this.dataChannel = undefined;\n\n        try {\n            this.peerConnection?.close();\n        }\n        catch {\n        }\n        this.peerConnection = undefined;\n    }\n\n    private refreshSnapshot(): void {\n        this.snapshot = {\n            role: this.snapshot.role,\n            localDescriptionText: this.peerConnection?.localDescription\n                ? JSON.stringify(this.peerConnection.localDescription)\n                : this.snapshot.localDescriptionText,\n            remoteDescriptionApplied: this.snapshot.remoteDescriptionApplied,\n            connectionState: this.peerConnection?.connectionState ?? 'closed',\n            iceConnectionState: this.peerConnection?.iceConnectionState ?? 'closed',\n            iceGatheringState: this.peerConnection?.iceGatheringState ?? 'new',\n            signalingState: this.peerConnection?.signalingState ?? 'stable',\n            channelState: this.dataChannel?.readyState ?? 'closed',\n        };\n        this.dispatchSnapshot();\n    }\n\n    private logLocalDescriptionDiagnostics(label: string): void {\n        const summary = summarizeSdpCandidates(this.peerConnection?.localDescription);\n        this.log('info', `${label} 候选情况：${formatSdpCandidateSummary(summary)}。`);\n        const warning = getSdpCandidateWarning(summary);\n        if (warning) {\n            this.log('warn', warning);\n        }\n    }\n\n    private dispatchSnapshot(): void {\n        this.onSnapshotChange.dispatch(this, { ...this.snapshot });\n    }\n\n    private getRemoteLabel(): string {\n        return this.snapshot.role === 'host' ? '加入者' : '房主';\n    }\n\n    private log(level: ManualLanLogEntry['level'], text: string): void {\n        this.onLog.dispatch(this, {\n            level,\n            text,\n            timestamp: Date.now(),\n        });\n    }\n}\n"
  },
  {
    "path": "src/network/lan/SdpCandidateDiagnostics.ts",
    "content": "export interface SdpCandidateSummary {\n    totalCandidates: number;\n    hasMdnsHostCandidate: boolean;\n    hasPrivateIpv4Candidate: boolean;\n    hasLoopbackCandidate: boolean;\n    hasIpv6Candidate: boolean;\n    hasSrflxCandidate: boolean;\n    hasRelayCandidate: boolean;\n}\n\nfunction isPrivateIpv4(address: string): boolean {\n    return /^10\\./.test(address) ||\n        /^192\\.168\\./.test(address) ||\n        /^172\\.(1[6-9]|2\\d|3[01])\\./.test(address);\n}\n\nfunction isIpv6(address: string): boolean {\n    return address.includes(':');\n}\n\nfunction parseCandidateLine(line: string): { address?: string; type?: string } {\n    const tokens = line.trim().split(/\\s+/);\n    const typIndex = tokens.indexOf('typ');\n    return {\n        address: tokens[4],\n        type: typIndex >= 0 ? tokens[typIndex + 1] : undefined,\n    };\n}\n\nexport function summarizeSdpCandidates(description?: RTCSessionDescriptionInit | null): SdpCandidateSummary {\n    const summary: SdpCandidateSummary = {\n        totalCandidates: 0,\n        hasMdnsHostCandidate: false,\n        hasPrivateIpv4Candidate: false,\n        hasLoopbackCandidate: false,\n        hasIpv6Candidate: false,\n        hasSrflxCandidate: false,\n        hasRelayCandidate: false,\n    };\n\n    if (!description?.sdp) {\n        return summary;\n    }\n\n    description.sdp\n        .split(/\\r?\\n/)\n        .filter((line) => line.startsWith('a=candidate:'))\n        .forEach((line) => {\n            summary.totalCandidates += 1;\n            const { address = '', type } = parseCandidateLine(line);\n            const normalizedAddress = address.toLowerCase();\n            if (normalizedAddress.endsWith('.local')) {\n                summary.hasMdnsHostCandidate = true;\n            }\n            if (isPrivateIpv4(normalizedAddress)) {\n                summary.hasPrivateIpv4Candidate = true;\n            }\n            if (normalizedAddress === '127.0.0.1' || normalizedAddress === '::1' || normalizedAddress === 'localhost') {\n                summary.hasLoopbackCandidate = true;\n            }\n            if (isIpv6(normalizedAddress)) {\n                summary.hasIpv6Candidate = true;\n            }\n            if (type === 'srflx') {\n                summary.hasSrflxCandidate = true;\n            }\n            if (type === 'relay') {\n                summary.hasRelayCandidate = true;\n            }\n        });\n\n    return summary;\n}\n\nexport function formatSdpCandidateSummary(summary: SdpCandidateSummary): string {\n    const parts = [\n        `候选 ${summary.totalCandidates} 个`,\n        summary.hasPrivateIpv4Candidate ? '含局域网 IPv4' : '无局域网 IPv4',\n        summary.hasMdnsHostCandidate ? '含 mDNS 主机名' : '无 mDNS 主机名',\n        summary.hasSrflxCandidate ? '含 srflx' : '无 srflx',\n        summary.hasRelayCandidate ? '含 relay' : '无 relay',\n    ];\n    return parts.join('，');\n}\n\nexport function getSdpCandidateWarning(summary: SdpCandidateSummary): string | undefined {\n    if (!summary.totalCandidates) {\n        return '当前 SDP 没有收集到任何 ICE 候选，跨机器肯定无法建立局域网直连。';\n    }\n    if (summary.hasMdnsHostCandidate &&\n        !summary.hasPrivateIpv4Candidate &&\n        !summary.hasSrflxCandidate &&\n        !summary.hasRelayCandidate) {\n        return '当前浏览器只暴露了 mDNS host candidate（*.local），没有局域网 IPv4/srflx/relay 候选；同机或 127.0.0.1 往往可用，但跨机器局域网很容易因为 mDNS/UDP 被拦而失败。';\n    }\n    return undefined;\n}\n"
  },
  {
    "path": "src/performance/PerformanceOptions.ts",
    "content": "import { BoxedVar } from '@/util/BoxedVar';\n\nexport const performanceOptionKeys = [\n    'raycastHelperReuse',\n    'entityIntersectTraversal',\n    'mapTileHitTest',\n    'worldViewportCache',\n    'worldSoundLoopCache',\n    'telemetry',\n] as const;\n\nexport type PerformanceOptionKey = typeof performanceOptionKeys[number];\n\nexport type PerformanceOptionSnapshot = Record<PerformanceOptionKey, boolean>;\n\nexport interface PerformanceOptionVars {\n    raycastHelperReuse: BoxedVar<boolean>;\n    entityIntersectTraversal: BoxedVar<boolean>;\n    mapTileHitTest: BoxedVar<boolean>;\n    worldViewportCache: BoxedVar<boolean>;\n    worldSoundLoopCache: BoxedVar<boolean>;\n    telemetry: BoxedVar<boolean>;\n}\n\nexport const defaultPerformanceOptionValues: PerformanceOptionSnapshot = {\n    raycastHelperReuse: true,\n    entityIntersectTraversal: true,\n    mapTileHitTest: true,\n    worldViewportCache: true,\n    worldSoundLoopCache: true,\n    telemetry: false,\n};\n\nexport function snapshotPerformanceOptions(options: PerformanceOptionVars): PerformanceOptionSnapshot {\n    return performanceOptionKeys.reduce((snapshot, key) => {\n        snapshot[key] = options[key].value;\n        return snapshot;\n    }, {} as PerformanceOptionSnapshot);\n}\n\nexport function createPerformanceOptionVars(initialValues: Partial<PerformanceOptionSnapshot> = {}): PerformanceOptionVars {\n    const values = {\n        ...defaultPerformanceOptionValues,\n        ...initialValues,\n    };\n    return {\n        raycastHelperReuse: new BoxedVar<boolean>(values.raycastHelperReuse),\n        entityIntersectTraversal: new BoxedVar<boolean>(values.entityIntersectTraversal),\n        mapTileHitTest: new BoxedVar<boolean>(values.mapTileHitTest),\n        worldViewportCache: new BoxedVar<boolean>(values.worldViewportCache),\n        worldSoundLoopCache: new BoxedVar<boolean>(values.worldSoundLoopCache),\n        telemetry: new BoxedVar<boolean>(values.telemetry),\n    };\n}\n\nexport function serializePerformanceOptions(options: PerformanceOptionVars): string {\n    return performanceOptionKeys\n        .map((key) => Number(options[key].value))\n        .join('');\n}\n\nexport function unserializePerformanceOptions(options: PerformanceOptionVars, serializedValue: string | undefined): void {\n    if (!serializedValue) {\n        return;\n    }\n    performanceOptionKeys.forEach((key, index) => {\n        const encodedValue = serializedValue[index];\n        if (encodedValue === undefined) {\n            return;\n        }\n        options[key].value = encodedValue === '1';\n    });\n}\n\nexport class PerformanceOptions implements PerformanceOptionVars {\n    raycastHelperReuse: BoxedVar<boolean>;\n    entityIntersectTraversal: BoxedVar<boolean>;\n    mapTileHitTest: BoxedVar<boolean>;\n    worldViewportCache: BoxedVar<boolean>;\n    worldSoundLoopCache: BoxedVar<boolean>;\n    telemetry: BoxedVar<boolean>;\n\n    constructor(initialValues: Partial<PerformanceOptionSnapshot> = {}) {\n        const options = createPerformanceOptionVars(initialValues);\n        this.raycastHelperReuse = options.raycastHelperReuse;\n        this.entityIntersectTraversal = options.entityIntersectTraversal;\n        this.mapTileHitTest = options.mapTileHitTest;\n        this.worldViewportCache = options.worldViewportCache;\n        this.worldSoundLoopCache = options.worldSoundLoopCache;\n        this.telemetry = options.telemetry;\n    }\n\n    serialize(): string {\n        return serializePerformanceOptions(this);\n    }\n\n    unserialize(serializedValue: string | undefined): this {\n        unserializePerformanceOptions(this, serializedValue);\n        return this;\n    }\n\n    snapshot(): PerformanceOptionSnapshot {\n        return snapshotPerformanceOptions(this);\n    }\n}\n"
  },
  {
    "path": "src/performance/PerformanceRuntime.ts",
    "content": "import { PerformanceOptions, type PerformanceOptionKey, type PerformanceOptionSnapshot, type PerformanceOptionVars, snapshotPerformanceOptions } from '@/performance/PerformanceOptions';\n\ntype FrameMetricKind = 'ui' | 'game';\n\ninterface FrameMetricState {\n    lastTimestamp?: number;\n    averageMs?: number;\n    fps: number | null;\n    frameMs: number | null;\n    lastSampleAt: number;\n}\n\ninterface PerformanceMetricState {\n    calls: number;\n    totalMs: number;\n}\n\nexport interface PerformanceMetricSnapshot {\n    calls: number;\n    totalMs: number;\n    avgMs: number;\n}\n\nexport interface PerformanceTelemetrySnapshot {\n    enabled: boolean;\n    options: PerformanceOptionSnapshot;\n    uiFps: number | null;\n    uiFrameMs: number | null;\n    gameFps: number | null;\n    gameFrameMs: number | null;\n    metrics: Record<string, PerformanceMetricSnapshot>;\n    updatedAt: number;\n}\n\nconst createFrameMetricState = (): FrameMetricState => ({\n    fps: null,\n    frameMs: null,\n    lastSampleAt: 0,\n});\n\nclass PerformanceTelemetry {\n    private readonly metrics = new Map<string, PerformanceMetricState>();\n    private readonly uiFrame = createFrameMetricState();\n    private readonly gameFrame = createFrameMetricState();\n\n    constructor(private readonly isEnabled: () => boolean) {\n    }\n\n    reset(): void {\n        this.metrics.clear();\n        this.resetFrames();\n    }\n\n    resetMetrics(): void {\n        this.metrics.clear();\n    }\n\n    private resetFrames(): void {\n        this.uiFrame.lastTimestamp = undefined;\n        this.uiFrame.averageMs = undefined;\n        this.uiFrame.fps = null;\n        this.uiFrame.frameMs = null;\n        this.uiFrame.lastSampleAt = 0;\n        this.gameFrame.lastTimestamp = undefined;\n        this.gameFrame.averageMs = undefined;\n        this.gameFrame.fps = null;\n        this.gameFrame.frameMs = null;\n        this.gameFrame.lastSampleAt = 0;\n    }\n\n    recordFrame(kind: FrameMetricKind, timestamp: number): void {\n        if (!this.isEnabled()) {\n            return;\n        }\n        const target = kind === 'ui' ? this.uiFrame : this.gameFrame;\n        if (target.lastTimestamp !== undefined) {\n            const delta = timestamp - target.lastTimestamp;\n            if (delta > 0) {\n                if (delta > 1200) {\n                    target.averageMs = undefined;\n                    target.fps = null;\n                    target.frameMs = null;\n                }\n                else {\n                    const smoothing = delta > 200 ? 0.2 : 0.1;\n                    target.averageMs = target.averageMs === undefined\n                        ? delta\n                        : target.averageMs + (delta - target.averageMs) * smoothing;\n                    target.frameMs = target.averageMs;\n                    target.fps = target.averageMs > 0 ? 1000 / target.averageMs : null;\n                }\n            }\n        }\n        target.lastTimestamp = timestamp;\n        target.lastSampleAt = this.now();\n    }\n\n    measure<T>(metricName: string, callback: () => T): T {\n        if (!this.isEnabled()) {\n            return callback();\n        }\n        const start = this.now();\n        try {\n            return callback();\n        }\n        finally {\n            this.recordMetric(metricName, this.now() - start);\n        }\n    }\n\n    async measureAsync<T>(metricName: string, callback: () => Promise<T>): Promise<T> {\n        if (!this.isEnabled()) {\n            return callback();\n        }\n        const start = this.now();\n        try {\n            return await callback();\n        }\n        finally {\n            this.recordMetric(metricName, this.now() - start);\n        }\n    }\n\n    snapshot(options: PerformanceOptionVars): PerformanceTelemetrySnapshot {\n        const metrics = Array.from(this.metrics.entries()).reduce((acc, [key, metric]) => {\n            acc[key] = {\n                calls: metric.calls,\n                totalMs: metric.totalMs,\n                avgMs: metric.calls ? metric.totalMs / metric.calls : 0,\n            };\n            return acc;\n        }, {} as Record<string, PerformanceMetricSnapshot>);\n\n        return {\n            enabled: this.isEnabled(),\n            options: snapshotPerformanceOptions(options),\n            uiFps: this.uiFrame.fps,\n            uiFrameMs: this.uiFrame.frameMs,\n            gameFps: this.gameFrame.fps,\n            gameFrameMs: this.gameFrame.frameMs,\n            metrics,\n            updatedAt: Date.now(),\n        };\n    }\n\n    private recordMetric(metricName: string, elapsedMs: number): void {\n        const metric = this.metrics.get(metricName) ?? { calls: 0, totalMs: 0 };\n        metric.calls += 1;\n        metric.totalMs += elapsedMs;\n        this.metrics.set(metricName, metric);\n    }\n\n    private now(): number {\n        if (typeof performance !== 'undefined' && typeof performance.now === 'function') {\n            return performance.now();\n        }\n        return Date.now();\n    }\n}\n\nlet performanceOptions: PerformanceOptionVars = new PerformanceOptions();\n\nconst telemetry = new PerformanceTelemetry(() => performanceOptions.telemetry.value);\n\nexport function attachPerformanceOptions(options: PerformanceOptionVars): void {\n    performanceOptions = options;\n}\n\nexport function getPerformanceOptions(): PerformanceOptionVars {\n    return performanceOptions;\n}\n\nexport function snapshotPerformanceConfig(): PerformanceOptionSnapshot {\n    return snapshotPerformanceOptions(performanceOptions);\n}\n\nexport function isPerformanceFeatureEnabled(feature: Exclude<PerformanceOptionKey, 'telemetry'>): boolean {\n    return performanceOptions[feature].value;\n}\n\nexport function measurePerformanceFeature<T>(feature: Exclude<PerformanceOptionKey, 'telemetry'>, callback: () => T): T {\n    return telemetry.measure(feature, callback);\n}\n\nexport function measurePerformanceMetric<T>(metricName: string, callback: () => T): T {\n    return telemetry.measure(metricName, callback);\n}\n\nexport async function measurePerformanceMetricAsync<T>(metricName: string, callback: () => Promise<T>): Promise<T> {\n    return telemetry.measureAsync(metricName, callback);\n}\n\nexport function recordUiPerformanceFrame(timestamp: number): void {\n    telemetry.recordFrame('ui', timestamp);\n}\n\nexport function recordGamePerformanceFrame(timestamp: number): void {\n    telemetry.recordFrame('game', timestamp);\n}\n\nexport function resetPerformanceTelemetry(): void {\n    telemetry.reset();\n}\n\nexport function resetPerformanceMetricSamples(): void {\n    telemetry.resetMetrics();\n}\n\nexport function snapshotPerformanceTelemetry(): PerformanceTelemetrySnapshot {\n    return telemetry.snapshot(performanceOptions);\n}\n\nexport function installPerformanceDebugApi(target: Record<string, any>): void {\n    target.performance = {\n        reset: () => resetPerformanceTelemetry(),\n        snapshot: () => snapshotPerformanceTelemetry(),\n        getOptions: () => snapshotPerformanceConfig(),\n        setEnabled: (feature: PerformanceOptionKey, enabled: boolean) => {\n            if (!(feature in performanceOptions)) {\n                throw new Error(`Unknown performance option \"${feature}\"`);\n            }\n            performanceOptions[feature].value = enabled;\n        },\n    };\n}\n"
  },
  {
    "path": "src/setupThreeGlobal.ts",
    "content": "import * as THREE from 'three';\nconst bufferGeometryProto = THREE.BufferGeometry.prototype as THREE.BufferGeometry & {\n    addAttribute?: (name: string, attribute: THREE.BufferAttribute) => THREE.BufferGeometry;\n};\nif (!bufferGeometryProto.addAttribute) {\n    bufferGeometryProto.addAttribute = function addAttribute(this: THREE.BufferGeometry, name: string, attribute: THREE.BufferAttribute): THREE.BufferGeometry {\n        this.setAttribute(name, attribute);\n        return this;\n    };\n}\nconst bufferAttributeProto = THREE.BufferAttribute.prototype as THREE.BufferAttribute & {\n    setDynamic?: (dynamic: boolean) => THREE.BufferAttribute;\n    updateRange?: {\n        offset: number;\n        count: number;\n    };\n};\nif (!bufferAttributeProto.setDynamic) {\n    bufferAttributeProto.setDynamic = function setDynamic(this: THREE.BufferAttribute, dynamic: boolean): THREE.BufferAttribute {\n        this.setUsage(dynamic ? THREE.DynamicDrawUsage : THREE.StaticDrawUsage);\n        return this;\n    };\n}\nif (!Object.getOwnPropertyDescriptor(bufferAttributeProto, 'updateRange')) {\n    Object.defineProperty(bufferAttributeProto, 'updateRange', {\n        configurable: true,\n        enumerable: false,\n        get(this: THREE.BufferAttribute & {\n            __legacyUpdateRange?: {\n                offset: number;\n                count: number;\n            };\n        }) {\n            this.__legacyUpdateRange ??= { offset: 0, count: -1 };\n            return this.__legacyUpdateRange;\n        },\n        set(this: THREE.BufferAttribute & {\n            __legacyUpdateRange?: {\n                offset: number;\n                count: number;\n            };\n        }, value: {\n            offset: number;\n            count: number;\n        }) {\n            this.__legacyUpdateRange = value;\n        },\n    });\n}\nconst legacyThree = Object.assign({}, THREE, {\n    Math: {\n        generateUUID: THREE.MathUtils.generateUUID,\n    },\n});\nconst globalWindow = window as Window & typeof globalThis & {\n    THREE?: typeof legacyThree;\n};\nglobalWindow.THREE = legacyThree;\nexport { THREE };\n"
  },
  {
    "path": "src/test/performance/GeneralOptions.test.ts",
    "content": "import { describe, expect, test } from 'bun:test';\nimport { GeneralOptions } from '@/gui/screen/options/GeneralOptions';\n\ndescribe('GeneralOptions performance settings', () => {\n    test('loads legacy serialized options without performance suffix', () => {\n        const current = new GeneralOptions();\n        const legacySerialized = current.serialize().split(',').slice(0, 8).join(',');\n\n        const restored = new GeneralOptions().unserialize(legacySerialized);\n\n        expect(restored.performance.raycastHelperReuse.value).toBe(true);\n        expect(restored.performance.entityIntersectTraversal.value).toBe(true);\n        expect(restored.performance.mapTileHitTest.value).toBe(true);\n        expect(restored.performance.worldViewportCache.value).toBe(true);\n        expect(restored.performance.worldSoundLoopCache.value).toBe(true);\n        expect(restored.performance.telemetry.value).toBe(false);\n    });\n\n    test('round-trips performance settings in serialized options', () => {\n        const options = new GeneralOptions();\n        options.performance.raycastHelperReuse.value = false;\n        options.performance.entityIntersectTraversal.value = false;\n        options.performance.mapTileHitTest.value = false;\n        options.performance.worldViewportCache.value = false;\n        options.performance.worldSoundLoopCache.value = false;\n        options.performance.telemetry.value = true;\n\n        const restored = new GeneralOptions().unserialize(options.serialize());\n\n        expect(restored.performance.raycastHelperReuse.value).toBe(false);\n        expect(restored.performance.entityIntersectTraversal.value).toBe(false);\n        expect(restored.performance.mapTileHitTest.value).toBe(false);\n        expect(restored.performance.worldViewportCache.value).toBe(false);\n        expect(restored.performance.worldSoundLoopCache.value).toBe(false);\n        expect(restored.performance.telemetry.value).toBe(true);\n    });\n});\n"
  },
  {
    "path": "src/test/performance/PerformanceHelpers.test.ts",
    "content": "import { beforeEach, describe, expect, test } from 'bun:test';\nimport * as THREE from 'three';\nimport { IsoCoords } from '@/engine/IsoCoords';\nimport { Coords } from '@/game/Coords';\nimport { RaycastHelper } from '@/engine/util/RaycastHelper';\nimport { EntityIntersectHelper } from '@/engine/util/EntityIntersectHelper';\nimport { MapTileIntersectHelper } from '@/engine/util/MapTileIntersectHelper';\nimport { WorldViewportHelper } from '@/engine/util/WorldViewportHelper';\nimport { WorldSound } from '@/engine/sound/WorldSound';\nimport { SoundControl, SoundType } from '@/engine/sound/SoundSpecs';\nimport { PerformanceOptions } from '@/performance/PerformanceOptions';\nimport { attachPerformanceOptions, resetPerformanceTelemetry } from '@/performance/PerformanceRuntime';\nimport { EventDispatcher } from '@/util/event';\n\nbeforeEach(() => {\n    attachPerformanceOptions(new PerformanceOptions());\n    resetPerformanceTelemetry();\n    (globalThis as any).window = { THREE };\n    IsoCoords.init({ x: 0, y: 0 });\n});\n\ndescribe('RaycastHelper', () => {\n    test('returns the same intersection and reuses a single Raycaster when enabled', () => {\n        const viewport = { x: 0, y: 0, width: 800, height: 600 };\n        const camera = new THREE.OrthographicCamera(-400, 400, 300, -300, 0.1, 1000);\n        camera.position.set(0, 0, 100);\n        camera.lookAt(0, 0, 0);\n        camera.updateProjectionMatrix();\n        const mesh = new THREE.Mesh(new THREE.BoxGeometry(50, 50, 10), new THREE.MeshBasicMaterial());\n        mesh.updateMatrixWorld(true);\n        const helper = new RaycastHelper({ viewport, camera });\n        const point = { x: 400, y: 300 };\n\n        attachPerformanceOptions(new PerformanceOptions({ raycastHelperReuse: false }));\n        const legacyResult = helper.intersect(point, [mesh], false);\n\n        attachPerformanceOptions(new PerformanceOptions({ raycastHelperReuse: true }));\n        const optimizedFirst = helper.intersect(point, [mesh], false);\n        const raycasterRef = (helper as any).raycaster;\n        const optimizedSecond = helper.intersect(point, [mesh], false);\n\n        expect(legacyResult[0]?.object).toBe(mesh);\n        expect(optimizedFirst[0]?.object).toBe(mesh);\n        expect(optimizedSecond[0]?.object).toBe(mesh);\n        expect((helper as any).raycaster).toBe(raycasterRef);\n    });\n});\n\ndescribe('EntityIntersectHelper', () => {\n    test('preserves traversal results with nested children and array intersect targets', () => {\n        const leafA = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial());\n        leafA.name = 'leaf-a';\n        const leafB = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial());\n        leafB.name = 'leaf-b';\n        const nested = new THREE.Group();\n        nested.userData.id = 'nested';\n        nested.add(leafA, leafB);\n        const destroyed = new THREE.Group();\n        destroyed.userData.id = 'destroyed';\n        const root = new THREE.Group();\n        root.add(nested, destroyed);\n        const renderables = new Map([\n            ['nested', {\n                    gameObject: {\n                        isDestroyed: false,\n                        isCrashing: false,\n                    },\n                    getIntersectTarget: () => [leafA, leafB],\n                }],\n            ['destroyed', {\n                    gameObject: {\n                        isDestroyed: true,\n                        isCrashing: false,\n                    },\n                    getIntersectTarget: () => destroyed,\n                }],\n        ]);\n        const helper = new EntityIntersectHelper({\n            getObjectsOnTile: () => [],\n        } as any, {\n            getRenderableContainer: () => ({ get3DObject: () => root }),\n            getRenderableById: (id: string) => renderables.get(id),\n            getRenderableByGameObject: () => undefined,\n        } as any, {\n            getTileAtScreenPoint: () => undefined,\n        } as any, {\n            intersect: () => [],\n        } as any, {\n            viewport: { x: 0, y: 0, width: 800, height: 600 },\n        } as any, {\n            intersectsScreenBox: () => true,\n        } as any);\n\n        const legacyTargets = (helper as any).collectIntersectTargetsLegacy(root).map((item: THREE.Object3D) => item.name);\n        const optimizedTargets = (helper as any).collectIntersectTargetsOptimized(root).map((item: THREE.Object3D) => item.name);\n\n        expect(optimizedTargets).toEqual(legacyTargets);\n        expect(optimizedTargets).toEqual(['leaf-a', 'leaf-b']);\n    });\n});\n\ndescribe('MapTileIntersectHelper', () => {\n    test('matches legacy results for center, boundary, fallback-style, and out-of-range points', () => {\n        const tiles = new Map<string, { rx: number; ry: number; z: number; }>();\n        for (let x = -8; x <= 8; x += 1) {\n            for (let y = -8; y <= 8; y += 1) {\n                tiles.set(`${x},${y}`, { rx: x, ry: y, z: x === 0 && y === 0 ? 1 : 0 });\n            }\n        }\n        const helper = new MapTileIntersectHelper({\n            tiles: {\n                getByMapCoords: (x: number, y: number) => tiles.get(`${x},${y}`),\n            },\n        } as any, {\n            viewport: { x: 0, y: 0, width: 800, height: 600 },\n            cameraPan: {\n                getPan: () => ({ x: 0, y: 0 }),\n            },\n        } as any);\n        const points = [\n            { x: 400, y: 300 },\n            { x: 415, y: 308 },\n            { x: 400, y: 315 },\n            { x: -5000, y: -5000 },\n        ];\n\n        points.forEach((point) => {\n            const legacy = (helper as any).intersectTilesByScreenPosLegacy(point).map((tile: any) => `${tile.rx},${tile.ry},${tile.z}`);\n            const optimized = (helper as any).intersectTilesByScreenPosOptimized(point).map((tile: any) => `${tile.rx},${tile.ry},${tile.z}`);\n            expect(optimized).toEqual(legacy);\n        });\n    });\n});\n\ndescribe('WorldViewportHelper', () => {\n    test('matches legacy distances for iso and camera projection branches', () => {\n        const viewport = { x: 0, y: 0, width: 800, height: 600 };\n        const pan = { x: 0, y: 0 };\n        const isoHelper = new WorldViewportHelper({\n            viewport,\n            cameraPan: { getPan: () => pan },\n        } as any);\n        const camera = new THREE.OrthographicCamera(-400, 400, 300, -300, 0.1, 1000);\n        camera.position.set(0, 0, 100);\n        camera.lookAt(0, 0, 0);\n        camera.updateProjectionMatrix();\n        const cameraHelper = new WorldViewportHelper({\n            viewport,\n            cameraPan: { getPan: () => pan },\n            camera,\n        } as any);\n        const worldPosition = { x: 512, y: Coords.tileHeightToWorld(1), z: 256 };\n        const screenBox = new THREE.Box2(new THREE.Vector2(100, 100), new THREE.Vector2(700, 500));\n\n        const isoLegacyDistance = (isoHelper as any).distanceToViewportLegacy(worldPosition);\n        const isoOptimizedDistance = (isoHelper as any).distanceToViewportOptimized(worldPosition);\n        const isoLegacyCenter = (isoHelper as any).distanceToViewportCenterLegacy(worldPosition);\n        const isoOptimizedCenter = (isoHelper as any).distanceToViewportCenterOptimized(worldPosition);\n        const cameraLegacyDistance = (cameraHelper as any).distanceToScreenBoxLegacy(worldPosition, screenBox);\n        const cameraOptimizedDistance = (cameraHelper as any).distanceToScreenBoxOptimized(worldPosition, screenBox);\n\n        expect(isoOptimizedDistance).toBeCloseTo(isoLegacyDistance, 6);\n        expect(isoOptimizedCenter.x).toBeCloseTo(isoLegacyCenter.x, 6);\n        expect(isoOptimizedCenter.y).toBeCloseTo(isoLegacyCenter.y, 6);\n        expect(cameraOptimizedDistance).toBeCloseTo(cameraLegacyDistance, 6);\n    });\n});\n\ndescribe('WorldSound', () => {\n    test('keeps loop limits and output levels identical between legacy and optimized updates', () => {\n        const viewport = { x: 0, y: 0, width: 800, height: 600 };\n        const pan = { x: 0, y: 0 };\n        const tileHelper = new MapTileIntersectHelper({\n            tiles: {\n                getByMapCoords: (x: number, y: number) => ({ rx: x, ry: y, z: 0 }),\n            },\n        } as any, {\n            viewport,\n            cameraPan: { getPan: () => pan },\n        } as any);\n        const worldViewportHelper = new WorldViewportHelper({\n            viewport,\n            cameraPan: { getPan: () => pan },\n        } as any);\n        const frameDispatcher = new EventDispatcher<string, number>();\n        const createFixture = () => {\n            const worldSound = new WorldSound({\n                getSoundSpec: () => undefined,\n                playWithOptions: () => undefined,\n            } as any, { id: 'local' } as any, {\n                getShroudTypeByTileCoords: () => 0,\n            } as any, worldViewportHelper as any, tileHelper as any, {\n                onObjectRemoved: frameDispatcher.asEvent(),\n            } as any, {\n                viewport,\n            } as any, {\n                onFrame: frameDispatcher.asEvent(),\n            } as any);\n            const spec = {\n                name: 'looping',\n                volume: 100,\n                minVolume: 20,\n                type: [SoundType.Screen],\n                control: new Set([SoundControl.Loop]),\n                limit: 2,\n                loop: 1,\n                range: 8,\n            };\n            const handles = Array.from({ length: 4 }, () => {\n                const state = { volume: 0, pan: 0 };\n                return {\n                    state,\n                    isPlaying: () => true,\n                    stop: () => undefined,\n                    setVolume: (volume: number) => {\n                        state.volume = volume;\n                    },\n                    setPan: (panValue: number) => {\n                        state.pan = panValue;\n                    },\n                };\n            });\n            (worldSound as any).soundInstances = handles.map((handle, index) => ({\n                spec,\n                worldPos: { x: index * Coords.LEPTONS_PER_TILE, y: 0, z: 0 },\n                player: { id: 'local' },\n                handle,\n                gain: 1,\n                volume: 0,\n                loop: true,\n            }));\n            return { worldSound, handles };\n        };\n\n        const legacyFixture = createFixture();\n        const optimizedFixture = createFixture();\n\n        (legacyFixture.worldSound as any).updateLegacy();\n        (optimizedFixture.worldSound as any).updateOptimized();\n\n        expect(optimizedFixture.handles.map((handle) => handle.state.volume)).toEqual(legacyFixture.handles.map((handle) => handle.state.volume));\n        expect(optimizedFixture.handles.map((handle) => handle.state.pan)).toEqual(legacyFixture.handles.map((handle) => handle.state.pan));\n    });\n});\n"
  },
  {
    "path": "src/tools/AircraftTester.ts",
    "content": "import { Renderer } from \"@/engine/gfx/Renderer\";\nimport { Engine } from \"@/engine/Engine\";\nimport { Coords } from \"@/game/Coords\";\nimport { IsoCoords } from \"@/engine/IsoCoords\";\nimport { Player } from \"@/game/Player\";\nimport { WorldScene } from \"@/engine/renderable/WorldScene\";\nimport { Rules } from \"@/game/rules/Rules\";\nimport { MapGrid } from \"@/engine/renderable/entity/map/MapGrid\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\nimport { UiAnimationLoop } from \"@/engine/UiAnimationLoop\";\nimport { ImageFinder } from \"@/engine/ImageFinder\";\nimport { Art } from \"@/game/art/Art\";\nimport { RenderableFactory } from \"@/engine/renderable/entity/RenderableFactory\";\nimport { TheaterType } from \"@/engine/TheaterType\";\nimport { Alliances } from \"@/game/Alliances\";\nimport { PlayerList } from \"@/game/PlayerList\";\nimport { SelectionLevel } from \"@/game/gameobject/selection/SelectionLevel\";\nimport { VeteranLevel } from \"@/game/gameobject/unit/VeteranLevel\";\nimport { PointerEvents } from \"@/gui/PointerEvents\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { UnitSelection } from \"@/game/gameobject/selection/UnitSelection\";\nimport { CameraZoomControls } from \"@/tools/CameraZoomControls\";\nimport { Lighting } from \"@/engine/Lighting\";\nimport { ObjectFactory } from \"@/game/gameobject/ObjectFactory\";\nimport { TileCollection } from \"@/game/map/TileCollection\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { MoveState } from \"@/game/gameobject/trait/MoveTrait\";\nimport { TileOccupation } from \"@/game/map/TileOccupation\";\nimport { Bridges } from \"@/game/map/Bridges\";\nimport { RenderableManager } from \"@/engine/RenderableManager\";\nimport { World } from \"@/game/World\";\nimport { Strings } from \"@/data/Strings\";\nimport { MapBounds } from \"@/game/map/MapBounds\";\nimport { FlyerHelperMode } from \"@/engine/renderable/entity/unit/FlyerHelperMode\";\nimport { VxlBuilderFactory } from \"@/engine/renderable/builder/VxlBuilderFactory\";\nimport { VxlGeometryPool } from \"@/engine/renderable/builder/vxlGeometry/VxlGeometryPool\";\nimport { VxlGeometryCache } from \"@/engine/gfx/geometry/VxlGeometryCache\";\nimport { ShadowQuality } from \"@/engine/renderable/entity/unit/ShadowQuality\";\nimport { CanvasMetrics } from \"@/gui/CanvasMetrics\";\nimport { PipOverlay } from \"@/engine/renderable/entity/PipOverlay\";\nimport { TextureUtils } from \"@/engine/gfx/TextureUtils\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { LightingDirector } from \"@/engine/gfx/lighting/LightingDirector\";\nimport { ResourceType } from \"@/engine/resourceConfigs\";\nimport { TestToolSupport, type TestToolRuntimeContext } from \"@/tools/TestToolSupport\";\ndeclare const THREE: any;\nexport class AircraftTester {\n    private static disposables = new CompositeDisposable();\n    private static renderer: Renderer;\n    private static theater: any;\n    private static rules: Rules;\n    private static art: Art;\n    private static images: any;\n    private static voxels: any;\n    private static voxelAnims: any;\n    private static uiAnimationLoop: UiAnimationLoop;\n    private static worldScene: WorldScene;\n    private static world: World;\n    private static currentRenderable: any;\n    private static currentAircraft: any;\n    private static listEl: HTMLDivElement;\n    private static controlsEl: HTMLDivElement | undefined;\n    private static hostElement?: HTMLElement;\n    private static vxlGeometryPool: VxlGeometryPool;\n    private static currentAircraftType?: string;\n    static async main(_args: any, context: TestToolRuntimeContext = {}): Promise<void> {\n        await TestToolSupport.ensureTheater(TheaterType.Temperate, context.cdnResourceLoader, [ResourceType.Vxl, ResourceType.Anims]);\n        const hostElement = this.hostElement = TestToolSupport.prepareHost(context, 1224, 600);\n        const renderer = (this.renderer = new Renderer(800, 600));\n        renderer.init(hostElement);\n        TestToolSupport.placeRendererCanvas(renderer, 212, 0);\n        renderer.initStats(document.body);\n        this.buildHomeButton();\n        const worldScene = WorldScene.factory({ x: 0, y: 0, width: 800, height: 600 }, new BoxedVar(true), new BoxedVar(ShadowQuality.High));\n        this.disposables.add(worldScene);\n        (worldScene.scene.background as any) = new THREE.Color(12632256);\n        IsoCoords.init({ x: 0, y: 0 });\n        this.theater = await Engine.loadTheater(TheaterType.Temperate);\n        const rules = new Rules(Engine.getRules());\n        this.rules = rules;\n        this.art = new Art(rules as any, Engine.getArt(), undefined as any, console);\n        this.images = Engine.getImages();\n        this.voxels = Engine.getVoxels();\n        this.voxelAnims = Engine.getVoxelAnims();\n        this.buildBrowser(rules[\"aircraftRules\"] as Map<string, any>);\n        const canvasMetrics = new CanvasMetrics(renderer.getCanvas(), window);\n        canvasMetrics.init();\n        this.disposables.add(() => canvasMetrics.dispose());\n        const pointerEvents = new PointerEvents(renderer as any, { x: 0, y: 0 }, document, canvasMetrics as any);\n        const cameraZoomControls = new CameraZoomControls(pointerEvents, worldScene.cameraZoom);\n        cameraZoomControls.init();\n        this.disposables.add(pointerEvents, cameraZoomControls);\n        renderer.addScene(worldScene);\n        const uiAnimationLoop = (this.uiAnimationLoop = new UiAnimationLoop(renderer));\n        uiAnimationLoop.start();\n        this.worldScene = worldScene;\n        this.vxlGeometryPool = new VxlGeometryPool(new VxlGeometryCache(null, null));\n        this.addGrid();\n        this.createFloor();\n        this.syncState();\n    }\n    static addGrid(): void {\n        const mapGrid = new MapGrid({ width: 10, height: 10 });\n        const gridObject = mapGrid.get3DObject();\n        const container = new THREE.Object3D();\n        container.add(gridObject);\n        this.worldScene.scene.add(container);\n    }\n    static createFloor(): void {\n        const geometry = new THREE.PlaneGeometry(10000, 10000);\n        const material = new THREE.ShadowMaterial();\n        material.opacity = 0.5;\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.rotation.x = -Math.PI / 2;\n        mesh.receiveShadow = true;\n        mesh.renderOrder = 200000;\n        this.worldScene.scene.add(mesh);\n    }\n    static selectAircraft(aircraftType: string): void {\n        if (this.currentAircraft && !this.currentAircraft.isDisposed) {\n            this.world.removeObject(this.currentAircraft);\n            this.currentAircraft.dispose();\n        }\n        const player = new Player(\"Player\");\n        this.disposables.add(player);\n        const desiredColor = this.rules.getMultiplayerColors().get(\"DarkRed\")!;\n        (player as any).color = desiredColor;\n        const playerList = new PlayerList();\n        playerList.addPlayer(player);\n        const alliances = new Alliances(playerList);\n        const unitSelection = new UnitSelection();\n        const lighting = new Lighting();\n        this.disposables.add(lighting);\n        const renderableFactory = new RenderableFactory(new BoxedVar(player) as any, unitSelection as any, alliances as any, this.rules as any, this.art as any, undefined as any, new ImageFinder(this.images, this.theater) as any, Engine.getPalettes() as any, this.voxels as any, this.voxelAnims as any, this.theater as any, this.worldScene.camera as any, new Lighting(), new LightingDirector(new Lighting().mapLighting, this.renderer as any, new BoxedVar(1) as any) as any, new BoxedVar(false) as any, new BoxedVar(false) as any, new BoxedVar(2) as any, undefined as any, new Strings() as any, new BoxedVar(FlyerHelperMode.Selected) as any, new BoxedVar(false) as any, new VxlBuilderFactory(this.vxlGeometryPool, false, this.worldScene.camera) as any, new Map() as any);\n        const tileCollection = new TileCollection([\n            { rx: 0, ry: 0, dx: 0, dy: 0, z: 0, tileNum: 0, subTile: 0 },\n            { rx: 1, ry: 0, dx: 1, dy: 0, z: 0, tileNum: 0, subTile: 0 },\n            { rx: 0, ry: 1, dx: 0, dy: 1, z: 0, tileNum: 0, subTile: 0 },\n            { rx: 1, ry: 1, dx: 1, dy: 1, z: 0, tileNum: 0, subTile: 0 },\n        ] as any, this.theater.tileSets as any, this.rules.general as any, () => 0);\n        const tileOccupation = new TileOccupation(tileCollection);\n        const mapBounds = new MapBounds();\n        const bridges = new Bridges(this.theater.tileSets, tileCollection, tileOccupation, mapBounds, this.rules);\n        const aircraft = (this.currentAircraft = new ObjectFactory(tileCollection, tileOccupation, bridges, new BoxedVar(0)).create(ObjectType.Aircraft, aircraftType, this.rules as any, this.art as any));\n        this.currentAircraftType = aircraftType;\n        aircraft.owner = player;\n        aircraft.position.tile = { rx: 1, ry: 1, z: 0, rampType: 0 };\n        const world = (this.world = new World());\n        const renderableManager = new RenderableManager(world, this.worldScene, this.worldScene.camera, renderableFactory);\n        renderableManager.init();\n        this.disposables.add(renderableManager);\n        world.spawnObject(aircraft);\n        const renderable = (this.currentRenderable = renderableManager.getRenderableByGameObject(aircraft));\n        renderable.selectionModel.setSelectionLevel(SelectionLevel.None);\n        renderable.selectionModel.setControlGroupNumber(3);\n        this.buildControls();\n        this.syncState();\n    }\n    static buildControls(): void {\n        if (this.controlsEl) {\n            this.controlsEl.remove();\n        }\n        const controls = (this.controlsEl = document.createElement(\"div\"));\n        controls.dataset.testid = \"aircraft-controls\";\n        controls.style.position = \"absolute\";\n        controls.style.left = \"0\";\n        controls.style.top = \"0\";\n        controls.style.width = \"200px\";\n        controls.style.padding = \"5px\";\n        controls.style.background = \"rgba(255, 255, 255, 0.5)\";\n        controls.style.border = \"1px black solid\";\n        controls.appendChild(document.createTextNode(\"Remap color:\"));\n        const colorMap = new Map(this.rules.getMultiplayerColors());\n        const colorSelect = document.createElement(\"select\");\n        colorSelect.dataset.testid = \"aircraft-color\";\n        colorSelect.style.display = \"block\";\n        colorSelect.addEventListener(\"change\", () => {\n            this.currentAircraft.owner.color = colorMap.get(colorSelect.value);\n            this.syncState();\n        });\n        controls.appendChild(colorSelect);\n        colorMap.forEach((color, name) => {\n            const option = document.createElement(\"option\");\n            option.innerHTML = name;\n            option.value = name;\n            option.selected = color.asHex() === this.currentAircraft.owner.color.asHex();\n            colorSelect.appendChild(option);\n        });\n        controls.appendChild(document.createTextNode(\"Selection level:\"));\n        const selDiv = document.createElement(\"div\");\n        controls.appendChild(selDiv);\n        [SelectionLevel.None, SelectionLevel.Hover, SelectionLevel.Selected].forEach((level) => {\n            const btn = document.createElement(\"button\");\n            btn.innerHTML = SelectionLevel[level];\n            btn.disabled = !this.currentAircraft.rules.selectable && level === SelectionLevel.Selected;\n            btn.dataset.testid = `aircraft-selection-${SelectionLevel[level].toLowerCase()}`;\n            btn.addEventListener(\"click\", () => {\n                this.currentRenderable.selectionModel.setSelectionLevel(level);\n                this.syncState();\n            });\n            selDiv.appendChild(btn);\n        });\n        controls.appendChild(document.createTextNode(\"Veteran level:\"));\n        const vetDiv = document.createElement(\"div\");\n        controls.appendChild(vetDiv);\n        if (this.currentAircraft.veteranTrait) {\n            [VeteranLevel.None, VeteranLevel.Veteran, VeteranLevel.Elite].forEach((lvl) => {\n                const btn = document.createElement(\"button\");\n                btn.innerHTML = VeteranLevel[lvl];\n                btn.dataset.testid = `aircraft-veteran-${VeteranLevel[lvl].toLowerCase()}`;\n                btn.addEventListener(\"click\", () => {\n                    this.currentAircraft.veteranTrait.veteranLevel = lvl;\n                    this.syncState();\n                });\n                vetDiv.appendChild(btn);\n            });\n        }\n        controls.appendChild(document.createTextNode(\"Rudder:\"));\n        const yaw = document.createElement(\"input\");\n        yaw.dataset.testid = \"aircraft-yaw\";\n        yaw.style.display = \"block\";\n        yaw.type = \"range\";\n        yaw.min = \"-180\";\n        yaw.max = \"180\";\n        yaw.value = \"0\";\n        yaw.addEventListener(\"input\", () => {\n            this.currentAircraft.yaw = Number(yaw.value);\n            this.syncState();\n        });\n        controls.appendChild(yaw);\n        const pitch = document.createElement(\"input\");\n        pitch.dataset.testid = \"aircraft-pitch\";\n        pitch.style.display = \"block\";\n        pitch.type = \"range\";\n        pitch.min = \"-180\";\n        pitch.max = \"180\";\n        pitch.value = \"0\";\n        pitch.addEventListener(\"input\", () => {\n            this.currentAircraft.pitch = Number(pitch.value);\n            this.syncState();\n        });\n        controls.appendChild(pitch);\n        const roll = document.createElement(\"input\");\n        roll.dataset.testid = \"aircraft-roll\";\n        roll.style.display = \"block\";\n        roll.type = \"range\";\n        roll.min = \"-180\";\n        roll.max = \"180\";\n        roll.value = \"0\";\n        roll.addEventListener(\"input\", () => {\n            this.currentAircraft.roll = Number(roll.value);\n            this.syncState();\n        });\n        controls.appendChild(roll);\n        controls.appendChild(document.createTextNode(\"Height:\"));\n        const height = document.createElement(\"input\");\n        height.dataset.testid = \"aircraft-height\";\n        height.type = \"range\";\n        height.min = \"0\";\n        height.max = \"2560\";\n        height.value = \"0\";\n        height.style.display = \"block\";\n        height.addEventListener(\"input\", () => {\n            this.currentAircraft.position.tileElevation = Coords.worldToTileHeight(Number(height.value));\n            this.syncState();\n        });\n        controls.appendChild(height);\n        controls.appendChild(document.createTextNode(\"isMoving:\"));\n        const moving = document.createElement(\"input\");\n        moving.dataset.testid = \"aircraft-moving\";\n        moving.type = \"checkbox\";\n        moving.style.display = \"block\";\n        moving.addEventListener(\"change\", (e) => {\n            const checked = (e.target as HTMLInputElement).checked;\n            this.currentAircraft.moveTrait.moveState = checked ? MoveState.Moving : MoveState.Idle;\n            this.currentAircraft.zone = checked ? ZoneType.Air : ZoneType.Ground;\n            this.syncState();\n        });\n        controls.appendChild(moving);\n        controls.appendChild(document.createTextNode(\"Warped out:\"));\n        const warped = document.createElement(\"input\");\n        warped.dataset.testid = \"aircraft-warped\";\n        warped.type = \"checkbox\";\n        warped.style.display = \"block\";\n        warped.addEventListener(\"change\", (e) => {\n            this.currentAircraft.warpedOutTrait.debugSetActive((e.target as HTMLInputElement).checked);\n            this.syncState();\n        });\n        controls.appendChild(warped);\n        const destroy = document.createElement(\"button\");\n        destroy.dataset.testid = \"aircraft-destroy\";\n        destroy.style.display = \"block\";\n        destroy.style.color = \"red\";\n        destroy.innerHTML = \"DESTROY\";\n        destroy.addEventListener(\"click\", async () => {\n            this.currentAircraft.isDestroyed = true;\n            this.world.removeObject(this.currentAircraft);\n            this.currentAircraft.dispose();\n            this.currentAircraft = undefined;\n            this.currentAircraftType = undefined;\n            this.controlsEl?.remove();\n            this.controlsEl = undefined;\n            this.syncState();\n        });\n        controls.appendChild(destroy);\n        this.hostElement?.appendChild(controls);\n        TestToolSupport.applyPanelTheme(controls);\n        this.syncState();\n    }\n    private static syncState(): void {\n        const aircraft = this.currentAircraft;\n        const renderable = this.currentRenderable;\n        const selectionLevel = renderable?.selectionModel?.getSelectionLevel?.();\n        const veteranLevel = aircraft?.veteranTrait?.veteranLevel ?? aircraft?.veteranLevel ?? VeteranLevel.None;\n        const vxlExtraLightScalar = renderable?.extraLight?.x ?? null;\n        TestToolSupport.setState('aircraft', {\n            availableAircraft: this.listEl?.querySelectorAll('a').length ?? 0,\n            selectedAircraft: this.currentAircraftType ?? null,\n            rendered: Boolean(renderable?.get3DObject?.() ?? renderable),\n            selectionLevelValue: selectionLevel ?? null,\n            selectionLevel: TestToolSupport.enumLabel(SelectionLevel, selectionLevel),\n            selectionLevelOptions: TestToolSupport.enumOptions(SelectionLevel, [SelectionLevel.None, SelectionLevel.Hover, SelectionLevel.Selected]),\n            veteranLevelValue: aircraft ? veteranLevel : null,\n            veteranLevel: aircraft ? TestToolSupport.enumLabel(VeteranLevel, veteranLevel) : null,\n            veteranLevelOptions: TestToolSupport.enumOptions(VeteranLevel, [VeteranLevel.None, VeteranLevel.Veteran, VeteranLevel.Elite]),\n            yaw: aircraft?.yaw ?? null,\n            pitch: aircraft?.pitch ?? null,\n            roll: aircraft?.roll ?? null,\n            tileElevation: aircraft?.position?.tileElevation ?? null,\n            moveState: aircraft?.moveTrait?.moveState ?? null,\n            isMoving: aircraft?.moveTrait?.moveState === MoveState.Moving,\n            zoneValue: aircraft?.zone ?? null,\n            zone: TestToolSupport.enumLabel(ZoneType, aircraft?.zone),\n            warpedOut: Boolean(aircraft?.warpedOutTrait?.isActive?.()),\n            ownerColor: aircraft?.owner?.color?.asHexString?.() ?? null,\n            vxlExtraLightScalar,\n        });\n    }\n    static buildBrowser(aircraftRules: Map<string, any>): void {\n        const list = (this.listEl = document.createElement(\"div\"));\n        list.style.position = \"absolute\";\n        list.style.right = \"0\";\n        list.style.top = \"0\";\n        list.style.height = \"600px\";\n        list.style.width = \"200px\";\n        list.style.overflowY = \"auto\";\n        list.style.padding = \"5px\";\n        list.style.background = \"rgba(255, 255, 255, 0.5)\";\n        list.style.border = \"1px black solid\";\n        list.appendChild(document.createTextNode(\"Aircraft types:\"));\n        const types = [...aircraftRules.keys()]\n            .filter((name) => this.art.hasObject(name, ObjectType.Aircraft))\n            .sort();\n        types.forEach((name) => {\n            const link = document.createElement(\"a\");\n            link.dataset.aircraftType = name;\n            link.style.display = \"block\";\n            link.textContent = name;\n            link.setAttribute(\"href\", \"javascript:;\");\n            link.addEventListener(\"click\", () => {\n                console.log(\"Selected aircraft\", name);\n                this.selectAircraft(name);\n            });\n            list.appendChild(link);\n        });\n        this.hostElement?.appendChild(list);\n        TestToolSupport.applyPanelTheme(list);\n        this.syncState();\n        setTimeout(() => {\n            if (types.length)\n                this.selectAircraft(types[0]);\n        }, 50);\n    }\n    private static buildHomeButton(): void {\n        const homeButton = document.createElement('button');\n        homeButton.innerHTML = '点此返回主页';\n        homeButton.style.cssText = `\n      position: fixed;\n      left: 50%;\n      top: 10px;\n      transform: translateX(-50%);\n      padding: 10px 20px;\n      background-color: rgba(0, 0, 0, 0.8);\n      color: white;\n      border: 2px solid rgba(255, 255, 255, 0.3);\n      border-radius: 6px;\n      cursor: pointer;\n      font-size: 16px;\n      font-weight: bold;\n      z-index: 1000;\n      transition: all 0.3s ease;\n      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n    `;\n        homeButton.onmouseover = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';\n            homeButton.style.transform = 'translateX(-50%) translateY(-2px)';\n            homeButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';\n        };\n        homeButton.onmouseout = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';\n            homeButton.style.transform = 'translateX(-50%) translateY(0)';\n            homeButton.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';\n        };\n        homeButton.onclick = () => {\n            window.location.hash = '/';\n        };\n        document.body.appendChild(homeButton);\n        this.disposables.add(() => homeButton.remove());\n    }\n    static destroy(): void {\n        this.renderer.dispose();\n        this.uiAnimationLoop.destroy();\n        this.listEl?.remove();\n        if (this.controlsEl) {\n            this.controlsEl.remove();\n            this.controlsEl = undefined;\n        }\n        this.currentAircraftType = undefined;\n        this.disposables.dispose();\n        TestToolSupport.clearState('aircraft');\n        try {\n            if ((PipOverlay as any)?.clearCaches) {\n                PipOverlay.clearCaches();\n            }\n            if ((TextureUtils as any)?.cache) {\n                TextureUtils.cache.forEach((tex: any) => tex.dispose?.());\n                TextureUtils.cache.clear();\n            }\n        }\n        catch (err) {\n            console.warn('[AircraftTester] Failed to clear caches during destroy:', err);\n        }\n    }\n}\n"
  },
  {
    "path": "src/tools/BuildingTester.ts",
    "content": "import { Renderer } from \"@/engine/gfx/Renderer\";\nimport { Engine } from \"@/engine/Engine\";\nimport { IsoCoords } from \"@/engine/IsoCoords\";\nimport { TheaterType } from \"@/engine/TheaterType\";\nimport { Player } from \"@/game/Player\";\nimport { DamageType } from \"@/engine/renderable/entity/building/DamageType\";\nimport { AnimationType } from \"@/engine/renderable/entity/building/AnimationType\";\nimport { WorldScene } from \"@/engine/renderable/WorldScene\";\nimport { Rules } from \"@/game/rules/Rules\";\nimport { MapGrid } from \"@/engine/renderable/entity/map/MapGrid\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\nimport { UiAnimationLoop } from \"@/engine/UiAnimationLoop\";\nimport { ImageFinder } from \"@/engine/ImageFinder\";\nimport { Art } from \"@/game/art/Art\";\nimport { RenderableFactory } from \"@/engine/renderable/entity/RenderableFactory\";\nimport { Color } from \"@/util/Color\";\nimport { SelectionLevel } from \"@/game/gameobject/selection/SelectionLevel\";\nimport { Alliances } from \"@/game/Alliances\";\nimport { PlayerList } from \"@/game/PlayerList\";\nimport { ObjectFactory } from \"@/game/gameobject/ObjectFactory\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { PointerEvents } from \"@/gui/PointerEvents\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { UnitSelection } from \"@/game/gameobject/selection/UnitSelection\";\nimport { CameraZoomControls } from \"@/tools/CameraZoomControls\";\nimport { Infantry } from \"@/game/gameobject/Infantry\";\nimport { Lighting } from \"@/engine/Lighting\";\nimport { TileCollection } from \"@/game/map/TileCollection\";\nimport { TileOccupation } from \"@/game/map/TileOccupation\";\nimport { Bridges } from \"@/game/map/Bridges\";\nimport { Strings } from \"@/data/Strings\";\nimport { AutoRepairTrait } from \"@/game/gameobject/trait/AutoRepairTrait\";\nimport { MapBounds } from \"@/game/map/MapBounds\";\nimport { FlyerHelperMode } from \"@/engine/renderable/entity/unit/FlyerHelperMode\";\nimport { LightingDirector } from \"@/engine/gfx/lighting/LightingDirector\";\nimport { World } from \"@/game/World\";\nimport { RenderableManager } from \"@/engine/RenderableManager\";\nimport { VxlBuilderFactory } from \"@/engine/renderable/builder/VxlBuilderFactory\";\nimport { VxlGeometryPool } from \"@/engine/renderable/builder/vxlGeometry/VxlGeometryPool\";\nimport { VxlGeometryCache } from \"@/engine/gfx/geometry/VxlGeometryCache\";\nimport { ShadowQuality } from \"@/engine/renderable/entity/unit/ShadowQuality\";\nimport { CanvasMetrics } from \"@/gui/CanvasMetrics\";\nimport { getRandomInt } from \"@/util/math\";\nimport { PipOverlay } from \"@/engine/renderable/entity/PipOverlay\";\nimport { TextureUtils } from \"@/engine/gfx/TextureUtils\";\nimport { ResourceType } from \"@/engine/resourceConfigs\";\nimport { TestToolSupport, type TestToolRuntimeContext } from \"@/tools/TestToolSupport\";\ndeclare const THREE: any;\ninterface BuildingRule {\n    techLevel: number;\n    constructionYard: boolean;\n    selectable: boolean;\n    maxNumberOccupants: number;\n}\ninterface GameObject {\n    owner: Player;\n    position: {\n        tile: {\n            rx: number;\n            ry: number;\n            z: number;\n            rampType: number;\n        };\n        setCenterOffset(offset: any): void;\n    };\n    getFoundationCenterOffset(): any;\n    isDisposed: boolean;\n    dispose(): void;\n    isDestroyed: boolean;\n    healthTrait: {\n        health: number;\n    };\n    garrisonTrait: {\n        units: Infantry[];\n    } | null;\n    rules: BuildingRule;\n    traits: Map<any, any>;\n    warpedOutTrait: {\n        debugSetActive(active: boolean): void;\n    };\n}\ninterface Renderable {\n    selectionModel: {\n        setSelectionLevel(level: SelectionLevel): void;\n        setControlGroupNumber(number: number): void;\n    };\n    setAnimation(type: AnimationType, time: number): void;\n    endCurrentAnimation(): void;\n    hasAnimation(type: AnimationType): boolean;\n    setPowered(powered: boolean): void;\n}\nexport class BuildingTester {\n    private static disposables = new CompositeDisposable();\n    private static renderer: Renderer;\n    private static theater: any;\n    private static rules: Rules;\n    private static art: Art;\n    private static images: any;\n    private static voxels: any;\n    private static voxelAnims: any;\n    private static uiAnimationLoop: UiAnimationLoop;\n    private static worldScene: WorldScene;\n    private static vxlGeometryPool: VxlGeometryPool;\n    private static currentRenderable: Renderable | null = null;\n    private static currentBuilding: GameObject | null = null;\n    private static world: World;\n    private static animButtonsWrap: HTMLDivElement;\n    private static occupiedButtonsWrap: HTMLDivElement;\n    private static controlsElement: HTMLDivElement | undefined;\n    private static listEl: HTMLDivElement;\n    private static hostElement?: HTMLElement;\n    private static currentBuildingType?: string;\n    private static currentBuildingColor?: Color;\n    static async main(args: any[], context: TestToolRuntimeContext = {}): Promise<void> {\n        await TestToolSupport.ensureTheater(TheaterType.Snow, context.cdnResourceLoader, [ResourceType.BuildGen, ResourceType.Anims]);\n        const hostElement = this.hostElement = TestToolSupport.prepareHost(context, 1244, 600);\n        const renderer = (this.renderer = new Renderer(800, 600));\n        renderer.init(hostElement);\n        TestToolSupport.placeRendererCanvas(renderer, 232, 0);\n        renderer.initStats(document.body);\n        this.buildHomeButton();\n        const worldScene = WorldScene.factory({ x: 0, y: 0, width: 800, height: 600 }, new BoxedVar(true), new BoxedVar(ShadowQuality.High));\n        this.disposables.add(worldScene);\n        (worldScene.scene.background as any) = new THREE.Color(12632256);\n        IsoCoords.init({ x: 0, y: 0 });\n        this.theater = await Engine.loadTheater(TheaterType.Snow);\n        const rules = new Rules(Engine.getRules());\n        this.buildBrowser(rules.buildingRules);\n        this.rules = rules;\n        console.log('current all rules', rules);\n        this.art = new Art(rules, Engine.getArt());\n        this.images = Engine.getImages();\n        this.voxels = Engine.getVoxels();\n        this.voxelAnims = Engine.getVoxelAnims();\n        const canvasMetrics = new CanvasMetrics(renderer.getCanvas(), window);\n        canvasMetrics.init();\n        this.disposables.add(canvasMetrics);\n        const pointerEvents = new PointerEvents(renderer, { x: 0, y: 0 }, document, canvasMetrics);\n        const cameraZoomControls = new CameraZoomControls(pointerEvents, worldScene.cameraZoom);\n        this.disposables.add(cameraZoomControls, pointerEvents);\n        cameraZoomControls.init();\n        renderer.addScene(worldScene);\n        const uiAnimationLoop = (this.uiAnimationLoop = new UiAnimationLoop(renderer));\n        uiAnimationLoop.start();\n        this.worldScene = worldScene;\n        this.vxlGeometryPool = new VxlGeometryPool(new VxlGeometryCache());\n        this.addGrid();\n        this.syncState();\n    }\n    static addGrid(): void {\n        const mapGrid = new MapGrid({ width: 10, height: 10 });\n        const gridObject = mapGrid.get3DObject();\n        const container = new THREE.Object3D();\n        container.add(gridObject);\n        this.worldScene.scene.add(container);\n    }\n    static selectBuilding(buildingType: string): void {\n        if (this.currentRenderable && this.currentBuilding && !this.currentBuilding.isDisposed) {\n            this.world.removeObject(this.currentBuilding);\n            this.currentBuilding.dispose();\n        }\n        const buildingRule = this.rules.getBuilding(buildingType);\n        const player = new Player(\"Player\");\n        this.disposables.add(player);\n        const defaultColor = (buildingRule.techLevel !== -1 || buildingRule.constructionYard)\n            ? this.rules.getMultiplayerColors().get(\"DarkRed\")?.clone() ?? new Color(255, 0, 0)\n            : new Color(255, 255, 255);\n        const selectedColor = this.currentBuildingColor?.clone() ?? defaultColor;\n        player.color = selectedColor;\n        this.currentBuildingColor = selectedColor?.clone();\n        const playerList = new PlayerList();\n        playerList.addPlayer(player);\n        const alliances = new Alliances(playerList);\n        const unitSelection = new UnitSelection();\n        const lighting = new Lighting();\n        this.disposables.add(lighting);\n        const renderableFactory = new RenderableFactory(new BoxedVar(player), unitSelection, alliances, this.rules, this.art, undefined, new ImageFinder(this.images, this.theater), Engine.getPalettes(), this.voxels, this.voxelAnims, this.theater, this.worldScene.camera, lighting, new LightingDirector(lighting, this.renderer, new BoxedVar(1)), new BoxedVar(false), new BoxedVar(false), new BoxedVar(2), undefined, new Strings({ TXT_PRIMARY: \"Primary\" }), new BoxedVar(FlyerHelperMode.Selected), new BoxedVar(false), new VxlBuilderFactory(this.vxlGeometryPool, false, this.worldScene.camera), new Map());\n        const tileCollection = new TileCollection([], null as any, this.rules.general, (min: number, max: number) => getRandomInt(min, max));\n        const tileOccupation = new TileOccupation(tileCollection);\n        const mapBounds = new MapBounds();\n        const bridges = new Bridges(this.theater.tileSets, tileCollection, tileOccupation, mapBounds, this.rules);\n        const building = (this.currentBuilding = new ObjectFactory(tileCollection, tileOccupation, bridges, new BoxedVar(1)).create(ObjectType.Building, buildingType, this.rules, this.art) as GameObject);\n        this.currentBuildingType = buildingType;\n        building.owner = player;\n        building.position.tile = { rx: 1, ry: 1, z: 0, rampType: 0 };\n        building.position.setCenterOffset(building.getFoundationCenterOffset());\n        const world = (this.world = new World());\n        const renderableManager = new RenderableManager(world, this.worldScene, this.worldScene.camera, renderableFactory);\n        renderableManager.init();\n        this.disposables.add(renderableManager);\n        world.spawnObject(building);\n        const renderable = (this.currentRenderable = renderableManager.getRenderableByGameObject(building) as Renderable);\n        renderable.selectionModel.setSelectionLevel(SelectionLevel.Selected);\n        this.currentRenderable.selectionModel.setControlGroupNumber(3);\n        setTimeout(() => {\n            this.buildBuildingControls();\n            this.createAnimButtons();\n            this.createOccupiedButtons();\n            this.syncState();\n        }, 50);\n        this.syncState();\n    }\n    static selectAnimation(animationType: AnimationType): void {\n        if (this.currentRenderable) {\n            this.currentRenderable.setAnimation(animationType, performance.now());\n            this.syncState();\n        }\n    }\n    static stopCurrentAnimation(): void {\n        if (this.currentRenderable) {\n            this.currentRenderable.endCurrentAnimation();\n            this.syncState();\n        }\n    }\n    static setDamageType(damageType: DamageType | null): void {\n        if (!this.currentBuilding)\n            return;\n        this.currentBuilding.healthTrait.health = damageType\n            ? 100 * (damageType === DamageType.CONDITION_YELLOW\n                ? this.rules.audioVisual.conditionYellow\n                : this.rules.audioVisual.conditionRed)\n            : 100;\n        this.syncState();\n    }\n    static setActiveState(active: boolean): void {\n        if (this.currentRenderable) {\n            this.currentRenderable.setPowered(active);\n            this.syncState();\n        }\n    }\n    static createAnimButtons(): void {\n        const container = this.animButtonsWrap;\n        container.innerHTML = \"\";\n        const animationTypes = [\n            AnimationType.IDLE,\n            AnimationType.PRODUCTION,\n            AnimationType.BUILDUP,\n            AnimationType.UNBUILD,\n            AnimationType.SUPER_CHARGE_START,\n            AnimationType.SPECIAL_REPAIR_START,\n            AnimationType.SPECIAL_SHOOT,\n            AnimationType.SPECIAL_DOCKING,\n            AnimationType.FACTORY_DEPLOYING,\n            AnimationType.FACTORY_ROOF_DEPLOYING,\n        ];\n        for (const animType of animationTypes) {\n            const button = document.createElement(\"button\");\n            button.innerHTML = AnimationType[animType];\n            button.dataset.testid = `building-animation-${AnimationType[animType].toLowerCase()}`;\n            button.style.display = \"block\";\n            button.addEventListener(\"click\", () => this.selectAnimation(animType));\n            if (!this.currentRenderable) {\n                throw new Error(\"Must build anim buttons after a building is selected\");\n            }\n            const hasAnimation = this.currentRenderable.hasAnimation(animType);\n            button.disabled = !hasAnimation;\n            button.style.opacity = hasAnimation ? \"1\" : \".5\";\n            container.appendChild(button);\n        }\n        if (this.controlsElement) {\n            TestToolSupport.applyPanelTheme(this.controlsElement);\n        }\n        this.syncState();\n    }\n    static createOccupiedButtons(): void {\n        const container = this.occupiedButtonsWrap;\n        container.innerHTML = \"\";\n        const select = document.createElement(\"select\");\n        select.dataset.testid = \"building-occupants\";\n        select.disabled = !this.currentBuilding?.garrisonTrait;\n        select.style.display = \"block\";\n        select.addEventListener(\"change\", () => {\n            if (this.currentBuilding?.garrisonTrait) {\n                this.currentBuilding.garrisonTrait.units = new Array(Number(select.value))\n                    .fill(0)\n                    .map(() => new Infantry(\"dummy\", this.rules.getObject(\"E1\", ObjectType.Infantry), null));\n                this.syncState();\n            }\n        });\n        const maxOccupants = this.currentBuilding?.rules.maxNumberOccupants || 0;\n        for (let i = 0; i < maxOccupants + 1; i++) {\n            const option = document.createElement(\"option\");\n            option.innerHTML = String(i);\n            option.value = String(i);\n            option.selected = i === 0;\n            select.appendChild(option);\n        }\n        container.appendChild(select);\n        if (this.controlsElement) {\n            TestToolSupport.applyPanelTheme(this.controlsElement);\n        }\n    }\n    static buildBuildingControls(): void {\n        if (this.controlsElement) {\n            this.controlsElement.remove();\n        }\n        const controls = (this.controlsElement = document.createElement(\"div\"));\n        controls.dataset.testid = \"building-controls\";\n        controls.style.position = \"absolute\";\n        controls.style.left = \"0\";\n        controls.style.top = \"0\";\n        controls.style.width = \"220px\";\n        controls.style.padding = \"5px\";\n        controls.style.background = \"rgba(255, 255, 255, 0.5)\";\n        controls.style.border = \"1px black solid\";\n        controls.appendChild(document.createTextNode(\"Remap color:\"));\n        const colorMap = new Map(this.rules.getMultiplayerColors());\n        colorMap.set(\"None\", new Color(255, 255, 255));\n        const colorSelect = document.createElement(\"select\");\n        colorSelect.dataset.testid = \"building-color\";\n        colorSelect.style.display = \"block\";\n        controls.appendChild(colorSelect);\n        colorMap.forEach((color, name) => {\n            const option = document.createElement(\"option\");\n            option.innerHTML = name;\n            option.value = name;\n            option.selected = this.currentBuilding ?\n                color.asHex() === this.currentBuilding.owner.color.asHex() : false;\n            colorSelect.appendChild(option);\n        });\n        colorSelect.addEventListener(\"change\", () => {\n            const nextColor = colorMap.get(colorSelect.value) as Color | undefined;\n            if (nextColor && this.currentBuildingType) {\n                this.currentBuildingColor = nextColor.clone();\n                this.selectBuilding(this.currentBuildingType);\n            }\n        });\n        controls.appendChild(document.createTextNode(\"Selection level:\"));\n        const selectionDiv = document.createElement(\"div\");\n        controls.appendChild(selectionDiv);\n        [SelectionLevel.None, SelectionLevel.Hover, SelectionLevel.Selected].forEach((level) => {\n            console.log('current level', level, this.currentBuilding.rules);\n            const button = document.createElement(\"button\");\n            button.innerHTML = SelectionLevel[level];\n            button.dataset.testid = `building-selection-${SelectionLevel[level].toLowerCase()}`;\n            button.disabled = level === SelectionLevel.Selected &&\n                (!this.currentBuilding || !this.currentBuilding.rules.selectable);\n            button.addEventListener(\"click\", () => {\n                if (this.currentRenderable) {\n                    this.currentRenderable.selectionModel.setSelectionLevel(level);\n                    this.syncState();\n                }\n            });\n            selectionDiv.appendChild(button);\n        });\n        controls.appendChild(document.createTextNode(\"Animation type:\"));\n        this.animButtonsWrap = document.createElement(\"div\");\n        controls.appendChild(this.animButtonsWrap);\n        const stopButton = document.createElement(\"button\");\n        stopButton.dataset.testid = \"building-animation-stop\";\n        stopButton.innerHTML = \"Stop current\";\n        stopButton.style.display = \"block\";\n        stopButton.addEventListener(\"click\", () => this.stopCurrentAnimation());\n        controls.appendChild(stopButton);\n        controls.appendChild(document.createTextNode(\"Occupants:\"));\n        this.occupiedButtonsWrap = document.createElement(\"div\");\n        controls.appendChild(this.occupiedButtonsWrap);\n        controls.appendChild(document.createTextNode(\"Damage type:\"));\n        const normalButton = document.createElement(\"button\");\n        normalButton.dataset.testid = \"building-damage-normal\";\n        normalButton.innerHTML = \"NORMAL\";\n        normalButton.style.display = \"block\";\n        normalButton.addEventListener(\"click\", () => this.setDamageType(DamageType.NORMAL));\n        controls.appendChild(normalButton);\n        const yellowButton = document.createElement(\"button\");\n        yellowButton.dataset.testid = \"building-damage-yellow\";\n        yellowButton.innerHTML = \"YELLOW\";\n        yellowButton.style.display = \"block\";\n        yellowButton.addEventListener(\"click\", () => this.setDamageType(DamageType.CONDITION_YELLOW));\n        controls.appendChild(yellowButton);\n        const redButton = document.createElement(\"button\");\n        redButton.dataset.testid = \"building-damage-red\";\n        redButton.innerHTML = \"RED\";\n        redButton.style.display = \"block\";\n        redButton.addEventListener(\"click\", () => this.setDamageType(DamageType.CONDITION_RED));\n        controls.appendChild(redButton);\n        const destroyButton = document.createElement(\"button\");\n        destroyButton.dataset.testid = \"building-destroy\";\n        destroyButton.innerHTML = \"DESTROYED\";\n        destroyButton.style.display = \"block\";\n        destroyButton.addEventListener(\"click\", async () => {\n            if (this.currentBuilding) {\n                this.currentBuilding.isDestroyed = true;\n                this.currentBuilding.healthTrait.health = 0;\n                this.world.removeObject(this.currentBuilding);\n                this.currentBuilding.dispose();\n                this.currentBuilding = null;\n                this.currentRenderable = null;\n                this.currentBuildingType = undefined;\n                if (this.controlsElement) {\n                    this.controlsElement?.remove();\n                    this.controlsElement = undefined;\n                }\n                this.syncState();\n            }\n        });\n        controls.appendChild(destroyButton);\n        const repairButton = document.createElement(\"button\");\n        repairButton.dataset.testid = \"building-repair\";\n        repairButton.innerHTML = \"Toggle repair\";\n        repairButton.style.display = \"block\";\n        repairButton.disabled = !this.currentBuilding?.traits.get(AutoRepairTrait);\n        repairButton.addEventListener(\"click\", () => {\n            if (this.currentBuilding) {\n                const autoRepairTrait = this.currentBuilding.traits.get(AutoRepairTrait);\n                autoRepairTrait.setDisabled(!autoRepairTrait.isDisabled());\n                this.syncState();\n            }\n        });\n        controls.appendChild(repairButton);\n        controls.appendChild(document.createTextNode(\"Powered state:\"));\n        const inactiveButton = document.createElement(\"button\");\n        inactiveButton.dataset.testid = \"building-power-inactive\";\n        inactiveButton.innerHTML = \"INACTIVE\";\n        inactiveButton.style.display = \"block\";\n        inactiveButton.addEventListener(\"click\", () => this.setActiveState(false));\n        controls.appendChild(inactiveButton);\n        const activeButton = document.createElement(\"button\");\n        activeButton.dataset.testid = \"building-power-active\";\n        activeButton.innerHTML = \"ACTIVE\";\n        activeButton.style.display = \"block\";\n        activeButton.addEventListener(\"click\", () => this.setActiveState(true));\n        controls.appendChild(activeButton);\n        controls.appendChild(document.createTextNode(\"Warped out:\"));\n        const warpedCheckbox = document.createElement(\"input\");\n        warpedCheckbox.dataset.testid = \"building-warped\";\n        warpedCheckbox.type = \"checkbox\";\n        warpedCheckbox.style.display = \"block\";\n        warpedCheckbox.addEventListener(\"change\", (event) => {\n            if (this.currentBuilding && event.target) {\n                const target = event.target as HTMLInputElement;\n                this.currentBuilding.warpedOutTrait.debugSetActive(target.checked);\n                this.syncState();\n            }\n        });\n        controls.appendChild(warpedCheckbox);\n        this.hostElement?.appendChild(controls);\n        TestToolSupport.applyPanelTheme(controls);\n        this.syncState();\n    }\n    private static syncState(): void {\n        const building = this.currentBuilding;\n        const renderable = this.currentRenderable as any;\n        const selectionLevel = renderable?.selectionModel?.getSelectionLevel?.();\n        const health = building?.healthTrait?.health;\n        const damageType = health === undefined\n            ? null\n            : health <= 100 * this.rules.audioVisual.conditionRed\n                ? DamageType.CONDITION_RED\n                : health <= 100 * this.rules.audioVisual.conditionYellow\n                    ? DamageType.CONDITION_YELLOW\n                    : DamageType.NORMAL;\n        const animationButtons = this.animButtonsWrap\n            ? Array.from(this.animButtonsWrap.querySelectorAll('button')).map((button) => ({\n                label: button.textContent?.trim() ?? '',\n                enabled: !button.disabled,\n            }))\n            : [];\n        TestToolSupport.setState('building', {\n            availableBuildings: this.listEl?.querySelectorAll('a').length ?? 0,\n            selectedBuilding: this.currentBuildingType ?? null,\n            rendered: Boolean(renderable?.get3DObject?.() ?? renderable),\n            selectable: Boolean(building?.rules.selectable),\n            maxOccupants: building?.rules.maxNumberOccupants ?? 0,\n            occupantCount: building?.garrisonTrait?.units.length ?? 0,\n            selectionLevelValue: selectionLevel ?? null,\n            selectionLevel: TestToolSupport.enumLabel(SelectionLevel, selectionLevel),\n            selectionLevelOptions: TestToolSupport.enumOptions(SelectionLevel, [SelectionLevel.None, SelectionLevel.Hover, SelectionLevel.Selected]),\n            currentAnimationValue: renderable?.currentAnimType ?? null,\n            currentAnimation: TestToolSupport.enumLabel(AnimationType, renderable?.currentAnimType),\n            animationButtons,\n            damageTypeValue: damageType,\n            damageType: TestToolSupport.enumLabel(DamageType, damageType),\n            damageTypeOptions: TestToolSupport.enumOptions(DamageType, [DamageType.NORMAL, DamageType.CONDITION_YELLOW, DamageType.CONDITION_RED]),\n            powered: renderable?.powered ?? null,\n            repairAvailable: Boolean(building?.traits?.get(AutoRepairTrait)),\n            autoRepairDisabled: building?.traits?.get(AutoRepairTrait)?.isDisabled?.() ?? null,\n            warpedOut: Boolean(building?.warpedOutTrait?.isActive?.()),\n            ownerColor: building?.owner?.color?.asHexString?.() ?? null,\n        });\n    }\n    static buildBrowser(buildingRules: Map<string, any>): void {\n        const browser = (this.listEl = document.createElement(\"div\"));\n        browser.style.position = \"absolute\";\n        browser.style.right = \"0\";\n        browser.style.top = \"0\";\n        browser.style.height = \"600px\";\n        browser.style.width = \"200px\";\n        browser.style.overflowY = \"auto\";\n        browser.style.padding = \"5px\";\n        browser.style.background = \"rgba(255, 255, 255, 0.5)\";\n        browser.style.border = \"1px black solid\";\n        browser.appendChild(document.createTextNode(\"Building types:\"));\n        const buildingTypes: string[] = [];\n        buildingRules.forEach((rule, type) => {\n            const excludedTypes = [\"AMMOCRAT\", \"GADUMY\", \"GAGREEN\", \"CAARMR\"];\n            if (!excludedTypes.includes(type) &&\n                !/^CASIN/.test(type) &&\n                !/^CITY/.test(type)) {\n                buildingTypes.push(type);\n            }\n        });\n        buildingTypes.sort();\n        buildingTypes.forEach((type) => {\n            const link = document.createElement(\"a\");\n            link.dataset.buildingType = type;\n            link.style.display = \"block\";\n            link.textContent = type;\n            link.setAttribute(\"href\", \"javascript:;\");\n            link.addEventListener(\"click\", () => {\n                console.log(\"Selected building\", type);\n                this.selectBuilding(type);\n            });\n            browser.appendChild(link);\n        });\n        this.hostElement?.appendChild(browser);\n        TestToolSupport.applyPanelTheme(browser);\n        this.syncState();\n        setTimeout(() => {\n            this.selectBuilding(buildingTypes[0]);\n        }, 50);\n    }\n    private static buildHomeButton(): void {\n        const homeButton = document.createElement('button');\n        homeButton.innerHTML = '点此返回主页';\n        homeButton.style.cssText = `\n      position: fixed;\n      left: 50%;\n      top: 10px;\n      transform: translateX(-50%);\n      padding: 10px 20px;\n      background-color: rgba(0, 0, 0, 0.8);\n      color: white;\n      border: 2px solid rgba(255, 255, 255, 0.3);\n      border-radius: 6px;\n      cursor: pointer;\n      font-size: 16px;\n      font-weight: bold;\n      z-index: 1000;\n      transition: all 0.3s ease;\n      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n    `;\n        homeButton.onmouseover = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';\n            homeButton.style.transform = 'translateX(-50%) translateY(-2px)';\n            homeButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';\n        };\n        homeButton.onmouseout = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';\n            homeButton.style.transform = 'translateX(-50%) translateY(0)';\n            homeButton.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';\n        };\n        homeButton.onclick = () => {\n            window.location.hash = '/';\n        };\n        document.body.appendChild(homeButton);\n        this.disposables.add(() => homeButton.remove());\n    }\n    static destroy(): void {\n        this.renderer.dispose();\n        this.uiAnimationLoop.destroy();\n        this.listEl.remove();\n        if (this.controlsElement) {\n            this.controlsElement.remove();\n            this.controlsElement = undefined;\n        }\n        this.currentBuildingType = undefined;\n        this.currentBuildingColor = undefined;\n        this.disposables.dispose();\n        TestToolSupport.clearState('building');\n        try {\n            if ((PipOverlay as any)?.clearCaches) {\n                PipOverlay.clearCaches();\n            }\n            if ((TextureUtils as any)?.cache) {\n                TextureUtils.cache.forEach((tex: any) => tex.dispose?.());\n                TextureUtils.cache.clear();\n            }\n        }\n        catch (err) {\n            console.warn('[BuildingTester] Failed to clear caches during destroy:', err);\n        }\n    }\n}\n"
  },
  {
    "path": "src/tools/CameraZoomControls.ts",
    "content": "import { PointerEvents } from '../gui/PointerEvents';\nimport { CameraZoom } from '../engine/renderable/CameraZoom';\ninterface PointerEventData {\n    wheelDeltaY: number;\n}\nexport class CameraZoomControls {\n    private pointerEvents: PointerEvents;\n    private cameraZoom: CameraZoom;\n    private handleWheel: (event: PointerEventData) => void;\n    constructor(pointerEvents: PointerEvents, cameraZoom: CameraZoom) {\n        this.pointerEvents = pointerEvents;\n        this.cameraZoom = cameraZoom;\n        this.handleWheel = (event: PointerEventData) => {\n            this.cameraZoom.applyStep(event.wheelDeltaY > 0 ? -0.1 : 0.1);\n        };\n    }\n    init(): void {\n        this.pointerEvents.addEventListener(\"canvas\", \"wheel\", this.handleWheel);\n    }\n    destroy(): void {\n        this.pointerEvents.removeEventListener(\"canvas\", \"wheel\", this.handleWheel);\n    }\n}\n"
  },
  {
    "path": "src/tools/DevToolsApi.ts",
    "content": "import { BoxedVar } from '../util/BoxedVar';\nexport type CommandHandler = () => void;\ndeclare global {\n    interface Window {\n        r?: Record<string, any>;\n    }\n}\nexport class DevToolsApi {\n    private static cmdHandlers = new Map<string, CommandHandler>();\n    private static runtimeVars = new Map<string, BoxedVar<any>>();\n    static getPublicNamespace(): Record<string, any> {\n        if (!window.r) {\n            window.r = Object.create(null);\n        }\n        return window.r;\n    }\n    static registerCommand(name: string, handler: CommandHandler): void {\n        const namespace = this.getPublicNamespace();\n        if (namespace[name]) {\n            console.error(`Command ${name} is already registered`);\n            return;\n        }\n        this.cmdHandlers.set(name, handler);\n        Object.defineProperty(namespace, name, {\n            configurable: true,\n            get: () => {\n                const cmd = this.cmdHandlers.get(name);\n                return cmd ? cmd() : undefined;\n            },\n        });\n    }\n    static unregisterCommand(name: string): void {\n        if (this.cmdHandlers.has(name)) {\n            this.cmdHandlers.delete(name);\n            const namespace = this.getPublicNamespace();\n            delete namespace[name];\n        }\n        else {\n            console.error(`Command ${name} is not registered`);\n        }\n    }\n    static registerVar<T>(name: string, boxedVar: BoxedVar<T>): void {\n        const namespace = this.getPublicNamespace();\n        if (namespace[name]) {\n            console.error(`Runtime variable ${name} is already registered`);\n            return;\n        }\n        this.runtimeVars.set(name, boxedVar);\n        Object.defineProperty(namespace, name, {\n            configurable: true,\n            get: () => {\n                const variable = this.runtimeVars.get(name);\n                return variable ? variable.value : undefined;\n            },\n            set: (value: T) => {\n                const variable = this.runtimeVars.get(name);\n                if (variable) {\n                    variable.value = value;\n                }\n            },\n        });\n    }\n    static unregisterVar(name: string): void {\n        if (this.runtimeVars.has(name)) {\n            this.runtimeVars.delete(name);\n            const namespace = this.getPublicNamespace();\n            delete namespace[name];\n        }\n        else {\n            console.error(`Runtime variable ${name} is not registered`);\n        }\n    }\n    static listVars(): IterableIterator<string> {\n        return this.runtimeVars.keys();\n    }\n    static listCommands(): IterableIterator<string> {\n        return this.cmdHandlers.keys();\n    }\n}\n"
  },
  {
    "path": "src/tools/InfantryTester.ts",
    "content": "import { Renderer } from \"@/engine/gfx/Renderer\";\nimport { Engine } from \"@/engine/Engine\";\nimport { IsoCoords } from \"@/engine/IsoCoords\";\nimport { Player } from \"@/game/Player\";\nimport { WorldScene } from \"@/engine/renderable/WorldScene\";\nimport { Rules } from \"@/game/rules/Rules\";\nimport { MapGrid } from \"@/engine/renderable/entity/map/MapGrid\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\nimport { UiAnimationLoop } from \"@/engine/UiAnimationLoop\";\nimport { ImageFinder } from \"@/engine/ImageFinder\";\nimport { Art } from \"@/game/art/Art\";\nimport { RenderableFactory } from \"@/engine/renderable/entity/RenderableFactory\";\nimport { TheaterType } from \"@/engine/TheaterType\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { SpeedType } from \"@/game/type/SpeedType\";\nimport { StanceType } from \"@/game/gameobject/infantry/StanceType\";\nimport { InfDeathType } from \"@/game/gameobject/infantry/InfDeathType\";\nimport { Alliances } from \"@/game/Alliances\";\nimport { PlayerList } from \"@/game/PlayerList\";\nimport { SelectionLevel } from \"@/game/gameobject/selection/SelectionLevel\";\nimport { VeteranLevel } from \"@/game/gameobject/unit/VeteranLevel\";\nimport { PointerEvents } from \"@/gui/PointerEvents\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { UnitSelection } from \"@/game/gameobject/selection/UnitSelection\";\nimport { CameraZoomControls } from \"@/tools/CameraZoomControls\";\nimport { Lighting } from \"@/engine/Lighting\";\nimport { ObjectFactory } from \"@/game/gameobject/ObjectFactory\";\nimport { TileCollection } from \"@/game/map/TileCollection\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { MoveState } from \"@/game/gameobject/trait/MoveTrait\";\nimport { TileOccupation } from \"@/game/map/TileOccupation\";\nimport { Bridges } from \"@/game/map/Bridges\";\nimport { Strings } from \"@/data/Strings\";\nimport { MapBounds } from \"@/game/map/MapBounds\";\nimport { FlyerHelperMode } from \"@/engine/renderable/entity/unit/FlyerHelperMode\";\nimport { LightingDirector } from \"@/engine/gfx/lighting/LightingDirector\";\nimport { World } from \"@/game/World\";\nimport { RenderableManager } from \"@/engine/RenderableManager\";\nimport { VxlBuilderFactory } from \"@/engine/renderable/builder/VxlBuilderFactory\";\nimport { VxlGeometryPool } from \"@/engine/renderable/builder/vxlGeometry/VxlGeometryPool\";\nimport { VxlGeometryCache } from \"@/engine/gfx/geometry/VxlGeometryCache\";\nimport { ShadowQuality } from \"@/engine/renderable/entity/unit/ShadowQuality\";\nimport { CanvasMetrics } from \"@/gui/CanvasMetrics\";\nimport { PipOverlay } from \"@/engine/renderable/entity/PipOverlay\";\nimport { TextureUtils } from \"@/engine/gfx/TextureUtils\";\nimport { ResourceType } from \"@/engine/resourceConfigs\";\nimport { Color } from \"@/util/Color\";\nimport { TestToolSupport, type TestToolRuntimeContext } from \"@/tools/TestToolSupport\";\ndeclare const THREE: any;\nexport class InfantryTester {\n    private static disposables = new CompositeDisposable();\n    private static renderer: Renderer;\n    private static theater: any;\n    private static rules: Rules;\n    private static art: Art;\n    private static images: any;\n    private static uiAnimationLoop: UiAnimationLoop;\n    private static worldScene: WorldScene;\n    private static world: World;\n    private static currentRenderable: any;\n    private static currentInfantry: any;\n    private static listEl: HTMLDivElement;\n    private static controlsEl: HTMLDivElement | undefined;\n    private static hostElement?: HTMLElement;\n    private static vxlBuilderFactory: VxlBuilderFactory;\n    private static autoRotate: boolean = false;\n    private static currentInfantryType?: string;\n    private static currentInfantryColor?: Color;\n    static async main(_args: any, context: TestToolRuntimeContext = {}): Promise<void> {\n        await TestToolSupport.ensureTheater(TheaterType.Snow, context.cdnResourceLoader, [ResourceType.Anims]);\n        const hostElement = this.hostElement = TestToolSupport.prepareHost(context, 1224, 600);\n        const renderer = (this.renderer = new Renderer(800, 600));\n        renderer.init(hostElement);\n        TestToolSupport.placeRendererCanvas(renderer, 212, 0);\n        renderer.initStats(document.body);\n        this.buildHomeButton();\n        const worldScene = WorldScene.factory({ x: 0, y: 0, width: 800, height: 600 }, new BoxedVar(true), new BoxedVar(ShadowQuality.High));\n        this.disposables.add(worldScene);\n        (worldScene.scene.background as any) = new THREE.Color(12632256);\n        IsoCoords.init({ x: 0, y: 0 });\n        this.theater = await Engine.loadTheater(TheaterType.Snow);\n        const rules = new Rules(Engine.getRules());\n        this.rules = rules;\n        this.art = new Art(rules as any, Engine.getArt(), undefined as any, console);\n        this.images = Engine.getImages();\n        this.buildBrowser(rules[\"infantryRules\"] as Map<string, any>);\n        const canvasMetrics = new CanvasMetrics(renderer.getCanvas(), window);\n        canvasMetrics.init();\n        this.disposables.add(() => canvasMetrics.dispose());\n        const pointerEvents = new PointerEvents(renderer as any, { x: 0, y: 0 }, document, canvasMetrics as any);\n        const cameraZoomControls = new CameraZoomControls(pointerEvents, worldScene.cameraZoom);\n        this.disposables.add(cameraZoomControls, pointerEvents);\n        cameraZoomControls.init();\n        renderer.addScene(worldScene);\n        const uiAnimationLoop = (this.uiAnimationLoop = new UiAnimationLoop(renderer));\n        uiAnimationLoop.start();\n        this.worldScene = worldScene;\n        this.vxlBuilderFactory = new VxlBuilderFactory(new VxlGeometryPool(new VxlGeometryCache(null, null)), false, worldScene.camera);\n        this.addGrid();\n        this.syncState();\n    }\n    static addGrid(): void {\n        const mapGrid = new MapGrid({ width: 10, height: 10 });\n        const gridObject = mapGrid.get3DObject();\n        const container = new THREE.Object3D();\n        container.add(gridObject);\n        this.worldScene.scene.add(container);\n    }\n    static selectInfantry(infantryType: string): void {\n        if (this.currentInfantry && !this.currentInfantry.isDisposed) {\n            this.world.removeObject(this.currentInfantry);\n            this.currentInfantry.dispose();\n        }\n        const player = new Player(\"Player\");\n        this.disposables.add(player);\n        const playerList = new PlayerList();\n        playerList.addPlayer(player);\n        const alliances = new Alliances(playerList);\n        const unitSelection = new UnitSelection();\n        const lighting = new Lighting();\n        this.disposables.add(lighting);\n        const renderableFactory = new RenderableFactory(new BoxedVar(player) as any, unitSelection as any, alliances as any, this.rules as any, this.art as any, undefined as any, new ImageFinder(this.images, this.theater) as any, Engine.getPalettes() as any, Engine.getVoxels() as any, Engine.getVoxelAnims() as any, this.theater as any, this.worldScene.camera as any, new Lighting(), new LightingDirector(new Lighting().mapLighting as any, this.renderer as any, new BoxedVar(1) as any) as any, new BoxedVar(false) as any, new BoxedVar(false) as any, new BoxedVar(2) as any, null as any, new Strings({ TXT_PRIMARY: \"Primary\" }) as any, new BoxedVar(FlyerHelperMode.Selected) as any, new BoxedVar(false) as any, this.vxlBuilderFactory as any, new Map() as any);\n        const tileCollection = new TileCollection([\n            { rx: 0, ry: 0, dx: 0, dy: 0, z: 0, tileNum: 0, subTile: 0 },\n            { rx: 1, ry: 0, dx: 1, dy: 0, z: 0, tileNum: 0, subTile: 0 },\n            { rx: 0, ry: 1, dx: 0, dy: 1, z: 0, tileNum: 0, subTile: 0 },\n            { rx: 1, ry: 1, dx: 1, dy: 1, z: 0, tileNum: 0, subTile: 0 },\n        ] as any, this.theater.tileSets as any, this.rules.general as any, (_min: number, _max: number) => 0);\n        const tileOccupation = new TileOccupation(tileCollection);\n        const mapBounds = new MapBounds();\n        const bridges = new Bridges(this.theater.tileSets, tileCollection, tileOccupation, mapBounds, this.rules);\n        const infantry = (this.currentInfantry = new ObjectFactory(tileCollection, tileOccupation, bridges, new BoxedVar(1)).create(ObjectType.Infantry, infantryType, this.rules as any, this.art as any));\n        this.currentInfantryType = infantryType;\n        const selectedColor = this.currentInfantryColor?.clone()\n            ?? this.rules.getMultiplayerColors().get(\"DarkRed\")?.clone()\n            ?? new Color(255, 0, 0);\n        player.color = selectedColor;\n        this.currentInfantryColor = selectedColor.clone();\n        infantry.owner = player;\n        infantry.position.tile = { rx: 1, ry: 1, z: 0, rampType: 0 };\n        const world = (this.world = new World());\n        const renderableManager = new RenderableManager(world, this.worldScene, this.worldScene.camera, renderableFactory);\n        renderableManager.init();\n        this.disposables.add(renderableManager);\n        world.spawnObject(infantry);\n        const renderable = (this.currentRenderable = renderableManager.getRenderableByGameObject(infantry));\n        renderable.selectionModel.setSelectionLevel(SelectionLevel.Selected);\n        renderable.selectionModel.setControlGroupNumber(3);\n        this.buildControls();\n        this.syncState();\n    }\n    static buildControls(): void {\n        if (this.controlsEl) {\n            this.controlsEl.remove();\n        }\n        const controls = (this.controlsEl = document.createElement(\"div\"));\n        controls.dataset.testid = \"infantry-controls\";\n        controls.style.position = \"absolute\";\n        controls.style.left = \"0\";\n        controls.style.top = \"0\";\n        controls.style.width = \"200px\";\n        controls.style.padding = \"5px\";\n        controls.style.background = \"rgba(255, 255, 255, 0.5)\";\n        controls.style.border = \"1px black solid\";\n        controls.appendChild(document.createTextNode(\"Remap color:\"));\n        const colorMap = new Map(this.rules.getMultiplayerColors());\n        const colorSelect = document.createElement(\"select\");\n        colorSelect.dataset.testid = \"infantry-color\";\n        colorSelect.style.display = \"block\";\n        colorSelect.addEventListener(\"change\", () => {\n            const nextColor = colorMap.get(colorSelect.value);\n            if (nextColor && this.currentInfantryType) {\n                this.currentInfantryColor = nextColor.clone();\n                this.selectInfantry(this.currentInfantryType);\n            }\n        });\n        controls.appendChild(colorSelect);\n        colorMap.forEach((color, name) => {\n            const option = document.createElement(\"option\");\n            option.innerHTML = name;\n            option.value = name;\n            option.selected = color.asHex() === this.currentInfantry.owner.color.asHex();\n            colorSelect.appendChild(option);\n        });\n        controls.appendChild(document.createTextNode(\"Selection level:\"));\n        const selDiv = document.createElement(\"div\");\n        controls.appendChild(selDiv);\n        [SelectionLevel.None, SelectionLevel.Hover, SelectionLevel.Selected].forEach((level) => {\n            const btn = document.createElement(\"button\");\n            btn.innerHTML = SelectionLevel[level];\n            btn.dataset.testid = `infantry-selection-${SelectionLevel[level].toLowerCase()}`;\n            btn.addEventListener(\"click\", () => {\n                this.currentRenderable.selectionModel.setSelectionLevel(level);\n                this.syncState();\n            });\n            selDiv.appendChild(btn);\n        });\n        controls.appendChild(document.createTextNode(\"Veteran level:\"));\n        const vetDiv = document.createElement(\"div\");\n        controls.appendChild(vetDiv);\n        [VeteranLevel.None, VeteranLevel.Veteran, VeteranLevel.Elite].forEach((lvl) => {\n            const btn = document.createElement(\"button\");\n            btn.innerHTML = VeteranLevel[lvl];\n            btn.dataset.testid = `infantry-veteran-${VeteranLevel[lvl].toLowerCase()}`;\n            btn.addEventListener(\"click\", () => {\n                if (this.currentInfantry.veteranTrait) {\n                    this.currentInfantry.veteranTrait.veteranLevel = lvl;\n                }\n                this.syncState();\n            });\n            vetDiv.appendChild(btn);\n        });\n        controls.appendChild(document.createTextNode(\"SubCell:\"));\n        const subCell = document.createElement(\"select\");\n        subCell.dataset.testid = \"infantry-subcell\";\n        subCell.style.display = \"block\";\n        subCell.addEventListener(\"change\", () => {\n            this.currentInfantry.position.subCell = Number(subCell.value);\n            this.syncState();\n        });\n        controls.appendChild(subCell);\n        for (let i = 0; i < 5; i++) {\n            const opt = document.createElement(\"option\");\n            opt.innerHTML = String(i);\n            opt.value = String(i);\n            subCell.appendChild(opt);\n        }\n        controls.appendChild(document.createTextNode(\"Zone:\"));\n        this.createZoneSelect(controls);\n        controls.appendChild(document.createTextNode(\"Stance:\"));\n        this.createStanceSelect(controls);\n        controls.appendChild(document.createTextNode(\"isMoving:\"));\n        const moving = document.createElement(\"input\");\n        moving.dataset.testid = \"infantry-moving\";\n        moving.type = \"checkbox\";\n        moving.style.display = \"block\";\n        moving.addEventListener(\"change\", (e) => {\n            this.currentInfantry.moveTrait.moveState = (e.target as HTMLInputElement).checked\n                ? MoveState.Moving\n                : MoveState.Idle;\n            this.syncState();\n        });\n        controls.appendChild(moving);\n        controls.appendChild(document.createTextNode(\"isFiring:\"));\n        const firing = document.createElement(\"input\");\n        firing.dataset.testid = \"infantry-firing\";\n        firing.type = \"checkbox\";\n        firing.disabled = !this.currentInfantry.rules.primary;\n        firing.style.display = \"block\";\n        firing.addEventListener(\"change\", (e) => {\n            this.currentInfantry.isFiring = (e.target as HTMLInputElement).checked;\n            this.syncState();\n        });\n        controls.appendChild(firing);\n        controls.appendChild(document.createTextNode(\"isPanicked:\"));\n        const panic = document.createElement(\"input\");\n        panic.dataset.testid = \"infantry-panic\";\n        panic.type = \"checkbox\";\n        panic.disabled = !this.currentInfantry.rules.fraidycat;\n        panic.style.display = \"block\";\n        panic.addEventListener(\"change\", (e) => {\n            this.currentInfantry.isPanicked = (e.target as HTMLInputElement).checked;\n            this.syncState();\n        });\n        controls.appendChild(panic);\n        controls.appendChild(document.createTextNode(\"Warped out:\"));\n        const warped = document.createElement(\"input\");\n        warped.dataset.testid = \"infantry-warped\";\n        warped.type = \"checkbox\";\n        warped.style.display = \"block\";\n        warped.addEventListener(\"change\", (e) => {\n            this.currentInfantry.warpedOutTrait.debugSetActive((e.target as HTMLInputElement).checked);\n            this.syncState();\n        });\n        controls.appendChild(warped);\n        this.createDeathSelect(controls);\n        controls.appendChild(document.createElement(\"hr\"));\n        controls.appendChild(document.createTextNode(\"Facing (0-7):\"));\n        const facingSelect = document.createElement(\"select\");\n        facingSelect.dataset.testid = \"infantry-facing\";\n        facingSelect.style.display = \"block\";\n        const facingLabels = [\"0 上\", \"1 左上\", \"2 左\", \"3 左下\", \"4 下\", \"5 右下\", \"6 右\", \"7 右上\"];\n        for (let i = 0; i < 8; i++) {\n            const opt = document.createElement(\"option\");\n            opt.value = String(i);\n            opt.innerHTML = facingLabels[i];\n            facingSelect.appendChild(opt);\n        }\n        facingSelect.addEventListener(\"change\", () => {\n            const n = Number(facingSelect.value) % 8;\n            const dir = (45 + (360 * n) / 8) % 360;\n            this.currentInfantry.direction = dir;\n            this.syncState();\n        });\n        controls.appendChild(facingSelect);\n        controls.appendChild(document.createTextNode(\"Direction (0-359):\"));\n        const dirWrap = document.createElement(\"div\");\n        const dirInput = document.createElement(\"input\");\n        dirInput.dataset.testid = \"infantry-direction-input\";\n        dirInput.type = \"number\";\n        dirInput.min = \"0\";\n        dirInput.max = \"359\";\n        dirInput.style.width = \"80px\";\n        dirInput.value = String(this.currentInfantry?.direction ?? 0);\n        const dirBtn = document.createElement(\"button\");\n        dirBtn.dataset.testid = \"infantry-direction-apply\";\n        dirBtn.innerHTML = \"Apply\";\n        dirBtn.addEventListener(\"click\", () => {\n            let v = Number(dirInput.value);\n            if (isNaN(v))\n                v = 0;\n            v = ((Math.floor(v) % 360) + 360) % 360;\n            this.currentInfantry.direction = v;\n            this.syncState();\n        });\n        dirWrap.appendChild(dirInput);\n        dirWrap.appendChild(dirBtn);\n        controls.appendChild(dirWrap);\n        controls.appendChild(document.createTextNode(\"Auto rotate:\"));\n        const autoRot = document.createElement(\"input\");\n        autoRot.dataset.testid = \"infantry-auto-rotate\";\n        autoRot.type = \"checkbox\";\n        autoRot.style.display = \"block\";\n        autoRot.checked = this.autoRotate;\n        autoRot.addEventListener(\"change\", (e) => {\n            this.autoRotate = (e.target as HTMLInputElement).checked;\n            this.syncState();\n        });\n        controls.appendChild(autoRot);\n        this.hostElement?.appendChild(controls);\n        TestToolSupport.applyPanelTheme(controls);\n        this.syncState();\n    }\n    private static getZoneValues(): ZoneType[] {\n        const values = [ZoneType.Ground];\n        if (this.currentInfantry?.rules.consideredAircraft) {\n            values.push(ZoneType.Air);\n        }\n        if (this.currentInfantry?.rules.speedType === SpeedType.Amphibious) {\n            values.push(ZoneType.Water);\n        }\n        return values;\n    }\n    private static getStanceValues(): StanceType[] {\n        const values = [StanceType.None, StanceType.Guard, StanceType.Paradrop, StanceType.Cheer];\n        if (!this.currentInfantry?.rules.fearless) {\n            values.push(StanceType.Prone);\n        }\n        if (this.currentInfantry?.rules.deployer) {\n            values.push(StanceType.Deployed);\n        }\n        return values;\n    }\n    private static syncState(): void {\n        const infantry = this.currentInfantry;\n        const renderable = this.currentRenderable;\n        const selectionLevel = renderable?.selectionModel?.getSelectionLevel?.();\n        const veteranLevel = infantry?.veteranTrait?.veteranLevel ?? infantry?.veteranLevel ?? VeteranLevel.None;\n        TestToolSupport.setState('infantry', {\n            availableInfantry: this.listEl?.querySelectorAll('a').length ?? 0,\n            selectedInfantry: this.currentInfantryType ?? null,\n            rendered: Boolean(renderable?.get3DObject?.() ?? renderable),\n            selectionLevelValue: selectionLevel ?? null,\n            selectionLevel: TestToolSupport.enumLabel(SelectionLevel, selectionLevel),\n            selectionLevelOptions: TestToolSupport.enumOptions(SelectionLevel, [SelectionLevel.None, SelectionLevel.Hover, SelectionLevel.Selected]),\n            veteranLevelValue: infantry ? veteranLevel : null,\n            veteranLevel: infantry ? TestToolSupport.enumLabel(VeteranLevel, veteranLevel) : null,\n            veteranLevelOptions: TestToolSupport.enumOptions(VeteranLevel, [VeteranLevel.None, VeteranLevel.Veteran, VeteranLevel.Elite]),\n            subCell: infantry?.position?.subCell ?? null,\n            subCellOptions: [0, 1, 2, 3, 4].map(String),\n            zoneValue: infantry?.zone ?? null,\n            zone: TestToolSupport.enumLabel(ZoneType, infantry?.zone),\n            zoneOptions: TestToolSupport.enumOptions(ZoneType, this.getZoneValues()),\n            stanceValue: infantry?.stance ?? null,\n            stance: TestToolSupport.enumLabel(StanceType, infantry?.stance),\n            stanceOptions: TestToolSupport.enumOptions(StanceType, this.getStanceValues()),\n            moveState: infantry?.moveTrait?.moveState ?? null,\n            isMoving: infantry?.moveTrait?.moveState === MoveState.Moving,\n            isFiring: Boolean(infantry?.isFiring),\n            isPanicked: Boolean(infantry?.isPanicked),\n            warpedOut: Boolean(infantry?.warpedOutTrait?.isActive?.()),\n            direction: infantry?.direction ?? null,\n            autoRotate: this.autoRotate,\n            ownerColor: infantry?.owner?.color?.asHexString?.() ?? null,\n        });\n    }\n    static createZoneSelect(container: HTMLElement): void {\n        const select = document.createElement(\"select\");\n        select.dataset.testid = \"infantry-zone\";\n        select.style.display = \"block\";\n        select.addEventListener(\"change\", () => {\n            this.currentInfantry.zone = Number(select.value);\n            this.syncState();\n        });\n        container.appendChild(select);\n        this.getZoneValues().forEach((zoneValue) => {\n            const option = document.createElement(\"option\");\n            option.value = String(zoneValue);\n            option.innerHTML = ZoneType[zoneValue];\n            option.selected = zoneValue === this.currentInfantry?.zone;\n            select.appendChild(option);\n        });\n    }\n    static createStanceSelect(container: HTMLElement): void {\n        const select = document.createElement(\"select\");\n        select.dataset.testid = \"infantry-stance\";\n        select.style.display = \"block\";\n        select.addEventListener(\"change\", () => {\n            this.currentInfantry.stance = Number(select.value);\n            this.syncState();\n        });\n        container.appendChild(select);\n        this.getStanceValues().forEach((stanceValue) => {\n            const option = document.createElement(\"option\");\n            option.value = String(stanceValue);\n            option.innerHTML = StanceType[stanceValue];\n            option.selected = stanceValue === this.currentInfantry?.stance;\n            select.appendChild(option);\n        });\n    }\n    static createDeathSelect(container: HTMLElement): void {\n        container.appendChild(document.createTextNode(\"Death\"));\n        const select = document.createElement(\"select\");\n        select.dataset.testid = \"infantry-death\";\n        let i = 1;\n        let name: string | undefined = InfDeathType[i] as any;\n        while (name !== undefined) {\n            const option = document.createElement(\"option\");\n            option.innerHTML = name;\n            option.value = String(i);\n            option.disabled = !this.currentInfantry.rules.isHuman && ![InfDeathType.Gunfire, InfDeathType.Explode].includes(i as any);\n            select.appendChild(option);\n            name = InfDeathType[++i] as any;\n        }\n        container.appendChild(select);\n        const kill = document.createElement(\"button\");\n        kill.dataset.testid = \"infantry-kill\";\n        kill.style.display = \"block\";\n        kill.style.color = \"red\";\n        kill.innerHTML = \"KILL\";\n        kill.addEventListener(\"click\", async () => {\n            this.currentInfantry.isDestroyed = true;\n            this.currentInfantry.infDeathType = Number(select.value);\n            this.world.removeObject(this.currentInfantry);\n            this.currentInfantry.dispose();\n            this.currentInfantry = undefined;\n            this.currentInfantryType = undefined;\n            if (this.controlsEl) {\n                this.controlsEl?.remove();\n                this.controlsEl = undefined;\n            }\n            this.syncState();\n        });\n        container.appendChild(kill);\n    }\n    static buildBrowser(infantryRules: Map<string, any>): void {\n        const allTypes = [...infantryRules.keys()];\n        const withArt = allTypes.filter((name) => this.art.hasObject(name, ObjectType.Infantry));\n        const missingArt = allTypes.filter((name) => !this.art.hasObject(name, ObjectType.Infantry));\n        console.info(`[InfantryTester] Rules infantry types: ${allTypes.length}, renderable (has art): ${withArt.length}, no-art: ${missingArt.length}`);\n        if (missingArt.length) {\n            const artIni = this.art.getIni?.() as any;\n            const sample = missingArt.slice(0, 20).map((name) => {\n                let imageName = \"<unknown>\";\n                try {\n                    imageName = (this.rules.getObject(name, ObjectType.Infantry) as any).imageName;\n                }\n                catch { }\n                const hasName = !!artIni?.getSection?.(name);\n                const hasImage = !!artIni?.getSection?.(imageName);\n                return { name, imageName, hasName, hasImage };\n            });\n            console.warn('[InfantryTester] First missing-art infantry details:', sample);\n        }\n        const browser = (this.listEl = document.createElement(\"div\"));\n        browser.style.position = \"absolute\";\n        browser.style.right = \"0\";\n        browser.style.top = \"0\";\n        browser.style.height = \"600px\";\n        browser.style.width = \"200px\";\n        browser.style.overflowY = \"auto\";\n        browser.style.padding = \"5px\";\n        browser.style.width = \"200px\";\n        browser.style.background = \"rgba(255, 255, 255, 0.5)\";\n        browser.style.border = \"1px black solid\";\n        browser.appendChild(document.createTextNode(\"Infantry types:\"));\n        const types = withArt.sort();\n        types.forEach((name) => {\n            const link = document.createElement(\"a\");\n            link.dataset.infantryType = name;\n            link.style.display = \"block\";\n            link.textContent = name;\n            link.setAttribute(\"href\", \"javascript:;\");\n            link.addEventListener(\"click\", () => {\n                console.log(\"Selected infantry\", name);\n                this.selectInfantry(name);\n            });\n            browser.appendChild(link);\n        });\n        this.hostElement?.appendChild(browser);\n        TestToolSupport.applyPanelTheme(browser);\n        this.syncState();\n        setTimeout(() => {\n            this.selectInfantry(types[0]);\n            this.animateInfantry();\n        }, 50);\n    }\n    private static animateInfantry(): void {\n        if (!this.currentInfantry?.isDisposed && this.autoRotate) {\n            this.currentInfantry.direction = (this.currentInfantry.direction + 1) % 360;\n        }\n        setTimeout(() => this.animateInfantry(), 50);\n    }\n    private static buildHomeButton(): void {\n        const homeButton = document.createElement('button');\n        homeButton.innerHTML = '点此返回主页';\n        homeButton.style.cssText = `\n      position: fixed;\n      left: 50%;\n      top: 10px;\n      transform: translateX(-50%);\n      padding: 10px 20px;\n      background-color: rgba(0, 0, 0, 0.8);\n      color: white;\n      border: 2px solid rgba(255, 255, 255, 0.3);\n      border-radius: 6px;\n      cursor: pointer;\n      font-size: 16px;\n      font-weight: bold;\n      z-index: 1000;\n      transition: all 0.3s ease;\n      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n    `;\n        homeButton.onmouseover = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';\n            homeButton.style.transform = 'translateX(-50%) translateY(-2px)';\n            homeButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';\n        };\n        homeButton.onmouseout = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';\n            homeButton.style.transform = 'translateX(-50%) translateY(0)';\n            homeButton.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';\n        };\n        homeButton.onclick = () => {\n            window.location.hash = '/';\n        };\n        document.body.appendChild(homeButton);\n        this.disposables.add(() => homeButton.remove());\n    }\n    static destroy(): void {\n        this.renderer.dispose();\n        this.uiAnimationLoop.destroy();\n        this.listEl?.remove();\n        if (this.controlsEl) {\n            this.controlsEl.remove();\n            this.controlsEl = undefined;\n        }\n        this.currentInfantryType = undefined;\n        this.currentInfantryColor = undefined;\n        this.disposables.dispose();\n        TestToolSupport.clearState('infantry');\n        try {\n            if ((PipOverlay as any)?.clearCaches) {\n                PipOverlay.clearCaches();\n            }\n            if ((TextureUtils as any)?.cache) {\n                TextureUtils.cache.forEach((tex: any) => tex.dispose?.());\n                TextureUtils.cache.clear();\n            }\n        }\n        catch (err) {\n            console.warn('[InfantryTester] Failed to clear caches during destroy:', err);\n        }\n    }\n}\n"
  },
  {
    "path": "src/tools/LiveInteractionTester.ts",
    "content": "import * as THREE from 'three';\nimport { Art } from '@/game/art/Art';\nimport { BoxedVar } from '@/util/BoxedVar';\nimport { Color } from '@/util/Color';\nimport { CompositeDisposable } from '@/util/disposable/CompositeDisposable';\nimport { CanvasMetrics } from '@/gui/CanvasMetrics';\nimport { Pointer } from '@/gui/Pointer';\nimport { UiScene } from '@/gui/UiScene';\nimport { JsxRenderer } from '@/gui/jsx/JsxRenderer';\nimport { GeneralOptions } from '@/gui/screen/options/GeneralOptions';\nimport { WorldView } from '@/gui/screen/game/WorldView';\nimport { Minimap } from '@/gui/screen/game/component/Minimap';\nimport { ReplayLoadingScreenApi } from '@/gui/screen/game/loadingScreen/ReplayLoadingScreenApi';\nimport { WorldInteractionFactory } from '@/gui/screen/game/worldInteraction/WorldInteractionFactory';\nimport { Engine } from '@/engine/Engine';\nimport { IsoCoords } from '@/engine/IsoCoords';\nimport { Renderer } from '@/engine/gfx/Renderer';\nimport { WorldScene } from '@/engine/renderable/WorldScene';\nimport { TheaterType } from '@/engine/TheaterType';\nimport { ResourceType } from '@/engine/resourceConfigs';\nimport { UiAnimationLoop } from '@/engine/UiAnimationLoop';\nimport { ConsoleVars } from '@/ConsoleVars';\nimport { GameMap } from '@/game/GameMap';\nimport { GameFactory } from '@/game/GameFactory';\nimport { Game } from '@/game/Game';\nimport { BuildStatus } from '@/game/gameobject/Building';\nimport { Coords } from '@/game/Coords';\nimport { Infantry } from '@/game/gameobject/Infantry';\nimport { AttackMoveOrder } from '@/game/order/AttackMoveOrder';\nimport { Player } from '@/game/Player';\nimport { Rules } from '@/game/rules/Rules';\nimport { SpeedType } from '@/game/type/SpeedType';\nimport { TileSets } from '@/game/theater/TileSets';\nimport { MapPanningHelper } from '@/engine/util/MapPanningHelper';\nimport { RenderableManager } from '@/engine/RenderableManager';\nimport { VxlGeometryPool } from '@/engine/renderable/builder/vxlGeometry/VxlGeometryPool';\nimport { VxlGeometryCache } from '@/engine/gfx/geometry/VxlGeometryCache';\nimport { ImageFinder } from '@/engine/ImageFinder';\nimport { MissingImageError } from '@/engine/ImageFinder';\nimport { ObjectType } from '@/engine/type/ObjectType';\nimport { BuildingAnimArtProps } from '@/engine/renderable/entity/building/BuildingAnimArtProps';\nimport { TestToolSupport, type TestToolRuntimeContext } from '@/tools/TestToolSupport';\nimport { RadialTileFinder } from '@/game/map/tileFinder/RadialTileFinder';\nimport { NO_TEAM_ID } from '@/game/gameopts/constants';\n\ntype StringsLike = {\n    get(key: string): string | undefined;\n};\n\ntype LiveInteractionRuntimeDeps = {\n    generalOptions?: GeneralOptions;\n    runtimeVars?: ConsoleVars;\n};\n\ntype BattleSideId = 'left' | 'right';\ntype LiveMode = 'mock' | 'live';\ntype InteractionKind =\n    | 'room-enter'\n    | 'gift'\n    | 'guard'\n    | 'super-chat'\n    | 'like'\n    | 'danmaku'\n    | 'live-start'\n    | 'live-end'\n    | 'unknown';\n\ntype NormalizedInteractionEvent = {\n    id: string;\n    kind: InteractionKind;\n    cmd: string;\n    timestamp: number;\n    uname?: string;\n    openId?: string;\n    message?: string;\n    giftName?: string;\n    giftNum?: number;\n    price?: number;\n    totalPrice?: number;\n    likeCount?: number;\n    guardLevel?: number;\n    raw?: Record<string, unknown>;\n};\n\ntype RuntimeStatus = {\n    mode: LiveMode;\n    connected: boolean;\n    connecting: boolean;\n    sessionActive: boolean;\n    eventCount: number;\n    lastError?: string | null;\n    lastEventAt?: number | null;\n    anchor?: {\n        roomId?: number;\n        uname?: string;\n        openId?: string;\n    } | null;\n};\n\ntype UnitCatalog = {\n    infantryBasic: string;\n    infantryElite: string;\n    vehicleLight: string;\n    vehicleHeavy: string;\n};\n\ntype WavePlan = {\n    side: BattleSideId;\n    reason: string;\n    infantryBasic?: number;\n    infantryElite?: number;\n    vehicleLight?: number;\n    vehicleHeavy?: number;\n    veteran?: boolean;\n    viewerLabel?: string;\n};\n\ntype SideStats = {\n    totalSpawned: number;\n    lastReinforcementAt?: number;\n};\n\ntype BattleContext = {\n    game: Game;\n    gameMap: GameMap;\n    worldView: WorldView;\n    uiScene: UiScene;\n    minimap: Minimap;\n    pointer: Pointer;\n    canvasMetrics: CanvasMetrics;\n    worldInteraction: any;\n    renderableManager: RenderableManager;\n    worldScene: WorldScene;\n    leftPlayer: Player;\n    rightPlayer: Player;\n    leftAnchor: any;\n    rightAnchor: any;\n    leftTarget: any;\n    rightTarget: any;\n    leftBase: any;\n    rightBase: any;\n    centerTile: any;\n    localBounds: {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    };\n    unitCatalog: UnitCatalog;\n};\n\ntype UiRefs = {\n    host?: HTMLDivElement;\n    canvasPane?: HTMLDivElement;\n    overlayPane?: HTMLDivElement;\n    minimapShell?: HTMLDivElement;\n    panel?: HTMLDivElement;\n    panelContent?: HTMLDivElement;\n    panelToggle?: HTMLButtonElement;\n    log?: HTMLDivElement;\n    statusSummary?: HTMLDivElement;\n    leftSummary?: HTMLDivElement;\n    rightSummary?: HTMLDivElement;\n    catalogSummary?: HTMLDivElement;\n    mappingSummary?: HTMLPreElement;\n    statusBadge?: HTMLDivElement;\n    modeSelect?: HTMLSelectElement;\n    appIdInput?: HTMLInputElement;\n    accessKeyIdInput?: HTMLInputElement;\n    accessSecretInput?: HTMLInputElement;\n    codeInput?: HTMLInputElement;\n    credentialsGrid?: HTMLDivElement;\n    danmakuInput?: HTMLInputElement;\n};\n\ntype LiveLoadingScreenSession = {\n    api: ReplayLoadingScreenApi;\n    uiScene: UiScene;\n    rootEl: HTMLDivElement;\n    dispose: () => void;\n};\n\nconst TOOL_NAME = 'liveinteraction';\nconst DEFAULT_VIEWPORT_WIDTH = 1280;\nconst DEFAULT_VIEWPORT_HEIGHT = 720;\nconst PANEL_WIDTH = 360;\nconst PANEL_MIN_WIDTH = 300;\nconst PANEL_MARGIN = 16;\nconst PANEL_COLLAPSED_VISIBLE_WIDTH = 30;\nconst MIN_BATTLE_VIEWPORT_WIDTH = 720;\nconst MINIMAP_SIZE = 248;\nconst MINIMAP_MARGIN = 16;\nconst STATUS_POLL_MS = 3000;\nconst GAME_TICK_MS = 33;\nconst ORDER_REFRESH_MS = 1200;\nconst MAX_LOG_ENTRIES = 14;\nconst BASE_HP_DISPLAY_MAX = 100;\nconst MAX_UNIT_LABELS_PER_WAVE = 2;\nconst API_BASE = '/api/live-interaction';\nconst MIN_BATTLE_ZOOM = 0.75;\nconst MAX_BATTLE_ZOOM = 2.4;\nconst LOADING_SCREEN_MIN_DURATION_MS = 350;\nexport class LiveInteractionTester {\n    private static disposables = new CompositeDisposable();\n    private static renderer?: Renderer;\n    private static uiAnimationLoop?: UiAnimationLoop;\n    private static renderableManager?: RenderableManager;\n    private static battle?: BattleContext;\n    private static ui: UiRefs = {};\n    private static eventSource?: EventSource;\n    private static gameTickTimer?: number;\n    private static statusPollTimer?: number;\n    private static orderRefreshTimer?: number;\n    private static panelCollapsed = false;\n    private static state = {\n        ready: false,\n        mode: 'mock' as LiveMode,\n        runtimeStatus: {\n            mode: 'mock',\n            connected: false,\n            connecting: false,\n            sessionActive: false,\n            eventCount: 0,\n            lastError: null,\n            lastEventAt: null,\n            anchor: null,\n        } as RuntimeStatus,\n        left: {\n            totalSpawned: 0,\n            lastReinforcementAt: undefined,\n        } as SideStats,\n        right: {\n            totalSpawned: 0,\n            lastReinforcementAt: undefined,\n        } as SideStats,\n        lastEvent: null as NormalizedInteractionEvent | null,\n        recentEvents: [] as Array<{\n            at: number;\n            text: string;\n        }>,\n    };\n\n    static async main(_mixFileLoader: any, gameMapFile: any, parentElement: HTMLElement, strings: StringsLike, context: TestToolRuntimeContext = {}, deps: LiveInteractionRuntimeDeps = {}): Promise<void> {\n        await this.destroy();\n        const root = TestToolSupport.prepareHost(context, DEFAULT_VIEWPORT_WIDTH, DEFAULT_VIEWPORT_HEIGHT);\n        const loadingStartedAt = performance.now();\n        const loadingScreen = this.createLoadingScreen(root, strings);\n        try {\n            await loadingScreen.api.start(this.createLoadingScreenPlayers(), '直播互动');\n            loadingScreen.uiScene.create3DObject();\n            loadingScreen.uiScene.update(0);\n            await this.flushUi();\n            loadingScreen.api.onLoadProgress(12);\n\n            await TestToolSupport.ensureTheater(gameMapFile.theaterType ?? TheaterType.Temperate, context.cdnResourceLoader, [\n                ResourceType.UiAlly,\n                ResourceType.BuildGen,\n                ResourceType.Vxl,\n                ResourceType.Anims,\n            ]);\n            loadingScreen.api.onLoadProgress(38);\n            await this.flushUi();\n\n            this.buildLayout(root, strings);\n            loadingScreen.api.onLoadProgress(56);\n            await this.flushUi();\n\n            const battle = await this.initializeBattle(gameMapFile, strings, context, deps);\n            this.battle = battle;\n            loadingScreen.api.onLoadProgress(86);\n\n            this.installResponsiveViewport();\n            this.syncBattleViewport(true);\n            this.bindRuntimeBridge();\n            this.startSimulationLoops();\n            this.buildHomeButton();\n            loadingScreen.api.onLoadProgress(100);\n\n            const remainingLoadingTime = LOADING_SCREEN_MIN_DURATION_MS - (performance.now() - loadingStartedAt);\n            await this.flushUi(Math.max(0, remainingLoadingTime));\n\n            this.appendLog('系统', '直播互动模式已加载，可先用右侧 mock 按钮验证效果。');\n            this.state.ready = true;\n            this.syncState();\n            const debugRoot = ((window as any).__ra2debug ??= {});\n            debugRoot.liveInteraction = {\n                snapshot: () => this.getDebugSnapshot(),\n                emitMock: (kind: InteractionKind, payload: Record<string, unknown> = {}) => this.postJson(`${API_BASE}/mock`, { kind, ...payload }),\n            };\n            if (context.rootElement) {\n                context.rootElement.dataset.ra2LiveInteractionReady = '1';\n            }\n        } finally {\n            loadingScreen.dispose();\n        }\n    }\n\n    static async destroy(): Promise<void> {\n        TestToolSupport.clearState(TOOL_NAME);\n        this.eventSource?.close();\n        this.eventSource = undefined;\n        if (this.gameTickTimer) {\n            clearInterval(this.gameTickTimer);\n            this.gameTickTimer = undefined;\n        }\n        if (this.statusPollTimer) {\n            clearInterval(this.statusPollTimer);\n            this.statusPollTimer = undefined;\n        }\n        if (this.orderRefreshTimer) {\n            clearInterval(this.orderRefreshTimer);\n            this.orderRefreshTimer = undefined;\n        }\n        this.disposables.dispose();\n        this.renderer = undefined;\n        this.uiAnimationLoop = undefined;\n        this.renderableManager = undefined;\n        this.battle = undefined;\n        const host = this.ui.host;\n        if (host) {\n            host.replaceChildren();\n            host.style.position = 'relative';\n            host.style.inset = '';\n            host.style.display = 'block';\n            host.style.width = '';\n            host.style.height = '';\n            host.style.overflow = 'visible';\n            host.style.background = '';\n            host.style.zIndex = '';\n            host.style.left = '';\n            host.style.top = '';\n            host.style.right = '';\n            host.style.bottom = '';\n            host.style.cursor = '';\n            host.style.touchAction = '';\n            delete host.dataset.ra2LiveInteractionReady;\n        }\n        this.ui = {};\n        this.panelCollapsed = false;\n        this.state.ready = false;\n        this.state.lastEvent = null;\n        this.state.recentEvents = [];\n        this.state.left = { totalSpawned: 0 };\n        this.state.right = { totalSpawned: 0 };\n        const debugRoot = (window as any).__ra2debug;\n        if (debugRoot?.liveInteraction) {\n            delete debugRoot.liveInteraction;\n        }\n        if (debugRoot?.liveInteractionBattle) {\n            delete debugRoot.liveInteractionBattle;\n        }\n    }\n\n    private static buildLayout(root: HTMLElement, strings: StringsLike): void {\n        root.replaceChildren();\n        root.style.position = 'fixed';\n        root.style.inset = '0';\n        root.style.display = 'block';\n        root.style.width = '100vw';\n        root.style.height = '100vh';\n        root.style.overflow = 'hidden';\n        root.style.background = 'radial-gradient(circle at 50% 45%, #3f0f0f 0%, #180404 45%, #090202 100%)';\n        root.style.zIndex = '1';\n        this.panelCollapsed = false;\n\n        const canvasPane = document.createElement('div');\n        canvasPane.style.position = 'absolute';\n        canvasPane.style.inset = '0';\n        canvasPane.style.width = '100%';\n        canvasPane.style.height = '100%';\n        canvasPane.style.background = '#0d0d0d';\n        canvasPane.style.cursor = 'grab';\n        canvasPane.style.touchAction = 'none';\n        canvasPane.dataset.liveCanvas = '1';\n\n        const overlayPane = document.createElement('div');\n        overlayPane.style.position = 'absolute';\n        overlayPane.style.inset = '0';\n        overlayPane.style.pointerEvents = 'none';\n        overlayPane.style.zIndex = '2';\n\n        const minimapShell = document.createElement('div');\n        minimapShell.style.position = 'absolute';\n        minimapShell.style.left = `${MINIMAP_MARGIN}px`;\n        minimapShell.style.bottom = `${MINIMAP_MARGIN}px`;\n        minimapShell.style.width = `${MINIMAP_SIZE}px`;\n        minimapShell.style.height = `${MINIMAP_SIZE}px`;\n        minimapShell.style.boxSizing = 'border-box';\n        minimapShell.style.border = '2px solid rgba(255, 216, 74, 0.72)';\n        minimapShell.style.borderRadius = '6px';\n        minimapShell.style.boxShadow = '0 0 0 1px rgba(0, 0, 0, 0.45), 0 8px 20px rgba(0, 0, 0, 0.28)';\n        minimapShell.style.pointerEvents = 'none';\n        minimapShell.style.zIndex = '1';\n        minimapShell.dataset.liveMinimap = '1';\n\n        const minimapBadge = document.createElement('div');\n        minimapBadge.textContent = '正式小地图';\n        minimapBadge.style.position = 'absolute';\n        minimapBadge.style.left = `${MINIMAP_MARGIN}px`;\n        minimapBadge.style.bottom = `${MINIMAP_MARGIN + MINIMAP_SIZE + 8}px`;\n        minimapBadge.style.padding = '4px 8px';\n        minimapBadge.style.fontSize = '11px';\n        minimapBadge.style.fontWeight = '700';\n        minimapBadge.style.letterSpacing = '0.04em';\n        minimapBadge.style.pointerEvents = 'none';\n        minimapBadge.style.zIndex = '3';\n        TestToolSupport.applyPanelTheme(minimapBadge);\n\n        const panel = document.createElement('div');\n        panel.className = 'live-interaction-panel';\n        panel.style.position = 'absolute';\n        panel.style.top = `${PANEL_MARGIN}px`;\n        panel.style.right = `${PANEL_MARGIN}px`;\n        panel.style.boxSizing = 'border-box';\n        panel.style.height = `calc(100vh - ${PANEL_MARGIN * 2}px)`;\n        panel.style.overflow = 'hidden';\n        panel.style.zIndex = '3';\n        panel.style.transition = 'transform 180ms ease, width 180ms ease';\n        panel.style.willChange = 'transform, width';\n\n        const panelToggle = this.buildButton('▶', 'toggle-panel');\n        panelToggle.dataset.livePanelToggle = '1';\n        panelToggle.style.position = 'absolute';\n        panelToggle.style.left = '0';\n        panelToggle.style.top = '0';\n        panelToggle.style.bottom = '0';\n        panelToggle.style.width = `${PANEL_COLLAPSED_VISIBLE_WIDTH}px`;\n        panelToggle.style.padding = '10px 0';\n        panelToggle.style.display = 'flex';\n        panelToggle.style.alignItems = 'center';\n        panelToggle.style.justifyContent = 'center';\n        panelToggle.style.fontSize = '18px';\n        panelToggle.style.fontWeight = '700';\n        panelToggle.style.borderRadius = '0';\n        panelToggle.style.zIndex = '1';\n\n        const panelContent = document.createElement('div');\n        panelContent.style.height = '100%';\n        panelContent.style.padding = `14px 14px 14px ${PANEL_COLLAPSED_VISIBLE_WIDTH + 14}px`;\n        panelContent.style.boxSizing = 'border-box';\n        panelContent.style.display = 'flex';\n        panelContent.style.flexDirection = 'column';\n        panelContent.style.gap = '10px';\n        panelContent.style.overflow = 'auto';\n        panelContent.style.transition = 'opacity 120ms ease';\n\n        const title = document.createElement('div');\n        title.textContent = strings.get('GUI:MainMenu') ? '直播互动模式' : 'Live Interaction';\n        title.style.fontSize = '24px';\n        title.style.fontWeight = '700';\n\n        const subtitle = document.createElement('div');\n        subtitle.textContent = '当前直接复用了正式遭遇战的世界渲染、交互和小地图组件，只隐藏侧边栏和底栏。红方老家在上、蓝方老家在下，双方增援会默认朝敌方老家移动攻击，可用屏幕边缘滚动、右键拖拽、滚轮缩放和小地图跳转查看战场。';\n        subtitle.style.fontSize = '12px';\n        subtitle.style.opacity = '0.85';\n        subtitle.style.lineHeight = '1.5';\n\n        const statusBadge = document.createElement('div');\n        statusBadge.style.padding = '8px 10px';\n        statusBadge.style.fontSize = '12px';\n        statusBadge.style.border = '1px solid rgba(255, 200, 120, 0.4)';\n        statusBadge.style.background = 'rgba(0, 0, 0, 0.2)';\n        statusBadge.dataset.liveStatus = '1';\n\n        const modeRow = document.createElement('div');\n        modeRow.style.display = 'grid';\n        modeRow.style.gridTemplateColumns = '72px 1fr';\n        modeRow.style.alignItems = 'center';\n        modeRow.style.gap = '8px';\n        const modeLabel = document.createElement('label');\n        modeLabel.textContent = '模式';\n        const modeSelect = document.createElement('select');\n        modeSelect.dataset.liveInput = 'mode';\n        modeSelect.innerHTML = `\n            <option value=\"mock\">本地模拟</option>\n            <option value=\"live\">B站直播</option>\n        `;\n        modeSelect.value = 'mock';\n\n        const credentialsGrid = document.createElement('div');\n        credentialsGrid.style.display = 'none';\n        credentialsGrid.style.gridTemplateColumns = '72px 1fr';\n        credentialsGrid.style.gap = '8px';\n        credentialsGrid.style.alignItems = 'center';\n\n        const appIdInput = this.buildLabeledInput(credentialsGrid, 'App ID', 'appId');\n        const accessKeyIdInput = this.buildLabeledInput(credentialsGrid, 'Access Key', 'accessKeyId');\n        const accessSecretInput = this.buildLabeledInput(credentialsGrid, 'Access Secret', 'accessSecret', 'password');\n        const codeInput = this.buildLabeledInput(credentialsGrid, '身份码', 'code');\n\n        const actionsRow = document.createElement('div');\n        actionsRow.style.display = 'grid';\n        actionsRow.style.gridTemplateColumns = '1fr 1fr';\n        actionsRow.style.gap = '8px';\n        const connectButton = this.buildButton('连接', 'connect');\n        const disconnectButton = this.buildButton('断开', 'disconnect');\n        actionsRow.append(connectButton, disconnectButton);\n\n        const mockTitle = document.createElement('div');\n        mockTitle.textContent = '本地测试';\n        mockTitle.style.fontSize = '14px';\n        mockTitle.style.fontWeight = '700';\n\n        const mockButtons = document.createElement('div');\n        mockButtons.style.display = 'grid';\n        mockButtons.style.gridTemplateColumns = '1fr 1fr';\n        mockButtons.style.gap = '8px';\n        mockButtons.append(\n            this.buildButton('模拟进房', 'mock-room-enter'),\n            this.buildButton('模拟点赞', 'mock-like'),\n            this.buildButton('模拟礼物', 'mock-gift'),\n            this.buildButton('模拟上舰', 'mock-guard'),\n            this.buildButton('模拟醒目留言', 'mock-super-chat'),\n            this.buildButton('红方弹幕(上)', 'mock-danmaku-left'),\n            this.buildButton('蓝方弹幕(下)', 'mock-danmaku-right'),\n            this.buildButton('战场全览', 'focus-center'),\n        );\n\n        const danmakuRow = document.createElement('div');\n        danmakuRow.style.display = 'grid';\n        danmakuRow.style.gridTemplateColumns = '1fr auto';\n        danmakuRow.style.gap = '8px';\n        const danmakuInput = document.createElement('input');\n        danmakuInput.placeholder = '自定义弹幕，例如：蓝军 下路冲 / 红军 上路守';\n        danmakuInput.dataset.liveInput = 'danmaku';\n        const danmakuSubmit = this.buildButton('发送', 'mock-danmaku-custom');\n        danmakuRow.append(danmakuInput, danmakuSubmit);\n\n        const statusSummary = document.createElement('div');\n        const leftSummary = document.createElement('div');\n        const rightSummary = document.createElement('div');\n        const catalogSummary = document.createElement('div');\n        [statusSummary, leftSummary, rightSummary, catalogSummary].forEach((block) => {\n            block.style.fontSize = '12px';\n            block.style.lineHeight = '1.6';\n            block.style.whiteSpace = 'pre-wrap';\n            block.style.border = '1px solid rgba(255, 200, 120, 0.25)';\n            block.style.background = 'rgba(0, 0, 0, 0.2)';\n            block.style.padding = '8px';\n        });\n\n        const mappingSummary = document.createElement('pre');\n        mappingSummary.style.margin = '0';\n        mappingSummary.style.fontSize = '11px';\n        mappingSummary.style.lineHeight = '1.5';\n        mappingSummary.style.whiteSpace = 'pre-wrap';\n        mappingSummary.textContent = [\n            '事件映射',\n            '进房 / 点赞 -> 红方上方增援',\n            '礼物 / 上舰 / 醒目留言 -> 蓝方下方增援',\n            '弹幕含“红/上/top” -> 红方上路',\n            '弹幕含“蓝/下/bottom” -> 蓝方下路',\n            '重装与精英单位会优先显示观众昵称',\n        ].join('\\n');\n\n        const logTitle = document.createElement('div');\n        logTitle.textContent = '事件日志';\n        logTitle.style.fontSize = '14px';\n        logTitle.style.fontWeight = '700';\n\n        const log = document.createElement('div');\n        log.style.display = 'flex';\n        log.style.flexDirection = 'column';\n        log.style.gap = '6px';\n        log.style.fontSize = '12px';\n        log.style.lineHeight = '1.4';\n        log.style.minHeight = '120px';\n\n        modeRow.append(modeLabel, modeSelect);\n        panelContent.append(\n            title,\n            subtitle,\n            statusBadge,\n            modeRow,\n            credentialsGrid,\n            actionsRow,\n            mockTitle,\n            mockButtons,\n            danmakuRow,\n            statusSummary,\n            leftSummary,\n            rightSummary,\n            catalogSummary,\n            mappingSummary,\n            logTitle,\n            log,\n        );\n        panel.append(panelToggle, panelContent);\n        root.append(canvasPane, overlayPane, minimapShell, minimapBadge, panel);\n        TestToolSupport.applyPanelTheme(panel);\n        this.ui = {\n            host: root as HTMLDivElement,\n            canvasPane,\n            overlayPane,\n            minimapShell,\n            panel,\n            panelContent,\n            panelToggle,\n            log,\n            statusSummary,\n            leftSummary,\n            rightSummary,\n            catalogSummary,\n            mappingSummary,\n            statusBadge,\n            modeSelect,\n            appIdInput,\n            accessKeyIdInput,\n            accessSecretInput,\n            codeInput,\n            credentialsGrid,\n            danmakuInput,\n        };\n        this.updatePanelLayout(this.measureViewport());\n        modeSelect.addEventListener('change', () => {\n            this.state.mode = modeSelect.value === 'live' ? 'live' : 'mock';\n            credentialsGrid.style.display = this.state.mode === 'live' ? 'grid' : 'none';\n            this.syncState();\n        });\n        connectButton.addEventListener('click', () => void this.handleConnect());\n        disconnectButton.addEventListener('click', () => void this.handleDisconnect());\n        panel.addEventListener('click', (event) => {\n            const button = (event.target as HTMLElement | null)?.closest<HTMLButtonElement>('[data-live-action]');\n            if (!button) {\n                return;\n            }\n            const action = button.dataset.liveAction;\n            if (!action) {\n                return;\n            }\n            void this.handleUiAction(action);\n        });\n    }\n\n    private static createLoadingScreen(root: HTMLElement, strings: StringsLike): LiveLoadingScreenSession {\n        const viewport = {\n            x: 0,\n            y: 0,\n            width: root.clientWidth || DEFAULT_VIEWPORT_WIDTH,\n            height: root.clientHeight || DEFAULT_VIEWPORT_HEIGHT,\n        };\n        const uiScene = UiScene.factory(viewport);\n        const htmlRoot = document.createElement('div');\n        htmlRoot.dataset.liveLoadingScreen = '1';\n        htmlRoot.style.position = 'absolute';\n        htmlRoot.style.inset = '0';\n        htmlRoot.style.zIndex = '1200';\n        htmlRoot.style.pointerEvents = 'none';\n        htmlRoot.style.background = '#000';\n        uiScene.getHtmlContainer()?.setElement(htmlRoot);\n        uiScene.getHtmlContainer()?.setSize('100%', '100%');\n\n        const jsxRenderer = new JsxRenderer(Engine.images, Engine.palettes, uiScene.getCamera());\n        const loadingBaseUrl = new URL('/cdn/game-res/v2/', window.location.href).toString();\n        const loadingRules = new Rules(Engine.getRules());\n        const gameResConfig = {\n            isCdn: () => true,\n            getCdnBaseUrl: () => loadingBaseUrl,\n        };\n        const api = new ReplayLoadingScreenApi(\n            loadingRules as any,\n            strings as any,\n            uiScene as any,\n            jsxRenderer as any,\n            gameResConfig as any,\n        );\n\n        root.appendChild(htmlRoot);\n\n        return {\n            api,\n            uiScene,\n            rootEl: htmlRoot,\n            dispose: () => {\n                api.dispose();\n                uiScene.destroy();\n                htmlRoot.remove();\n            },\n        };\n    }\n\n    private static createLoadingScreenPlayers(): Array<{\n        name: string;\n        countryId: number;\n        colorId: number;\n        teamId: number;\n    }> {\n        const rules = new Rules(Engine.getRules());\n        const countryNames = rules.getMultiplayerCountries().map((country) => country.name);\n        const topCountryId = this.findNamedIndex(countryNames, ['Americans', 'America', 'British']);\n        const bottomCountryId = this.findNamedIndex(countryNames, ['Russians', 'Russia', 'Confederation']);\n        return [\n            {\n                name: '红方上方老家',\n                countryId: topCountryId,\n                colorId: 0,\n                teamId: NO_TEAM_ID,\n            },\n            {\n                name: '蓝方下方老家',\n                countryId: bottomCountryId,\n                colorId: 1,\n                teamId: NO_TEAM_ID,\n            },\n        ];\n    }\n\n    private static async flushUi(extraDelayMs: number = 0): Promise<void> {\n        await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));\n        if (extraDelayMs > 0) {\n            await new Promise<void>((resolve) => window.setTimeout(() => resolve(), extraDelayMs));\n        }\n    }\n\n    private static buildLabeledInput(parent: HTMLElement, label: string, key: string, type: string = 'text'): HTMLInputElement {\n        const labelEl = document.createElement('label');\n        labelEl.textContent = label;\n        const input = document.createElement('input');\n        input.type = type;\n        input.dataset.liveInput = key;\n        input.autocomplete = 'off';\n        parent.append(labelEl, input);\n        return input;\n    }\n\n    private static buildButton(label: string, action: string): HTMLButtonElement {\n        const button = document.createElement('button');\n        button.textContent = label;\n        button.dataset.liveAction = action;\n        return button;\n    }\n\n    private static buildHomeButton(): void {\n        const button = document.createElement('button');\n        button.textContent = '点此返回主页';\n        button.style.cssText = `\n            position: fixed;\n            left: 50%;\n            top: 10px;\n            transform: translateX(-50%);\n            padding: 10px 20px;\n            z-index: 1000;\n        `;\n        TestToolSupport.applyHomeButtonTheme(button);\n        button.onclick = () => {\n            window.location.hash = '/';\n        };\n        document.body.appendChild(button);\n        this.disposables.add(() => button.remove());\n    }\n\n    private static async initializeBattle(gameMapFile: any, strings: StringsLike, context: TestToolRuntimeContext, deps: LiveInteractionRuntimeDeps): Promise<BattleContext> {\n        const canvasPane = this.ui.canvasPane;\n        const host = this.ui.host;\n        if (!canvasPane || !host) {\n            throw new Error('Missing live interaction host panes');\n        }\n\n        const viewport = this.measureViewport();\n        this.updatePanelLayout(viewport);\n        const battleViewport = this.computeBattleViewport(viewport);\n        const renderer = (this.renderer = new Renderer(viewport.width, viewport.height));\n        renderer.init(canvasPane);\n        const rendererCanvas = TestToolSupport.placeRendererCanvas(renderer, 0, 0);\n        rendererCanvas.dataset.liveCameraCanvas = '1';\n        this.disposables.add(renderer);\n\n        const canvasMetrics = new CanvasMetrics(rendererCanvas, window);\n        canvasMetrics.init();\n        this.disposables.add(canvasMetrics);\n\n        const generalOptions = new GeneralOptions();\n        if (deps.generalOptions) {\n            generalOptions.unserialize(deps.generalOptions.serialize());\n        }\n        generalOptions.rightClickMove.value = false;\n        generalOptions.rightClickScroll.value = true;\n        const runtimeVars = new ConsoleVars();\n        runtimeVars.debugWireframes.value = deps.runtimeVars?.debugWireframes.value ?? false;\n        runtimeVars.debugPaths.value = deps.runtimeVars?.debugPaths.value ?? false;\n        runtimeVars.debugText.value = deps.runtimeVars?.debugText.value ?? false;\n        runtimeVars.freeCamera.value = false;\n\n        const pointer = Pointer.factory(\n            Engine.getImages().get('mouse.shp'),\n            Engine.getPalettes().get('mousepal.pal'),\n            renderer,\n            document,\n            canvasMetrics,\n            generalOptions.mouseAcceleration,\n        );\n        pointer.init();\n        pointer.unlock();\n        this.disposables.add(pointer);\n\n        const uiScene = UiScene.factory(viewport);\n        uiScene.add(pointer.getSprite());\n        this.disposables.add(uiScene);\n\n        const theaterType = gameMapFile.theaterType ?? TheaterType.Temperate;\n        const theater = await Engine.loadTheater(theaterType);\n        const activeEngine = Engine.getActiveEngine();\n        const theaterSettings = Engine.getTheaterSettings(activeEngine, theaterType);\n        const theaterIni = Engine.getTheaterIni(activeEngine, theaterType);\n        const tileSets = new TileSets(theaterIni);\n        tileSets.loadTileData(Engine.getTileData(), theaterSettings.extension);\n\n        const gameModes = Engine.getMpModes();\n        const gameModeId = gameModes.hasId(0) ? 0 : gameModes.getAll()[0]?.id ?? 0;\n        const baseRules = new Rules(Engine.getRules());\n        const multiplayerCountries = baseRules.getMultiplayerCountries().map((country) => country.name);\n        const multiplayerColors = [...baseRules.getMultiplayerColors().keys()];\n        const redCountryId = this.findNamedIndex(multiplayerCountries, ['Americans', 'America', 'British']);\n        const blueCountryId = this.findNamedIndex(multiplayerCountries, ['Russians', 'Russia', 'Confederation']);\n        const redColorId = this.findNamedIndex(multiplayerColors, ['DarkRed', 'Red', 'Orange']);\n        const blueColorId = this.findNamedIndex(multiplayerColors, ['DarkBlue', 'Blue', 'SkyBlue']);\n        const speedCheat = new BoxedVar(false);\n        const debugBotIndex = new BoxedVar(0);\n        const timestamp = Date.now();\n        const gameOpts: any = {\n            gameMode: gameModeId,\n            gameSpeed: 5,\n            credits: 10000,\n            unitCount: 0,\n            shortGame: false,\n            superWeapons: false,\n            buildOffAlly: false,\n            mcvRepacks: false,\n            cratesAppear: false,\n            destroyableBridges: true,\n            multiEngineer: false,\n            noDogEngiKills: false,\n            mapName: gameMapFile.name ?? '2_reconcile.map',\n            mapTitle: gameMapFile.getOrCreateSection?.('Basic')?.getString?.('Name') ?? 'Live Interaction',\n            mapDigest: '',\n            mapSizeBytes: 0,\n            maxSlots: 2,\n            mapOfficial: true,\n            humanPlayers: [\n                { name: '红方', countryId: redCountryId, colorId: redColorId, startPos: 0, teamId: 0 },\n                { name: '蓝方', countryId: blueCountryId, colorId: blueColorId, startPos: 1, teamId: 1 },\n            ],\n            aiPlayers: [],\n        };\n        const modRules = Engine.getIni(gameModes.getById(gameModeId).rulesOverride);\n        const game = GameFactory.create(\n            gameMapFile as any,\n            tileSets,\n            Engine.getRules(),\n            Engine.getArt(),\n            Engine.getAi(),\n            modRules,\n            [],\n            'LiveInteraction',\n            timestamp,\n            gameOpts,\n            gameModes,\n            true,\n            {},\n            undefined,\n            speedCheat,\n            debugBotIndex,\n        );\n        const leftPlayer = game.getPlayerByName('红方');\n        const rightPlayer = game.getPlayerByName('蓝方');\n        IsoCoords.init({\n            x: 0,\n            y: (game.map.mapBounds.getFullSize().width * Coords.getWorldTileSize()) / 2,\n        });\n        game.init(undefined);\n        this.removeBaseUnit(game, leftPlayer);\n        this.removeBaseUnit(game, rightPlayer);\n        game.start();\n\n        const rules = game.rules as Rules;\n        const art = game.art as Art;\n        const gameMap = game.map as GameMap;\n        const debugText = game.debugText as BoxedVar<string>;\n\n        const minimap = new Minimap(game, undefined, 0xffd84a, game.rules.general.radar);\n        minimap.setPointerEvents(pointer.pointerEvents);\n        this.disposables.add(minimap);\n        uiScene.add(minimap);\n        this.updateMinimapLayout(viewport, minimap, battleViewport);\n\n        const silentSound = {\n            getSoundSpec: (key: unknown) => ({\n                name: String(key),\n                volume: 0,\n                minVolume: 0,\n                type: [],\n                control: new Set(),\n                limit: 0,\n                range: 0,\n            }),\n            playWithOptions: () => undefined,\n        };\n        const worldView = new WorldView(\n            { width: 0, height: 0 },\n            game,\n            silentSound as any,\n            renderer,\n            runtimeVars,\n            minimap,\n            strings,\n            generalOptions,\n            new VxlGeometryPool(new VxlGeometryCache(null, null)),\n            new Map(),\n        );\n        const worldViewInitResult = worldView.init(undefined, battleViewport, theater);\n        const worldScene = worldViewInitResult.worldScene;\n        (worldScene as any).set3DObject?.((worldScene as any).scene);\n        worldScene.create3DObject?.();\n        (worldScene.scene as any).background = new THREE.Color(0x0f1416);\n        const renderableManager = (this.renderableManager = worldViewInitResult.renderableManager);\n        this.disposables.add(\n            worldView,\n            () => (this.renderableManager = undefined),\n        );\n\n        const keyBinds = {\n            getCommandType() {\n                return undefined;\n            },\n        };\n        const worldInteraction = new WorldInteractionFactory(\n            undefined,\n            game,\n            game.unitSelection,\n            renderableManager,\n            uiScene,\n            worldScene,\n            pointer,\n            renderer,\n            keyBinds,\n            generalOptions,\n            runtimeVars.freeCamera,\n            runtimeVars.debugPaths,\n            true,\n            document,\n            minimap,\n            strings,\n            '#ffd84a',\n            debugText,\n            undefined,\n        ).create();\n        worldInteraction.init?.();\n        this.disposables.add(worldInteraction);\n\n        renderer.addScene(worldScene);\n        renderer.addScene(uiScene);\n        host.appendChild(uiScene.getHtmlContainer().getElement());\n        this.disposables.add(() => uiScene.getHtmlContainer().getElement().remove());\n\n        const uiLoop = (this.uiAnimationLoop = new UiAnimationLoop(renderer));\n        uiLoop.start();\n        this.disposables.add(() => uiLoop.destroy());\n\n        const localBounds = (gameMap as any).mapBounds.getLocalSize();\n        const originalUpdateCamera = worldScene.updateCamera.bind(worldScene);\n        worldScene.updateCamera = (pan: { x: number; y: number; }, zoom: number) => {\n            originalUpdateCamera({\n                x: pan.x * zoom,\n                y: pan.y * zoom,\n            }, zoom);\n        };\n        this.disposables.add(() => (worldScene.updateCamera = originalUpdateCamera));\n        const originalZoomApplyStep = worldScene.cameraZoom.applyStep.bind(worldScene.cameraZoom);\n        worldScene.cameraZoom.applyStep = (step: number) => {\n            const minBattleZoom = this.computeMinBattleZoom(worldScene.viewport, gameMap, localBounds);\n            const nextZoom = Math.max(minBattleZoom, Math.min(MAX_BATTLE_ZOOM, worldScene.cameraZoom.getZoom() + step));\n            (worldScene.cameraZoom as any).zoom = nextZoom;\n            this.updateBattlePanLimits(worldScene, gameMap, localBounds);\n        };\n        this.disposables.add(() => (worldScene.cameraZoom.applyStep = originalZoomApplyStep));\n        this.updateBattlePanLimits(worldScene, gameMap, localBounds);\n        const centerRx = localBounds.x + Math.floor(localBounds.width / 2);\n        const centerRy = localBounds.y + Math.floor(localBounds.height / 2);\n        const centerTile = gameMap.tiles.getByMapCoords(centerRx, centerRy) ?? gameMap.tiles.getByMapCoords(centerRx, centerRy - 1);\n        if (!centerTile) {\n            throw new Error('Failed to find map center tile for live interaction battle');\n        }\n\n        const unitCatalog = this.buildUnitCatalog(rules, art);\n        const leftBaseName = this.pickHomeBaseName(rules, art, theater, ['GAPOWR', 'GACNST', 'GAREFN']);\n        const rightBaseName = this.pickHomeBaseName(rules, art, theater, ['NAPOWR', 'NACNST', 'NAREFN']);\n        const leftStart = gameMap.startingLocations[leftPlayer.startLocation];\n        const rightStart = gameMap.startingLocations[rightPlayer.startLocation];\n        if (!leftStart || !rightStart) {\n            throw new Error('Live interaction map is missing the expected top/bottom start locations');\n        }\n        const leftBaseTile = this.findBasePlacementTile(\n            gameMap,\n            rules,\n            art,\n            leftBaseName,\n            leftStart.x,\n            leftStart.y,\n        );\n        const rightBaseTile = this.findBasePlacementTile(\n            gameMap,\n            rules,\n            art,\n            rightBaseName,\n            rightStart.x,\n            rightStart.y,\n        );\n        if (!leftBaseTile || !rightBaseTile) {\n            throw new Error('Failed to place top/bottom home bases for live interaction battle');\n        }\n\n        const leftBase = this.spawnHomeBase(game, leftPlayer, leftBaseName, leftBaseTile, '红方老家');\n        const rightBase = this.spawnHomeBase(game, rightPlayer, rightBaseName, rightBaseTile, '蓝方老家');\n        const leftFoundation = leftBase.getFoundation();\n        const rightFoundation = rightBase.getFoundation();\n        const leftBaseCenterRx = leftBase.tile.rx + Math.floor(leftFoundation.width / 2);\n        const rightBaseCenterRx = rightBase.tile.rx + Math.floor(rightFoundation.width / 2);\n        const leftAnchor = this.findNearestPassableTile(gameMap, leftBaseCenterRx, leftBase.tile.ry + leftFoundation.height + 2);\n        const rightAnchor = this.findNearestPassableTile(gameMap, rightBaseCenterRx, rightBase.tile.ry - 2);\n        const leftTarget = this.findNearestPassableTile(gameMap, rightBaseCenterRx, rightBase.tile.ry - 1);\n        const rightTarget = this.findNearestPassableTile(gameMap, leftBaseCenterRx, leftBase.tile.ry + leftFoundation.height + 1);\n        const battleCenterTile = gameMap.tiles.getByMapCoords(\n            Math.floor(((leftBase.centerTile?.rx ?? leftBaseCenterRx) + (rightBase.centerTile?.rx ?? rightBaseCenterRx)) / 2),\n            Math.floor(((leftBase.centerTile?.ry ?? leftBase.tile.ry) + (rightBase.centerTile?.ry ?? rightBase.tile.ry)) / 2),\n        ) ?? centerTile;\n        if (!leftAnchor || !rightAnchor || !leftTarget || !rightTarget) {\n            throw new Error('Failed to establish top/bottom spawn lanes for live interaction battle');\n        }\n\n        this.ui.catalogSummary!.textContent = [\n            '单位映射',\n            `红方上方老家: ${leftBaseName}`,\n            `蓝方下方老家: ${rightBaseName}`,\n            `红方基础步兵: ${unitCatalog.infantryBasic}`,\n            `红方精英步兵: ${unitCatalog.infantryElite}`,\n            `蓝方轻装甲: ${unitCatalog.vehicleLight}`,\n            `蓝方重装甲: ${unitCatalog.vehicleHeavy}`,\n        ].join('\\n');\n\n        const debugRoot = ((window as any).__ra2debug ??= {});\n        debugRoot.liveInteractionBattle = {\n            game,\n            gameMap,\n            uiScene,\n            minimap,\n            worldView,\n            worldScene,\n            worldInteraction,\n            renderableManager,\n            leftPlayer,\n            rightPlayer,\n            unitCatalog,\n            leftAnchor,\n            rightAnchor,\n            leftTarget,\n            rightTarget,\n            leftBase,\n            rightBase,\n            centerTile: battleCenterTile,\n        };\n\n        return {\n            game,\n            gameMap,\n            worldView,\n            uiScene,\n            minimap,\n            pointer,\n            canvasMetrics,\n            worldInteraction,\n            renderableManager,\n            worldScene,\n            leftPlayer,\n            rightPlayer,\n            leftAnchor,\n            rightAnchor,\n            leftTarget,\n            rightTarget,\n            leftBase,\n            rightBase,\n            centerTile: battleCenterTile,\n            localBounds,\n            unitCatalog,\n        };\n    }\n\n    private static buildUnitCatalog(rules: Rules, art: Art): UnitCatalog {\n        const availableByType = (type: ObjectType, names: Iterable<string>) => [...names].filter((name) => art.hasObject(name, type));\n        const infantry = availableByType(ObjectType.Infantry, rules.infantryRules.keys());\n        const vehicles = availableByType(ObjectType.Vehicle, rules.vehicleRules.keys());\n        const pick = (candidates: string[], available: string[], fallbackLabel: string) => {\n            const match = candidates.find((candidate) => available.includes(candidate));\n            if (match) {\n                return match;\n            }\n            if (!available.length) {\n                throw new Error(`No available ${fallbackLabel} units found for live interaction mode`);\n            }\n            return available[0];\n        };\n        return {\n            infantryBasic: pick(['E1', 'E2', 'DOG', 'SHK', 'GGI'], infantry, 'infantry'),\n            infantryElite: pick(['GGI', 'SHK', 'E2', 'FLAKT', 'E1'], infantry, 'elite infantry'),\n            vehicleLight: pick(['MTNK', 'LTNK', 'FV', 'HTK', 'IFV'], vehicles, 'light vehicle'),\n            vehicleHeavy: pick(['HTNK', 'APOC', 'SREF', 'TTNK', 'GRIZ'], vehicles, 'heavy vehicle'),\n        };\n    }\n\n    private static findNamedIndex(values: string[], candidates: string[]): number {\n        const normalizedValues = values.map((value) => value.toLowerCase());\n        for (const candidate of candidates) {\n            const index = normalizedValues.indexOf(candidate.toLowerCase());\n            if (index >= 0) {\n                return index;\n            }\n        }\n        return 0;\n    }\n\n    private static removeBaseUnit(game: Game, player: Player): void {\n        const baseUnits = new Set(game.rules.general.baseUnit);\n        player.getOwnedObjects()\n            .filter((object: any) => object.isUnit?.() && baseUnits.has(object.name))\n            .forEach((object: any) => game.destroyObject(object, undefined, true));\n    }\n\n    private static pickHomeBaseName(rules: Rules, art: Art, theater: any, candidates: string[]): string {\n        const imageFinder = new ImageFinder(Engine.getImages() as any, theater);\n        const available = [...rules.buildingRules.keys()].filter((name) => {\n            if (!art.hasObject(name, ObjectType.Building)) {\n                return false;\n            }\n            return this.hasRenderableBuildingArt(art, imageFinder, name);\n        });\n        const preferred = candidates.find((candidate) => available.includes(candidate));\n        if (preferred) {\n            return preferred;\n        }\n        if (!available.length) {\n            throw new Error('No available building art found for live interaction home base');\n        }\n        return available[0];\n    }\n\n    private static hasRenderableBuildingArt(art: Art, imageFinder: ImageFinder, buildingName: string): boolean {\n        try {\n            const objectArt = art.getObject(buildingName, ObjectType.Building);\n            imageFinder.findByObjectArt(objectArt);\n            if (objectArt.bibShape) {\n                imageFinder.find(objectArt.bibShape, objectArt.useTheaterExtension);\n            }\n            const animProps = new BuildingAnimArtProps();\n            animProps.read(objectArt.art, art);\n            for (const anims of animProps.getAll().values()) {\n                for (const anim of anims) {\n                    imageFinder.find(anim.image, objectArt.useTheaterExtension);\n                }\n            }\n            return true;\n        } catch (error) {\n            if (error instanceof MissingImageError) {\n                return false;\n            }\n            throw error;\n        }\n    }\n\n    private static findBasePlacementTile(gameMap: GameMap, rules: Rules, art: Art, buildingName: string, centerRx: number, centerRy: number): any {\n        const buildingArt = art.getObject(buildingName, ObjectType.Building);\n        const foundation = buildingArt.foundation;\n        const foundationCenter = buildingArt.foundationCenter;\n        const startRx = centerRx - foundationCenter.x;\n        const startRy = centerRy - foundationCenter.y;\n        const startTile = gameMap.tiles.getByMapCoords(startRx, startRy)\n            ?? gameMap.tiles.getByMapCoords(startRx, startRy - 1)\n            ?? gameMap.tiles.getByMapCoords(startRx, startRy + 1);\n        if (!startTile) {\n            return undefined;\n        }\n        const finder = new RadialTileFinder(\n            gameMap.tiles as any,\n            (gameMap as any).mapBounds,\n            startTile,\n            foundation,\n            0,\n            20,\n            (tile: any) => this.canPlaceBuildingAt(gameMap, rules, art, buildingName, tile),\n        );\n        return finder.getNextTile();\n    }\n\n    private static canPlaceBuildingAt(gameMap: GameMap, rules: Rules, art: Art, buildingName: string, tile: any): boolean {\n        const buildingRules = rules.getBuilding(buildingName);\n        const foundation = art.getObject(buildingName, ObjectType.Building).foundation;\n        for (let x = 0; x < foundation.width; x += 1) {\n            for (let y = 0; y < foundation.height; y += 1) {\n                const candidateTile = gameMap.tiles.getByMapCoords(tile.rx + x, tile.ry + y);\n                if (!candidateTile) {\n                    return false;\n                }\n                const groundObjects = gameMap.getGroundObjectsOnTile(candidateTile);\n                const hasBlockingObject = groundObjects.some((obj: any) => {\n                    if (obj.isBuilding?.() && obj.rules.invisibleInGame) {\n                        return false;\n                    }\n                    return !obj.isSmudge?.();\n                });\n                if (hasBlockingObject) {\n                    return false;\n                }\n                const landRules = rules.getLandRules(candidateTile.landType);\n                if (buildingRules.waterBound) {\n                    if (landRules.getSpeedModifier(SpeedType.Float) <= 0) {\n                        return false;\n                    }\n                } else if (candidateTile.rampType !== 0 || !landRules.buildable) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n\n    private static spawnHomeBase(game: Game, player: Player, buildingName: string, tile: any, label: string): any {\n        const building = game.createObject(ObjectType.Building, buildingName);\n        game.changeObjectOwner(building, player);\n        building.purchaseValue = game.sellTrait.computePurchaseValue(building.rules, player);\n        game.spawnObject(building, tile);\n        building.setBuildStatus?.(BuildStatus.Ready, game);\n        building.debugLabel = label;\n        return building;\n    }\n\n    private static findNearestPassableTile(gameMap: GameMap, rx: number, ry: number, speedType: SpeedType = SpeedType.Foot, isInfantry = true): any {\n        const baseTile = gameMap.tiles.getByMapCoords(rx, ry);\n        const seedTile = baseTile ?? gameMap.tiles.getByMapCoords(rx, ry - 1) ?? gameMap.tiles.getByMapCoords(rx, ry + 1);\n        if (!seedTile) {\n            return undefined;\n        }\n        const finder = new RadialTileFinder(gameMap.tiles as any, (gameMap as any).mapBounds, seedTile, { width: 1, height: 1 }, 0, 12, (tile: any) => {\n            return gameMap.terrain.getPassableSpeed(tile, speedType, isInfantry, !!tile.onBridgeLandType) > 0;\n        });\n        return finder.getNextTile();\n    }\n\n    private static measureViewport(): { x: number; y: number; width: number; height: number; } {\n        const host = this.ui.host;\n        const width = Math.max(960, Math.floor(host?.clientWidth || window.innerWidth || DEFAULT_VIEWPORT_WIDTH));\n        const height = Math.max(540, Math.floor(host?.clientHeight || window.innerHeight || DEFAULT_VIEWPORT_HEIGHT));\n        return { x: 0, y: 0, width, height };\n    }\n\n    private static computePanelWidth(viewport: { width: number; height: number; }): number {\n        const preferredWidth = Math.min(PANEL_WIDTH, Math.max(PANEL_MIN_WIDTH, Math.floor(viewport.width * 0.3)));\n        const maxAllowedWidth = Math.max(PANEL_MIN_WIDTH, viewport.width - MIN_BATTLE_VIEWPORT_WIDTH);\n        return Math.max(PANEL_MIN_WIDTH, Math.min(preferredWidth, maxAllowedWidth));\n    }\n\n    private static getReservedPanelWidth(viewport: { width: number; height: number; }): number {\n        const panelWidth = this.computePanelWidth(viewport);\n        return this.panelCollapsed\n            ? PANEL_COLLAPSED_VISIBLE_WIDTH + PANEL_MARGIN\n            : panelWidth + PANEL_MARGIN;\n    }\n\n    private static computeBattleViewport(viewport: { x: number; y: number; width: number; height: number; }): { x: number; y: number; width: number; height: number; } {\n        const reservedWidth = this.getReservedPanelWidth(viewport);\n        return {\n            x: viewport.x,\n            y: viewport.y,\n            width: Math.max(1, viewport.width - reservedWidth),\n            height: viewport.height,\n        };\n    }\n\n    private static updatePanelLayout(viewport: { width: number; height: number; }): void {\n        const panel = this.ui.panel;\n        if (!panel) {\n            return;\n        }\n        const panelWidth = this.computePanelWidth(viewport);\n        const collapsedOffset = Math.max(0, panelWidth - PANEL_COLLAPSED_VISIBLE_WIDTH);\n        panel.style.top = `${PANEL_MARGIN}px`;\n        panel.style.right = `${PANEL_MARGIN}px`;\n        panel.style.width = `${panelWidth}px`;\n        panel.style.height = `${Math.max(220, viewport.height - PANEL_MARGIN * 2)}px`;\n        panel.style.transform = this.panelCollapsed ? `translateX(${collapsedOffset}px)` : 'translateX(0)';\n        panel.dataset.collapsed = String(this.panelCollapsed);\n        if (this.ui.panelContent) {\n            this.ui.panelContent.style.opacity = this.panelCollapsed ? '0' : '1';\n            this.ui.panelContent.style.pointerEvents = this.panelCollapsed ? 'none' : 'auto';\n        }\n        if (this.ui.panelToggle) {\n            this.ui.panelToggle.textContent = this.panelCollapsed ? '◀' : '▶';\n            this.ui.panelToggle.title = this.panelCollapsed ? '展开右侧工具栏' : '向右缩起工具栏';\n        }\n    }\n\n    private static computeMinimapSize(viewport: { width: number; height: number; }): number {\n        return Math.max(180, Math.min(MINIMAP_SIZE, Math.floor(Math.min(viewport.width * 0.2, viewport.height * 0.28))));\n    }\n\n    private static updateMinimapLayout(\n        viewport: { x?: number; y?: number; width: number; height: number; },\n        minimap: Minimap = this.battle?.minimap as Minimap,\n        battleViewport: { x: number; y: number; width: number; height: number; } = this.computeBattleViewport({\n            x: viewport.x ?? 0,\n            y: viewport.y ?? 0,\n            width: viewport.width,\n            height: viewport.height,\n        }),\n    ): void {\n        if (!minimap) {\n            return;\n        }\n        const size = this.computeMinimapSize({ width: battleViewport.width, height: viewport.height });\n        const x = MINIMAP_MARGIN;\n        const y = viewport.height - size - MINIMAP_MARGIN;\n        minimap.setFitSize({ width: size, height: size });\n        minimap.setPosition(x, y);\n        minimap.setZIndex(6);\n\n        if (this.ui.minimapShell) {\n            this.ui.minimapShell.style.left = `${x}px`;\n            this.ui.minimapShell.style.top = `${y}px`;\n            this.ui.minimapShell.style.width = `${size}px`;\n            this.ui.minimapShell.style.height = `${size}px`;\n        }\n    }\n\n    private static getMinimapBounds(): { x: number; y: number; width: number; height: number; centerX: number; centerY: number; } | null {\n        const battle = this.battle;\n        if (!battle) {\n            return null;\n        }\n        const battleViewport = this.computeBattleViewport(battle.uiScene.viewport);\n        const size = this.computeMinimapSize({ width: battleViewport.width, height: battle.uiScene.viewport.height });\n        const x = MINIMAP_MARGIN;\n        const y = battle.uiScene.viewport.height - size - MINIMAP_MARGIN;\n        return {\n            x,\n            y,\n            width: size,\n            height: size,\n            centerX: x + size / 2,\n            centerY: y + size / 2,\n        };\n    }\n\n    private static installResponsiveViewport(): void {\n        const onResize = () => this.syncBattleViewport();\n        window.addEventListener('resize', onResize);\n        this.disposables.add(() => window.removeEventListener('resize', onResize));\n    }\n\n    private static syncBattleViewport(forceFocus: boolean = false): void {\n        const viewport = this.measureViewport();\n        const battleViewport = this.computeBattleViewport(viewport);\n        this.updatePanelLayout(viewport);\n        this.renderer?.setSize(viewport.width, viewport.height);\n        this.ui.host!.style.width = '100vw';\n        this.ui.host!.style.height = '100vh';\n        this.ui.canvasPane!.style.width = '100%';\n        this.ui.canvasPane!.style.height = '100%';\n        if (!this.battle) {\n            return;\n        }\n        this.battle.canvasMetrics.notifyViewportChange();\n        this.battle.uiScene.setViewport(viewport);\n        this.battle.uiScene.setCamera(UiScene.createCamera(viewport));\n        this.battle.worldView.handleViewportChange(battleViewport);\n        this.updateMinimapLayout(viewport, this.battle.minimap, battleViewport);\n        this.updateBattlePanLimits(this.battle.worldScene, this.battle.gameMap, this.battle.localBounds);\n        if (forceFocus) {\n            this.focusCenter(false);\n            return;\n        }\n        this.clampBattleCamera();\n        this.syncState();\n    }\n\n    private static focusTile(rx: number, ry: number): void {\n        const battle = this.battle;\n        if (!battle) {\n            return;\n        }\n        const roundedRx = Math.round(rx);\n        const roundedRy = Math.round(ry);\n        const tile = battle.gameMap.tiles.getByMapCoords(roundedRx, roundedRy)\n            ?? this.findNearestPassableTile(battle.gameMap, roundedRx, roundedRy, SpeedType.Track, false)\n            ?? this.findNearestPassableTile(battle.gameMap, roundedRx, roundedRy);\n        if (!tile) {\n            return;\n        }\n        const panningHelper = new MapPanningHelper(battle.gameMap as any);\n        battle.worldScene.cameraPan.setPan((panningHelper as any).computeCameraPanFromTile(tile.rx, tile.ry));\n        this.clampBattleCamera();\n        this.syncState();\n    }\n\n    private static setBattleZoom(targetZoom: number): void {\n        const battle = this.battle;\n        if (!battle) {\n            return;\n        }\n        const minBattleZoom = this.computeMinBattleZoom(battle.worldScene.viewport, battle.gameMap, battle.localBounds);\n        const clamped = Math.max(minBattleZoom, Math.min(MAX_BATTLE_ZOOM, targetZoom));\n        if (Math.abs(clamped - battle.worldScene.cameraZoom.getZoom()) < 0.001) {\n            return;\n        }\n        (battle.worldScene.cameraZoom as any).zoom = clamped;\n        this.updateBattlePanLimits(battle.worldScene, battle.gameMap, battle.localBounds);\n        this.clampBattleCamera();\n    }\n\n    private static clampBattleCamera(): void {\n        const battle = this.battle;\n        if (!battle) {\n            return;\n        }\n        this.updateBattlePanLimits(battle.worldScene, battle.gameMap, battle.localBounds);\n        battle.worldScene.cameraPan.setPan(battle.worldScene.cameraPan.getPan());\n    }\n\n    private static computeMapScreenBounds(bounds: { x: number; y: number; width: number; height: number; }): { x: number; y: number; width: number; height: number; } {\n        const topLeft = IsoCoords.screenTileToScreen(bounds.x, bounds.y);\n        const bottomRight = IsoCoords.screenTileToScreen(bounds.x + bounds.width, bounds.y + bounds.height - 1);\n        return {\n            x: topLeft.x,\n            y: topLeft.y,\n            width: bottomRight.x - topLeft.x,\n            height: bottomRight.y - topLeft.y,\n        };\n    }\n\n    private static computeRenderableScreenBounds(\n        gameMap: GameMap,\n        localBounds: { x: number; y: number; width: number; height: number; },\n    ): { x: number; y: number; width: number; height: number; } {\n        const rawLocalSize = (gameMap as any).mapBounds?.getRawLocalSize?.();\n        if (!rawLocalSize) {\n            return this.computeMapScreenBounds(localBounds);\n        }\n        return this.computeMapScreenBounds({\n            x: 2 * rawLocalSize.x,\n            y: 2 * rawLocalSize.y + 4,\n            width: 2 * rawLocalSize.width,\n            height: 2 * rawLocalSize.height + 8,\n        });\n    }\n\n    private static computeMinBattleZoom(\n        viewport: { width: number; height: number; },\n        gameMap: GameMap,\n        localBounds: { x: number; y: number; width: number; height: number; },\n    ): number {\n        const mapBounds = this.computeRenderableScreenBounds(gameMap, localBounds);\n        const fitX = viewport.width / Math.max(1, mapBounds.width - 1);\n        const fitY = viewport.height / Math.max(1, mapBounds.height - 1);\n        return Math.max(MIN_BATTLE_ZOOM, Math.min(MAX_BATTLE_ZOOM, Math.max(fitX, fitY)));\n    }\n\n    private static updateBattlePanLimits(\n        worldScene: any,\n        gameMap: GameMap,\n        localBounds: { x: number; y: number; width: number; height: number; },\n    ): void {\n        const viewport = worldScene.viewport;\n        const zoom = Math.max(0.1, worldScene.cameraZoom.getZoom());\n        const effectiveViewport = {\n            ...viewport,\n            width: viewport.width / zoom,\n            height: viewport.height / zoom,\n        };\n        const panningHelper = new MapPanningHelper(gameMap as any);\n        const mapBounds = this.computeRenderableScreenBounds(gameMap, localBounds);\n        worldScene.cameraPan.setPanLimits((panningHelper as any).computeCameraPanLimits(effectiveViewport, mapBounds));\n    }\n\n    private static getCameraSafetyState(): Record<string, unknown> | null {\n        const battle = this.battle;\n        if (!battle) {\n            return null;\n        }\n        const origin = IsoCoords.worldToScreen(0, 0);\n        const pan = battle.worldScene.cameraPan.getPan();\n        const zoom = battle.worldScene.cameraZoom.getZoom();\n        const viewport = battle.worldScene.viewport;\n        const center = {\n            x: origin.x + pan.x,\n            y: origin.y + pan.y,\n        };\n        const visibleRect = {\n            x: center.x - viewport.width / (2 * zoom),\n            y: center.y - viewport.height / (2 * zoom),\n            width: viewport.width / zoom,\n            height: viewport.height / zoom,\n        };\n        const mapBounds = this.computeRenderableScreenBounds(battle.gameMap, battle.localBounds);\n        const tolerance = 1;\n        return {\n            origin,\n            renderBounds: mapBounds,\n            visibleRect,\n            center,\n            minZoom: this.computeMinBattleZoom(viewport, battle.gameMap, battle.localBounds),\n            withinRenderableBounds:\n                visibleRect.x >= mapBounds.x - tolerance &&\n                visibleRect.y >= mapBounds.y - tolerance &&\n                visibleRect.x + visibleRect.width <= mapBounds.x + mapBounds.width + tolerance &&\n                visibleRect.y + visibleRect.height <= mapBounds.y + mapBounds.height + tolerance,\n        };\n    }\n\n    private static createAttackMoveOrder(unit: any, _targetBase: any, fallbackTile: any): AttackMoveOrder {\n        if (!this.battle) {\n            throw new Error('Battle context missing while creating attack-move order');\n        }\n        const target = this.battle.game.createTarget(undefined, fallbackTile);\n        return new AttackMoveOrder(this.battle.game, this.battle.gameMap).set(unit, target) as AttackMoveOrder;\n    }\n\n    private static getBaseStatus(base: any, label: string): {\n        label: string;\n        hpDisplay: string;\n        isAlive: boolean;\n        hitPoints: number;\n        maxHitPoints: number;\n    } {\n        const hitPoints = Math.max(0, Math.floor(base?.healthTrait?.getHitPoints?.() ?? base?.healthTrait?.hitPoints ?? 0));\n        const maxHitPoints = Math.max(1, Math.floor(base?.healthTrait?.maxHitPoints ?? hitPoints ?? 1));\n        const displayHp = Math.max(0, Math.min(BASE_HP_DISPLAY_MAX, Math.round((hitPoints / maxHitPoints) * BASE_HP_DISPLAY_MAX)));\n        return {\n            label,\n            hpDisplay: `${displayHp}/${BASE_HP_DISPLAY_MAX}`,\n            isAlive: !!base && base.isSpawned && !base.isDestroyed,\n            hitPoints,\n            maxHitPoints,\n        };\n    }\n\n    private static updateBaseLabels(): void {\n        if (!this.battle) {\n            return;\n        }\n        const leftBaseStatus = this.getBaseStatus(this.battle.leftBase, '红方老家');\n        const rightBaseStatus = this.getBaseStatus(this.battle.rightBase, '蓝方老家');\n        if (this.battle.leftBase) {\n            this.battle.leftBase.debugLabel = `${leftBaseStatus.label}\\n${leftBaseStatus.hpDisplay}`;\n        }\n        if (this.battle.rightBase) {\n            this.battle.rightBase.debugLabel = `${rightBaseStatus.label}\\n${rightBaseStatus.hpDisplay}`;\n        }\n    }\n\n    private static renderBattleLabels(): void {\n        const battle = this.battle;\n        const overlayPane = this.ui.overlayPane;\n        if (!battle || !overlayPane) {\n            return;\n        }\n        overlayPane.replaceChildren();\n        const appendLabel = (text: string, worldPos: any, color: string, emphasized: boolean = false) => {\n            const point = this.projectWorldToViewport(worldPos);\n            if (!point) {\n                return;\n            }\n            const label = document.createElement('div');\n            label.textContent = text;\n            label.style.position = 'absolute';\n            label.style.left = `${point.x}px`;\n            label.style.top = `${point.y}px`;\n            label.style.transform = 'translate(-50%, -100%)';\n            label.style.whiteSpace = 'pre';\n            label.style.padding = emphasized ? '6px 10px' : '4px 8px';\n            label.style.border = `2px solid ${color}`;\n            label.style.borderRadius = '6px';\n            label.style.background = emphasized ? 'rgba(20, 10, 10, 0.82)' : 'rgba(12, 12, 12, 0.72)';\n            label.style.color = '#fff7d1';\n            label.style.fontWeight = emphasized ? '700' : '600';\n            label.style.fontSize = emphasized ? '16px' : '14px';\n            label.style.textShadow = '0 1px 2px rgba(0, 0, 0, 0.9)';\n            label.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.35)';\n            overlayPane.appendChild(label);\n        };\n\n        const leftBaseStatus = this.getBaseStatus(battle.leftBase, '红方老家');\n        const rightBaseStatus = this.getBaseStatus(battle.rightBase, '蓝方老家');\n        if (battle.leftBase?.position?.worldPosition) {\n            appendLabel(`${leftBaseStatus.label}\\n${leftBaseStatus.hpDisplay}`, battle.leftBase.position.worldPosition, '#ff6a6a', true);\n        }\n        if (battle.rightBase?.position?.worldPosition) {\n            appendLabel(`${rightBaseStatus.label}\\n${rightBaseStatus.hpDisplay}`, battle.rightBase.position.worldPosition, '#6ea8ff', true);\n        }\n\n        const namedUnits = [\n            ...battle.leftPlayer.getOwnedObjects(),\n            ...battle.rightPlayer.getOwnedObjects(),\n        ].filter((object: any) => object.isUnit?.() && object.isSpawned && !object.isDestroyed && object.debugLabel);\n        namedUnits.forEach((unit: any) => {\n            appendLabel(unit.debugLabel, unit.position.worldPosition, unit.owner === battle.leftPlayer ? '#ff7f7f' : '#7fb0ff');\n        });\n    }\n\n    private static projectWorldToViewport(worldPos: any): { x: number; y: number; } | undefined {\n        const battle = this.battle;\n        if (!battle || !worldPos) {\n            return undefined;\n        }\n        const viewport = battle.worldScene.viewport;\n        const projected = new THREE.Vector3(worldPos.x, worldPos.y, worldPos.z).project(battle.worldScene.camera);\n        if (!Number.isFinite(projected.x) || !Number.isFinite(projected.y) || !Number.isFinite(projected.z)) {\n            return undefined;\n        }\n        const x = viewport.x + ((projected.x + 1) / 2) * viewport.width;\n        const y = viewport.y + ((1 - projected.y) / 2) * viewport.height - 18;\n        if (x < -120 || x > viewport.width + 120 || y < -120 || y > viewport.height + 120) {\n            return undefined;\n        }\n        return { x, y };\n    }\n\n    private static toViewerLabel(uname?: string): string | undefined {\n        const normalized = uname?.replace(/\\s+/g, ' ').trim();\n        if (!normalized) {\n            return undefined;\n        }\n        return normalized.length > 10 ? normalized.slice(0, 10) : normalized;\n    }\n\n    private static startSimulationLoops(): void {\n        if (!this.battle) {\n            return;\n        }\n        this.gameTickTimer = window.setInterval(() => {\n            try {\n                this.battle?.game.update();\n                this.clampBattleCamera();\n                this.renderBattleLabels();\n            } catch (error) {\n                console.error('[LiveInteractionTester] game.update failed', error);\n            }\n        }, GAME_TICK_MS);\n        this.orderRefreshTimer = window.setInterval(() => {\n            this.refreshOrders();\n            this.syncState();\n        }, ORDER_REFRESH_MS);\n        this.statusPollTimer = window.setInterval(() => {\n            void this.fetchRuntimeStatus();\n        }, STATUS_POLL_MS);\n        this.disposables.add(() => {\n            if (this.gameTickTimer) {\n                clearInterval(this.gameTickTimer);\n                this.gameTickTimer = undefined;\n            }\n        });\n        this.disposables.add(() => {\n            if (this.orderRefreshTimer) {\n                clearInterval(this.orderRefreshTimer);\n                this.orderRefreshTimer = undefined;\n            }\n        });\n        this.disposables.add(() => {\n            if (this.statusPollTimer) {\n                clearInterval(this.statusPollTimer);\n                this.statusPollTimer = undefined;\n            }\n        });\n    }\n\n    private static bindRuntimeBridge(): void {\n        this.fetchRuntimeStatus().catch((error) => {\n            console.warn('[LiveInteractionTester] Failed to query runtime status', error);\n            this.state.runtimeStatus.lastError = '未连接到本地直播运行时，请通过 bun run live:runtime 启动。';\n            this.syncState();\n        });\n        try {\n            const eventSource = new EventSource(`${API_BASE}/events`);\n            this.eventSource = eventSource;\n            eventSource.addEventListener('status', (event) => {\n                const payload = JSON.parse((event as MessageEvent).data) as RuntimeStatus;\n                this.state.runtimeStatus = payload;\n                this.syncState();\n            });\n            eventSource.addEventListener('interaction', (event) => {\n                const payload = JSON.parse((event as MessageEvent).data) as NormalizedInteractionEvent;\n                this.handleInteractionEvent(payload);\n            });\n            eventSource.onerror = () => {\n                this.state.runtimeStatus = {\n                    ...this.state.runtimeStatus,\n                    connected: false,\n                    sessionActive: false,\n                    lastError: '事件流连接中断，请确认本地运行时仍在运行。',\n                };\n                this.syncState();\n            };\n            this.disposables.add(() => eventSource.close());\n        } catch (error) {\n            console.warn('[LiveInteractionTester] Failed to initialize EventSource bridge', error);\n        }\n    }\n\n    private static async fetchRuntimeStatus(): Promise<void> {\n        const status = await this.postJson(`${API_BASE}/status`, undefined, 'GET') as RuntimeStatus;\n        this.state.runtimeStatus = status;\n        this.syncState();\n    }\n\n    private static async handleConnect(): Promise<void> {\n        const mode = this.state.mode;\n        const payload = mode === 'live'\n            ? {\n                mode,\n                appId: this.ui.appIdInput?.value.trim(),\n                accessKeyId: this.ui.accessKeyIdInput?.value.trim(),\n                accessSecret: this.ui.accessSecretInput?.value.trim(),\n                code: this.ui.codeInput?.value.trim(),\n            }\n            : { mode };\n        try {\n            const status = await this.postJson(`${API_BASE}/connect`, payload);\n            this.state.runtimeStatus = status as RuntimeStatus;\n            this.appendLog('系统', mode === 'live' ? '已发起 B 站直播连接。' : '已切换到本地模拟模式。');\n        } catch (error: any) {\n            this.appendLog('系统', `连接失败: ${error?.message || error}`);\n            this.state.runtimeStatus.lastError = String(error?.message || error);\n        }\n        this.syncState();\n    }\n\n    private static async handleDisconnect(): Promise<void> {\n        try {\n            const status = await this.postJson(`${API_BASE}/disconnect`, {});\n            this.state.runtimeStatus = status as RuntimeStatus;\n            this.appendLog('系统', '直播互动连接已断开。');\n        } catch (error: any) {\n            this.appendLog('系统', `断开失败: ${error?.message || error}`);\n            this.state.runtimeStatus.lastError = String(error?.message || error);\n        }\n        this.syncState();\n    }\n\n    private static async handleUiAction(action: string): Promise<void> {\n        switch (action) {\n            case 'toggle-panel':\n                this.panelCollapsed = !this.panelCollapsed;\n                this.syncBattleViewport();\n                return;\n            case 'mock-room-enter':\n                await this.emitMockEvent('room-enter', { uname: '观众A' });\n                return;\n            case 'mock-like':\n                await this.emitMockEvent('like', { uname: '观众B', likeCount: 3 });\n                return;\n            case 'mock-gift':\n                await this.emitMockEvent('gift', { uname: '舰队长', giftName: '辣条', giftNum: 5, price: 100, totalPrice: 500 });\n                return;\n            case 'mock-guard':\n                await this.emitMockEvent('guard', { uname: '总督', guardLevel: 3, totalPrice: 2000 });\n                return;\n            case 'mock-super-chat':\n                await this.emitMockEvent('super-chat', { uname: '醒目留言用户', price: 30, totalPrice: 3000, message: '蓝军冲锋' });\n                return;\n            case 'mock-danmaku-left':\n                await this.emitMockEvent('danmaku', { uname: '弹幕兵', message: '红军 上上上' });\n                return;\n            case 'mock-danmaku-right':\n                await this.emitMockEvent('danmaku', { uname: '弹幕兵', message: '蓝军 冲锋' });\n                return;\n            case 'mock-danmaku-custom':\n                await this.emitMockEvent('danmaku', {\n                    uname: '自定义弹幕',\n                    message: this.ui.danmakuInput?.value.trim() || '红军 支援',\n                });\n                return;\n            case 'focus-center':\n                this.focusCenter();\n                return;\n            default:\n                return;\n        }\n    }\n\n    private static async emitMockEvent(kind: InteractionKind, payload: Record<string, unknown>): Promise<void> {\n        try {\n            const status = await this.postJson(`${API_BASE}/mock`, { kind, ...payload });\n            if (status && typeof status === 'object' && 'mode' in status) {\n                this.state.runtimeStatus = status as RuntimeStatus;\n            }\n        } catch (error: any) {\n            this.appendLog('系统', `发送 mock 事件失败: ${error?.message || error}`);\n            this.state.runtimeStatus.lastError = String(error?.message || error);\n            this.syncState();\n        }\n    }\n\n    private static async postJson(url: string, payload?: unknown, method: 'GET' | 'POST' = 'POST'): Promise<unknown> {\n        const response = await fetch(url, {\n            method,\n            headers: payload !== undefined ? { 'Content-Type': 'application/json' } : undefined,\n            body: payload !== undefined ? JSON.stringify(payload) : undefined,\n        });\n        const text = await response.text();\n        const parsed = text ? JSON.parse(text) : null;\n        if (!response.ok) {\n            const message = parsed?.error || parsed?.message || text || response.statusText;\n            throw new Error(message);\n        }\n        return parsed;\n    }\n\n    private static handleInteractionEvent(event: NormalizedInteractionEvent): void {\n        this.state.lastEvent = event;\n        this.state.runtimeStatus.eventCount = Math.max(this.state.runtimeStatus.eventCount, (this.state.runtimeStatus.eventCount ?? 0) + 1);\n        this.state.runtimeStatus.lastEventAt = event.timestamp;\n        const plan = this.resolveWavePlan(event);\n        if (!plan) {\n            this.appendLog('事件', `${event.cmd} 已接收，但当前未配置为出兵。`);\n            this.syncState();\n            return;\n        }\n        this.spawnWave(plan);\n        this.appendLog('事件', this.describeEvent(event, plan));\n        this.syncState();\n    }\n\n    private static resolveWavePlan(event: NormalizedInteractionEvent): WavePlan | null {\n        const viewerLabel = this.toViewerLabel(event.uname);\n        switch (event.kind) {\n            case 'room-enter':\n                return { side: 'left', reason: '进房', infantryBasic: 1, viewerLabel };\n            case 'like': {\n                const likeCount = Math.max(1, Math.min(5, event.likeCount ?? 1));\n                return { side: 'left', reason: '点赞', infantryBasic: likeCount, viewerLabel };\n            }\n            case 'gift': {\n                const totalPrice = event.totalPrice ?? (event.price ?? 0) * (event.giftNum ?? 1);\n                if (totalPrice >= 2000) {\n                    return { side: 'right', reason: '高价值礼物', infantryElite: 6, vehicleHeavy: 2, veteran: true, viewerLabel };\n                }\n                if (totalPrice >= 500) {\n                    return { side: 'right', reason: '礼物', infantryElite: 3, vehicleLight: 1, veteran: true, viewerLabel };\n                }\n                return { side: 'right', reason: '礼物', infantryElite: 2, viewerLabel };\n            }\n            case 'guard':\n                return { side: 'right', reason: '上舰', infantryElite: 5, vehicleHeavy: 1, veteran: true, viewerLabel };\n            case 'super-chat':\n                return { side: 'right', reason: '醒目留言', infantryElite: 4, vehicleHeavy: 2, veteran: true, viewerLabel };\n            case 'danmaku': {\n                const message = event.message?.toLowerCase() ?? '';\n                const side = message.includes('蓝') || message.includes('下') || message.includes('right') || message.includes('blue') || message.includes('bottom')\n                    ? 'right'\n                    : 'left';\n                const premium = message.includes('坦克') || message.includes('tank');\n                return premium\n                    ? { side, reason: '弹幕指令', infantryBasic: 2, vehicleLight: 1, viewerLabel }\n                    : { side, reason: '弹幕指令', infantryBasic: 2, viewerLabel };\n            }\n            case 'live-start':\n                return { side: 'left', reason: '开播', infantryBasic: 3, vehicleLight: 1, viewerLabel };\n            case 'live-end':\n                return { side: 'right', reason: '下播', infantryElite: 3, vehicleHeavy: 1, viewerLabel };\n            default:\n                return null;\n        }\n    }\n\n    private static spawnWave(plan: WavePlan): void {\n        const battle = this.battle;\n        if (!battle) {\n            return;\n        }\n        const sidePlayer = plan.side === 'left' ? battle.leftPlayer : battle.rightPlayer;\n        const enemyBase = plan.side === 'left' ? battle.rightBase : battle.leftBase;\n        const targetTile = plan.side === 'left' ? battle.leftTarget : battle.rightTarget;\n        const spawnAnchor = plan.side === 'left' ? battle.leftAnchor : battle.rightAnchor;\n        const unitCatalog = battle.unitCatalog;\n        const batchIds = new Set<string>();\n        let remainingLabels = plan.viewerLabel ? MAX_UNIT_LABELS_PER_WAVE : 0;\n        const spawnUnitBatch = (unitName: string, count: number) => {\n            if (!count) {\n                return;\n            }\n            const unitRules = battle.game.rules.getObject(unitName, unitName === unitCatalog.infantryBasic || unitName === unitCatalog.infantryElite ? ObjectType.Infantry : ObjectType.Vehicle);\n            const spawnTiles = this.findSpawnTiles(spawnAnchor, count, unitRules, batchIds);\n            let infantrySpawnIndex = 0;\n            for (let index = 0; index < spawnTiles.length; index += 1) {\n                const tile = spawnTiles[index];\n                const unit = battle.game.createUnitForPlayer(unitRules, sidePlayer);\n                if (unit.isInfantry?.()) {\n                    unit.position.subCell = Infantry.SUB_CELLS[infantrySpawnIndex % Infantry.SUB_CELLS.length];\n                    infantrySpawnIndex += 1;\n                }\n                battle.game.spawnObject(unit, tile);\n                if (plan.veteran && unit.veteranTrait?.setVeteranLevel) {\n                    unit.veteranTrait.setVeteranLevel(1);\n                }\n                if (remainingLabels > 0 && plan.viewerLabel) {\n                    unit.debugLabel = plan.viewerLabel;\n                    remainingLabels -= 1;\n                }\n                const order = this.createAttackMoveOrder(unit, enemyBase, targetTile);\n                unit.unitOrderTrait.addOrder(order as any, false);\n            }\n        };\n        spawnUnitBatch(unitCatalog.vehicleHeavy, plan.vehicleHeavy ?? 0);\n        spawnUnitBatch(unitCatalog.vehicleLight, plan.vehicleLight ?? 0);\n        spawnUnitBatch(unitCatalog.infantryElite, plan.infantryElite ?? 0);\n        spawnUnitBatch(unitCatalog.infantryBasic, plan.infantryBasic ?? 0);\n        const sideStats = plan.side === 'left' ? this.state.left : this.state.right;\n        sideStats.totalSpawned +=\n            (plan.infantryBasic ?? 0) +\n            (plan.infantryElite ?? 0) +\n            (plan.vehicleLight ?? 0) +\n            (plan.vehicleHeavy ?? 0);\n        sideStats.lastReinforcementAt = Date.now();\n    }\n\n    private static findSpawnTiles(anchorTile: any, count: number, unitRules: any, usedKeys: Set<string>): any[] {\n        const battle = this.battle;\n        if (!battle) {\n            return [];\n        }\n        const finder = new RadialTileFinder(\n            battle.gameMap.tiles as any,\n            (battle.gameMap as any).mapBounds,\n            anchorTile,\n            { width: 1, height: 1 },\n            0,\n            10,\n            (tile: any) => {\n                const key = `${tile.rx}:${tile.ry}`;\n                if (usedKeys.has(key)) {\n                    return false;\n                }\n                return battle.gameMap.terrain.getPassableSpeed(tile, unitRules.speedType, unitRules.type === ObjectType.Infantry, !!tile.onBridgeLandType) > 0;\n            },\n        );\n        const tiles: any[] = [];\n        for (let index = 0; index < count; index += 1) {\n            const tile = finder.getNextTile();\n            if (!tile) {\n                break;\n            }\n            tiles.push(tile);\n            usedKeys.add(`${tile.rx}:${tile.ry}`);\n        }\n        return tiles;\n    }\n\n    private static refreshOrders(): void {\n        const battle = this.battle;\n        if (!battle) {\n            return;\n        }\n        const refreshSide = (player: Player, targetBase: any, targetTile: any) => {\n            player.getOwnedObjects()\n                .filter((object: any) => object.isUnit?.() && object.isSpawned && !object.isDestroyed)\n                .forEach((unit: any) => {\n                    if (!unit.unitOrderTrait?.isIdle?.()) {\n                        return;\n                    }\n                    if (!unit.attackTrait || !unit.moveTrait) {\n                        return;\n                    }\n                    const order = this.createAttackMoveOrder(unit, targetBase, targetTile);\n                    unit.unitOrderTrait.addOrder(order as any, false);\n                });\n        };\n        refreshSide(battle.leftPlayer, battle.rightBase, battle.leftTarget);\n        refreshSide(battle.rightPlayer, battle.leftBase, battle.rightTarget);\n    }\n\n    private static computeOverviewZoom(): number {\n        const battle = this.battle;\n        if (!battle) {\n            return 0.9;\n        }\n        return this.computeMinBattleZoom(battle.worldScene.viewport, battle.gameMap, battle.localBounds);\n    }\n\n    private static computeOverviewPan(): { x: number; y: number; } {\n        const battle = this.battle;\n        if (!battle) {\n            return { x: 0, y: 0 };\n        }\n        const limits = battle.worldScene.cameraPan.getPanLimits?.();\n        if (limits && Number.isFinite(limits.x) && Number.isFinite(limits.y) && Number.isFinite(limits.width) && Number.isFinite(limits.height)) {\n            return {\n                x: limits.x + limits.width / 2,\n                y: limits.y + limits.height / 2,\n            };\n        }\n        const panningHelper = new MapPanningHelper(battle.gameMap as any);\n        return panningHelper.computeCameraPanFromTile(battle.centerTile.rx, battle.centerTile.ry);\n    }\n\n    private static focusCenter(withLog: boolean = true): void {\n        const battle = this.battle;\n        if (!battle) {\n            return;\n        }\n        this.setBattleZoom(this.computeOverviewZoom());\n        battle.worldScene.cameraPan.setPan(this.computeOverviewPan());\n        this.clampBattleCamera();\n        this.syncState();\n        if (withLog) {\n            this.appendLog('系统', '镜头已切到战场总览，可拖拽继续查看细节。');\n        }\n    }\n\n    private static describeEvent(event: NormalizedInteractionEvent, plan: WavePlan): string {\n        const source = event.uname ? `${event.uname}` : '匿名观众';\n        const sideLabel = plan.side === 'left' ? '红方上路' : '蓝方下路';\n        const units: string[] = [];\n        if (plan.infantryBasic) {\n            units.push(`基础步兵 x${plan.infantryBasic}`);\n        }\n        if (plan.infantryElite) {\n            units.push(`精英步兵 x${plan.infantryElite}`);\n        }\n        if (plan.vehicleLight) {\n            units.push(`轻装甲 x${plan.vehicleLight}`);\n        }\n        if (plan.vehicleHeavy) {\n            units.push(`重装甲 x${plan.vehicleHeavy}`);\n        }\n        return `${source} 触发 ${plan.reason}，${sideLabel} 出兵: ${units.join(' / ')}`;\n    }\n\n    private static appendLog(tag: string, text: string): void {\n        const at = Date.now();\n        this.state.recentEvents.unshift({ at, text: `[${tag}] ${text}` });\n        this.state.recentEvents = this.state.recentEvents.slice(0, MAX_LOG_ENTRIES);\n        const log = this.ui.log;\n        if (!log) {\n            return;\n        }\n        log.replaceChildren();\n        this.state.recentEvents.forEach((entry) => {\n            const row = document.createElement('div');\n            row.textContent = `${new Date(entry.at).toLocaleTimeString()} ${entry.text}`;\n            row.style.padding = '6px 8px';\n            row.style.border = '1px solid rgba(255, 184, 74, 0.2)';\n            row.style.background = 'rgba(0, 0, 0, 0.16)';\n            log.appendChild(row);\n        });\n    }\n\n    private static syncState(): void {\n        const battle = this.battle;\n        const leftAlive = battle ? this.countAliveUnits(battle.leftPlayer) : 0;\n        const rightAlive = battle ? this.countAliveUnits(battle.rightPlayer) : 0;\n        const leftLost = battle ? battle.leftPlayer.getUnitsLost() : 0;\n        const rightLost = battle ? battle.rightPlayer.getUnitsLost() : 0;\n        const leftBaseStatus = this.getBaseStatus(battle?.leftBase, '红方老家');\n        const rightBaseStatus = this.getBaseStatus(battle?.rightBase, '蓝方老家');\n        const runtimeStatus = this.state.runtimeStatus;\n        const viewport = this.measureViewport();\n        const battleViewport = battle?.worldScene?.viewport ?? this.computeBattleViewport(viewport);\n        const cameraSafety = this.getCameraSafetyState();\n\n        this.updateBaseLabels();\n        this.renderBattleLabels();\n\n        if (this.ui.statusBadge) {\n            this.ui.statusBadge.textContent = runtimeStatus.lastError\n                ? `状态: ${runtimeStatus.lastError}`\n                : runtimeStatus.sessionActive\n                    ? `状态: 已连接直播间 ${runtimeStatus.anchor?.roomId ?? '-'}`\n                    : runtimeStatus.connected\n                        ? `状态: ${runtimeStatus.mode === 'mock' ? '本地模拟已就绪' : '直播连接中'}`\n                        : '状态: 未连接本地运行时';\n        }\n        if (this.ui.statusSummary) {\n            this.ui.statusSummary.textContent = [\n                '运行时',\n                `模式: ${runtimeStatus.mode}`,\n                `连接: ${runtimeStatus.connected ? '已连接' : '未连接'}`,\n                `场次: ${runtimeStatus.sessionActive ? '进行中' : '未开始'}`,\n                `房间: ${runtimeStatus.anchor?.roomId ?? '-'}`,\n                `主播: ${runtimeStatus.anchor?.uname ?? '-'}`,\n                `累计事件: ${runtimeStatus.eventCount ?? 0}`,\n            ].join('\\n');\n        }\n        if (this.ui.leftSummary) {\n            this.ui.leftSummary.textContent = [\n                '红方上方老家',\n                `基地血量: ${leftBaseStatus.hpDisplay}`,\n                `存活单位: ${leftAlive}`,\n                `累计出兵: ${this.state.left.totalSpawned}`,\n                `累计阵亡: ${leftLost}`,\n                `最近增援: ${this.formatTime(this.state.left.lastReinforcementAt)}`,\n            ].join('\\n');\n        }\n        if (this.ui.rightSummary) {\n            this.ui.rightSummary.textContent = [\n                '蓝方下方老家',\n                `基地血量: ${rightBaseStatus.hpDisplay}`,\n                `存活单位: ${rightAlive}`,\n                `累计出兵: ${this.state.right.totalSpawned}`,\n                `累计阵亡: ${rightLost}`,\n                `最近增援: ${this.formatTime(this.state.right.lastReinforcementAt)}`,\n            ].join('\\n');\n        }\n\n        TestToolSupport.setState(TOOL_NAME, {\n            ready: this.state.ready,\n            mode: this.state.mode,\n            runtimeStatus,\n            eventCount: runtimeStatus.eventCount ?? 0,\n            camera: battle ? {\n                pan: battle.worldScene.cameraPan.getPan(),\n                zoom: battle.worldScene.cameraZoom.getZoom(),\n                viewport: battle.worldScene.viewport,\n            } : null,\n            cameraSafety,\n            layout: {\n                panelCollapsed: this.panelCollapsed,\n                panelWidth: this.computePanelWidth(viewport),\n                reservedPanelWidth: this.getReservedPanelWidth(viewport),\n                windowViewport: viewport,\n                battleViewport,\n            },\n            minimap: {\n                ready: !!battle?.minimap,\n                bounds: this.getMinimapBounds(),\n            },\n            left: {\n                aliveUnits: leftAlive,\n                totalSpawned: this.state.left.totalSpawned,\n                unitsLost: leftLost,\n                baseHealth: leftBaseStatus,\n            },\n            right: {\n                aliveUnits: rightAlive,\n                totalSpawned: this.state.right.totalSpawned,\n                unitsLost: rightLost,\n                baseHealth: rightBaseStatus,\n            },\n            top: {\n                aliveUnits: leftAlive,\n                totalSpawned: this.state.left.totalSpawned,\n                unitsLost: leftLost,\n                baseHealth: leftBaseStatus,\n            },\n            bottom: {\n                aliveUnits: rightAlive,\n                totalSpawned: this.state.right.totalSpawned,\n                unitsLost: rightLost,\n                baseHealth: rightBaseStatus,\n            },\n            lastEvent: this.state.lastEvent,\n            recentEvents: this.state.recentEvents,\n        });\n    }\n\n    private static countAliveUnits(player: Player): number {\n        return player.getOwnedObjects().filter((object: any) => object.isUnit?.() && object.isSpawned && !object.isDestroyed).length;\n    }\n\n    private static formatTime(value?: number): string {\n        if (!value) {\n            return '-';\n        }\n        return new Date(value).toLocaleTimeString();\n    }\n\n    private static getDebugSnapshot(): Record<string, unknown> {\n        const battle = this.battle;\n        const viewport = this.measureViewport();\n        const battleViewport = battle?.worldScene?.viewport ?? this.computeBattleViewport(viewport);\n        const cameraSafety = this.getCameraSafetyState();\n        return {\n            ready: this.state.ready,\n            mode: this.state.mode,\n            runtimeStatus: this.state.runtimeStatus,\n            camera: battle ? {\n                pan: battle.worldScene.cameraPan.getPan(),\n                zoom: battle.worldScene.cameraZoom.getZoom(),\n                viewport: battle.worldScene.viewport,\n            } : null,\n            cameraSafety,\n            layout: {\n                panelCollapsed: this.panelCollapsed,\n                panelWidth: this.computePanelWidth(viewport),\n                reservedPanelWidth: this.getReservedPanelWidth(viewport),\n                windowViewport: viewport,\n                battleViewport,\n            },\n            minimap: {\n                ready: !!battle?.minimap,\n                bounds: this.getMinimapBounds(),\n            },\n            left: {\n                totalSpawned: this.state.left.totalSpawned,\n                aliveUnits: battle ? this.countAliveUnits(battle.leftPlayer) : 0,\n                unitsLost: battle ? battle.leftPlayer.getUnitsLost() : 0,\n                baseHealth: this.getBaseStatus(battle?.leftBase, '红方老家'),\n            },\n            right: {\n                totalSpawned: this.state.right.totalSpawned,\n                aliveUnits: battle ? this.countAliveUnits(battle.rightPlayer) : 0,\n                unitsLost: battle ? battle.rightPlayer.getUnitsLost() : 0,\n                baseHealth: this.getBaseStatus(battle?.rightBase, '蓝方老家'),\n            },\n            lastEvent: this.state.lastEvent,\n            recentEvents: this.state.recentEvents,\n        };\n    }\n}\n"
  },
  {
    "path": "src/tools/LobbyFormTester.ts",
    "content": "import { Renderer } from \"@/engine/gfx/Renderer\";\nimport { Engine } from \"@/engine/Engine\";\nimport { UiScene } from \"@/gui/UiScene\";\nimport { LobbyType, SlotType, SlotOccupation, PlayerStatus } from \"@/gui/screen/mainMenu/lobby/component/viewmodel/lobby\";\nimport { MainMenu } from \"@/gui/screen/mainMenu/component/MainMenu\";\nimport { Rules } from \"@/game/rules/Rules\";\nimport { LobbyForm } from \"@/gui/screen/mainMenu/lobby/component/LobbyForm\";\nimport { UiAnimationLoop } from \"@/engine/UiAnimationLoop\";\nimport { JsxRenderer } from \"@/gui/jsx/JsxRenderer\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { jsx } from \"@/gui/jsx/jsx\";\nimport { HtmlView } from \"@/gui/jsx/HtmlView\";\nimport { aiUiNames, RANDOM_START_POS, NO_TEAM_ID } from \"@/game/gameopts/constants\";\nimport { PlayerRankType } from \"@/network/ladder/PlayerRankType\";\nimport { LadderType } from \"@/network/ladder/wladderConfig\";\nimport { ShpBuilder } from '@/engine/renderable/builder/ShpBuilder';\nimport { TextureUtils } from '@/engine/gfx/TextureUtils.js';\nimport { TestToolSupport, type TestToolRuntimeContext } from '@/tools/TestToolSupport';\ninterface PlayerProfile {\n    name: string;\n    rank: number;\n    rankType: PlayerRankType;\n    points: number;\n    ladder: {\n        id: number;\n        name: string;\n        divisionName: string;\n        type: LadderType;\n    };\n    wins: number;\n    losses: number;\n}\ninterface PlayerSlot {\n    name?: string;\n    type: SlotType;\n    occupation: SlotOccupation;\n    country: string;\n    color: string;\n    startPos: number;\n    team: number;\n    status: PlayerStatus;\n    ping?: number;\n    playerProfile?: PlayerProfile;\n}\ninterface ViewportBounds {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\nexport class LobbyFormTester {\n    private static disposables = new CompositeDisposable();\n    private static homeButton?: HTMLButtonElement;\n    static main(container: HTMLElement, strings: any, context: TestToolRuntimeContext = {}): void {\n        const hostElement = TestToolSupport.prepareHost(context, 800, 600);\n        const renderer = new Renderer(800, 600);\n        renderer.init(hostElement);\n        TestToolSupport.placeRendererCanvas(renderer, 0, 0);\n        renderer.initStats(document.body);\n        this.disposables.add(renderer);\n        const uiScene = UiScene.factory({\n            x: 0,\n            y: 0,\n            width: 800,\n            height: 600,\n        });\n        this.disposables.add(uiScene);\n        const sceneWidth = 800;\n        const sceneHeight = 600;\n        const bounds: ViewportBounds = {\n            x: Math.max(0, (uiScene.viewport.width - sceneWidth) / 2),\n            y: Math.max(0, (uiScene.viewport.height - sceneHeight) / 2),\n            width: sceneWidth,\n            height: sceneHeight,\n        };\n        const jsxRenderer = new JsxRenderer(Engine.getImages(), Engine.getPalettes(), uiScene.getCamera(), undefined);\n        const mainMenu = new MainMenu(bounds, Engine.getImages(), jsxRenderer, \"dummy.webm\");\n        uiScene.add(mainMenu);\n        const rules = new Rules(Engine.getRules());\n        const [htmlElement] = jsxRenderer.render(jsx(HtmlView, {\n            x: bounds.x,\n            y: bounds.y,\n            component: LobbyForm,\n            props: {\n                strings,\n                countryUiNames: new Map([\n                    [\"Random\", \"GUI:RandomEx\"],\n                    [\"Observer\", \"GUI:Observer\"],\n                    ...rules.getMultiplayerCountries().map(country => [country.name, country.uiName] as [\n                        string,\n                        string\n                    ])\n                ]),\n                countryUiTooltips: new Map<string, string>(),\n                availablePlayerCountries: [\n                    \"Random\",\n                    ...rules.getMultiplayerCountries().map(country => country.name)\n                ],\n                availablePlayerColors: [\n                    \"\",\n                    ...[...rules.getMultiplayerColors().values()].map(color => color.asHexString())\n                ],\n                maxTeams: 4,\n                availableAiNames: aiUiNames,\n                availableStartPositions: new Array(8).fill(0).map((_, index) => index),\n                activeSlotIndex: 0,\n                teamsAllowed: true,\n                teamsRequired: false,\n                lobbyType: LobbyType.MultiplayerHost,\n                playerSlots: [\n                    {\n                        name: \"Player 1\",\n                        type: SlotType.Player,\n                        occupation: SlotOccupation.Occupied,\n                        country: \"French\",\n                        color: \"#2269d4\",\n                        startPos: RANDOM_START_POS,\n                        team: NO_TEAM_ID,\n                        status: PlayerStatus.Host,\n                        ping: 50,\n                        playerProfile: {\n                            name: \"Player 1\",\n                            rank: 2,\n                            rankType: PlayerRankType.Private,\n                            points: 100,\n                            ladder: {\n                                id: 0,\n                                name: \"1v1\",\n                                divisionName: \"Test ladder\",\n                                type: LadderType.Solo1v1,\n                            },\n                            wins: 0,\n                            losses: 0,\n                        },\n                    },\n                    {\n                        name: \"Player 2\",\n                        type: SlotType.Player,\n                        occupation: SlotOccupation.Occupied,\n                        country: \"Russians\",\n                        color: \"#ff1818\",\n                        startPos: 1,\n                        team: 0,\n                        status: PlayerStatus.Ready,\n                        ping: 300,\n                    },\n                    {\n                        name: \"Open\",\n                        type: SlotType.Player,\n                        occupation: SlotOccupation.Open,\n                        country: \"Random\",\n                        color: \"\",\n                        startPos: RANDOM_START_POS,\n                        team: NO_TEAM_ID,\n                        status: PlayerStatus.NotReady,\n                    },\n                    {\n                        type: SlotType.Observer,\n                        occupation: SlotOccupation.Open,\n                        country: \"Observer\",\n                        color: \"\",\n                        startPos: RANDOM_START_POS,\n                        team: NO_TEAM_ID,\n                        status: PlayerStatus.NotReady,\n                    },\n                ] as PlayerSlot[],\n                shortGame: true,\n                mcvRepacks: true,\n                cratesAppear: true,\n                superWeapons: true,\n                buildOffAlly: true,\n                destroyableBridges: true,\n                multiEngineer: false,\n                multiEngineerCount: 3,\n                noDogEngiKills: false,\n                gameSpeed: 6,\n                credits: 10000,\n                unitCount: 10,\n                messages: [],\n                mpDialogSettings: rules.mpDialogSettings,\n                onSendMessage: () => { },\n                onCountrySelect: (country: string) => {\n                    console.log(\"selected country\", country);\n                },\n                onColorSelect: (color: string) => {\n                    console.log(\"selected color\", color);\n                },\n                onStartPosSelect: (position: number) => {\n                    console.log(\"selected start pos\", position);\n                },\n                onTeamSelect: (team: number) => {\n                    console.log(\"selected team\", team);\n                },\n                onSlotChange: (slotIndex: number, slotType: SlotType) => {\n                    console.log(\"changed slot\", slotIndex, slotType);\n                },\n                onToggleShortGame: (enabled: boolean) => console.log(enabled),\n                onToggleMcvRepacks: (enabled: boolean) => console.log(enabled),\n                onToggleCratesAppear: (enabled: boolean) => console.log(enabled),\n                onToggleSuperWeapons: (enabled: boolean) => console.log(enabled),\n                onToggleBuildOffAlly: (enabled: boolean) => console.log(enabled),\n                onChangeGameSpeed: (speed: number) => console.log(speed),\n                onChangeCredits: (credits: number) => console.log(credits),\n                onChangeUnitCount: (count: number) => console.log(count),\n            },\n        }));\n        mainMenu.add(htmlElement);\n        renderer.addScene(uiScene);\n        const animationLoop = new UiAnimationLoop(renderer);\n        animationLoop.start();\n        this.disposables.add(animationLoop);\n        container.appendChild(uiScene.getHtmlContainer().getElement());\n        this.disposables.add(() => container.removeChild(uiScene.getHtmlContainer().getElement()));\n        this.buildHomeButton(container);\n        TestToolSupport.setState('lobby', {\n            slotCount: 4,\n            lobbyType: LobbyType.MultiplayerHost,\n            maxTeams: 4,\n        });\n    }\n    private static buildHomeButton(parent: HTMLElement): void {\n        const homeButton = this.homeButton = document.createElement('button');\n        homeButton.innerHTML = '点此返回主页';\n        homeButton.style.cssText = `\n      position: fixed;\n      left: 50%;\n      top: 10px;\n      transform: translateX(-50%);\n      padding: 10px 20px;\n      background-color: rgba(0, 0, 0, 0.8);\n      color: white;\n      border: 2px solid rgba(255, 255, 255, 0.3);\n      border-radius: 6px;\n      cursor: pointer;\n      font-size: 16px;\n      font-weight: bold;\n      z-index: 1000;\n      transition: all 0.3s ease;\n      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n    `;\n        homeButton.onmouseover = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';\n            homeButton.style.transform = 'translateX(-50%) translateY(-2px)';\n            homeButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';\n        };\n        homeButton.onmouseout = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';\n            homeButton.style.transform = 'translateX(-50%) translateY(0)';\n            homeButton.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';\n        };\n        homeButton.onclick = () => {\n            window.location.hash = '/';\n        };\n        parent.appendChild(homeButton);\n        this.disposables.add(() => homeButton.remove());\n    }\n    static destroy(): void {\n        TestToolSupport.clearState('lobby');\n        this.disposables.dispose();\n        if (this.homeButton) {\n            this.homeButton.remove();\n            this.homeButton = undefined;\n        }\n        try {\n            if (ShpBuilder?.clearCaches) {\n                ShpBuilder.clearCaches();\n            }\n            if (TextureUtils?.cache) {\n                TextureUtils.cache.forEach((tex: any) => tex.dispose?.());\n                TextureUtils.cache.clear();\n            }\n        }\n        catch (err) {\n            console.warn('[LobbyFormTester] Failed to clear caches during destroy:', err);\n        }\n    }\n}\n"
  },
  {
    "path": "src/tools/PerformanceTester.ts",
    "content": "import * as THREE from 'three';\nimport { EventDispatcher } from '@/util/event';\nimport { IsoCoords } from '@/engine/IsoCoords';\nimport { Coords } from '@/game/Coords';\nimport { RaycastHelper } from '@/engine/util/RaycastHelper';\nimport { EntityIntersectHelper } from '@/engine/util/EntityIntersectHelper';\nimport { MapTileIntersectHelper } from '@/engine/util/MapTileIntersectHelper';\nimport { WorldViewportHelper } from '@/engine/util/WorldViewportHelper';\nimport { WorldSound } from '@/engine/sound/WorldSound';\nimport { SoundControl, SoundType } from '@/engine/sound/SoundSpecs';\nimport { TestToolSupport, type TestToolRuntimeContext } from '@/tools/TestToolSupport';\nimport { type PerformanceOptionKey, performanceOptionKeys } from '@/performance/PerformanceOptions';\nimport { attachPerformanceOptions, measurePerformanceMetric, resetPerformanceTelemetry, snapshotPerformanceConfig, snapshotPerformanceTelemetry } from '@/performance/PerformanceRuntime';\n\ntype Strings = {\n    get(key: string): string;\n};\n\ntype RuntimeVars = {\n    perfRaycastHelperReuse?: { value: boolean; };\n    perfEntityIntersectTraversal?: { value: boolean; };\n    perfMapTileHitTest?: { value: boolean; };\n    perfWorldViewportCache?: { value: boolean; };\n    perfWorldSoundLoopCache?: { value: boolean; };\n    perfTelemetry?: { value: boolean; };\n};\n\ntype GeneralOptions = {\n    performance: Record<PerformanceOptionKey, { value: boolean; }>;\n};\n\ntype MetricSummary = {\n    totalMsMedian: number;\n    avgMsMedian: number;\n};\n\ntype BenchmarkSample = {\n    label: string;\n    profile: 'all-off' | 'all-on' | 'warmup';\n    telemetry: ReturnType<typeof snapshotPerformanceTelemetry>;\n};\n\ntype BenchmarkPhaseResult = {\n    metric: string;\n    baseline: BenchmarkSample[];\n    candidate: BenchmarkSample[];\n    baselineMedian: MetricSummary;\n    candidateMedian: MetricSummary;\n    regressionPct: MetricSummary;\n    passed: boolean;\n};\n\nconst metricNamesByPhase = {\n    entityIntersect: 'phase.entityIntersect',\n    mapTileHit: 'phase.mapTileHit',\n    worldViewport: 'phase.worldViewport',\n    worldSound: 'phase.worldSound',\n} as const;\n\nconst performanceFeatureRuntimeMap = {\n    raycastHelperReuse: 'perfRaycastHelperReuse',\n    entityIntersectTraversal: 'perfEntityIntersectTraversal',\n    mapTileHitTest: 'perfMapTileHitTest',\n    worldViewportCache: 'perfWorldViewportCache',\n    worldSoundLoopCache: 'perfWorldSoundLoopCache',\n    telemetry: 'perfTelemetry',\n} as const;\n\nexport class PerformanceTester {\n    private static host?: HTMLDivElement;\n    private static summaryBlock?: HTMLPreElement;\n    private static homeButton?: HTMLButtonElement;\n    private static currentOptions?: GeneralOptions['performance'];\n\n    static async main(parentElement: HTMLElement, strings: Strings, runtimeVars: RuntimeVars, generalOptions: GeneralOptions, context: TestToolRuntimeContext = {}): Promise<void> {\n        this.currentOptions = generalOptions.performance;\n        attachPerformanceOptions(generalOptions.performance as any);\n        this.buildLayout(parentElement, strings);\n        TestToolSupport.setState('performance', {\n            status: 'running',\n            completed: false,\n            phase: 'warmup',\n            performance: null,\n        });\n        const originalValues = this.captureOptionValues(generalOptions.performance);\n        const originalTelemetry = generalOptions.performance.telemetry.value;\n        generalOptions.performance.telemetry.value = true;\n        if (runtimeVars.perfTelemetry) {\n            runtimeVars.perfTelemetry.value = true;\n        }\n        try {\n            const benchmarkResult = await this.runBenchmarks();\n            const payload = {\n                status: 'complete',\n                completed: true,\n                browser: navigator.userAgent,\n                seed: 1337,\n                viewport: { width: 800, height: 600 },\n                featureFlags: snapshotPerformanceConfig(),\n                performance: benchmarkResult,\n            };\n            this.summaryBlock!.textContent = JSON.stringify(payload.performance, null, 2);\n            TestToolSupport.setState('performance', payload);\n        }\n        finally {\n            this.applyOptionValues(originalValues);\n            generalOptions.performance.telemetry.value = originalTelemetry;\n            if (runtimeVars.perfTelemetry) {\n                runtimeVars.perfTelemetry.value = originalTelemetry;\n            }\n            attachPerformanceOptions(generalOptions.performance as any);\n        }\n        if (context.rootElement) {\n            context.rootElement.dataset.ra2PerfReady = '1';\n        }\n    }\n\n    private static buildLayout(parentElement: HTMLElement, strings: Strings): void {\n        parentElement.replaceChildren();\n        const host = document.createElement('div');\n        host.style.cssText = `\n            width: min(960px, 100%);\n            margin: 0 auto;\n            padding: 24px;\n            box-sizing: border-box;\n            display: flex;\n            flex-direction: column;\n            gap: 16px;\n            min-height: 100%;\n        `;\n        const title = document.createElement('h2');\n        title.textContent = strings.get?.('GUI:Options') ? 'Performance Benchmark' : 'Performance Benchmark';\n        title.style.margin = '0';\n        title.style.color = '#ffd84a';\n        const summary = document.createElement('pre');\n        summary.style.cssText = `\n            margin: 0;\n            padding: 16px;\n            white-space: pre-wrap;\n            word-break: break-word;\n            font-size: 12px;\n            line-height: 1.5;\n            min-height: 240px;\n            overflow: auto;\n        `;\n        summary.textContent = 'Running performance benchmark...';\n        TestToolSupport.applyPanelTheme(host);\n        host.append(title, summary);\n        parentElement.appendChild(host);\n        this.host = host;\n        this.summaryBlock = summary;\n        this.buildHomeButton();\n    }\n\n    private static buildHomeButton(): void {\n        if (this.homeButton) {\n            this.homeButton.remove();\n        }\n        const button = document.createElement('button');\n        button.innerHTML = '点此返回主页';\n        button.style.cssText = `\n            position: fixed;\n            left: 50%;\n            top: 10px;\n            transform: translateX(-50%);\n            padding: 10px 20px;\n            z-index: 1000;\n        `;\n        TestToolSupport.applyHomeButtonTheme(button);\n        button.onclick = () => {\n            window.location.hash = '/';\n        };\n        document.body.appendChild(button);\n        this.homeButton = button;\n    }\n\n    private static captureOptionValues(options: GeneralOptions['performance']): Record<PerformanceOptionKey, boolean> {\n        return performanceOptionKeys.reduce((acc, key) => {\n            acc[key] = options[key].value;\n            return acc;\n        }, {} as Record<PerformanceOptionKey, boolean>);\n    }\n\n    private static applyOptionValues(values: Record<PerformanceOptionKey, boolean>): void {\n        if (!this.currentOptions) {\n            return;\n        }\n        performanceOptionKeys.forEach((key) => {\n            this.currentOptions![key].value = values[key];\n        });\n    }\n\n    private static setFeatureProfile(enabled: boolean): void {\n        if (!this.currentOptions) {\n            return;\n        }\n        this.currentOptions.raycastHelperReuse.value = enabled;\n        this.currentOptions.entityIntersectTraversal.value = enabled;\n        this.currentOptions.mapTileHitTest.value = enabled;\n        this.currentOptions.worldViewportCache.value = enabled;\n        this.currentOptions.worldSoundLoopCache.value = enabled;\n        Object.entries(performanceFeatureRuntimeMap).forEach(([feature, runtimeVarKey]) => {\n            if (feature === 'telemetry') {\n                return;\n            }\n            const runtimeVar = (window as any).__ra2debug?.runtimeVars?.[runtimeVarKey];\n            if (runtimeVar) {\n                runtimeVar.value = enabled;\n            }\n        });\n    }\n\n    private static async runBenchmarks(): Promise<Record<string, any>> {\n        const viewport = { x: 0, y: 0, width: 800, height: 600 };\n        const pan = { x: 0, y: 0 };\n        const seed = 1337;\n        const entityFixture = this.createEntityFixture(viewport, seed);\n        const mapFixture = this.createMapFixture(viewport, pan);\n        const worldViewportFixture = this.createWorldViewportFixture(viewport, pan);\n        const worldSoundFixture = this.createWorldSoundFixture(viewport, pan, mapFixture.helper, worldViewportFixture.isoHelper);\n        await this.runProfile('warmup', true, {\n            entityIntersect: entityFixture.run,\n            mapTileHit: mapFixture.run,\n            worldViewport: worldViewportFixture.run,\n            worldSound: worldSoundFixture.run,\n        });\n        const phaseResults = {\n            entityIntersect: await this.runPhase('entityIntersect', entityFixture.run),\n            mapTileHit: await this.runPhase('mapTileHit', mapFixture.run),\n            worldViewport: await this.runPhase('worldViewport', worldViewportFixture.run),\n            worldSound: await this.runPhase('worldSound', worldSoundFixture.run),\n        };\n        return {\n            phases: phaseResults,\n            thresholdPct: 10,\n            passed: Object.values(phaseResults).every((phase) => phase.passed),\n            generatedAt: new Date().toISOString(),\n        };\n    }\n\n    private static async runPhase(phase: keyof typeof metricNamesByPhase, runner: () => void): Promise<BenchmarkPhaseResult> {\n        const baseline = await this.runProfile('all-off', false, { [phase]: runner } as Record<string, () => void>);\n        const candidate = await this.runProfile('all-on', true, { [phase]: runner } as Record<string, () => void>);\n        const metric = metricNamesByPhase[phase];\n        const baselineMedian = this.computeMetricMedian(baseline, metric);\n        const candidateMedian = this.computeMetricMedian(candidate, metric);\n        const regressionPct = {\n            totalMsMedian: this.computeRegressionPct(baselineMedian.totalMsMedian, candidateMedian.totalMsMedian),\n            avgMsMedian: this.computeRegressionPct(baselineMedian.avgMsMedian, candidateMedian.avgMsMedian),\n        };\n        return {\n            metric,\n            baseline,\n            candidate,\n            baselineMedian,\n            candidateMedian,\n            regressionPct,\n            passed: regressionPct.totalMsMedian <= 10 && regressionPct.avgMsMedian <= 10,\n        };\n    }\n\n    private static async runProfile(profile: 'warmup' | 'all-off' | 'all-on', enabled: boolean, ...phaseGroups: Array<Record<string, () => void>>): Promise<BenchmarkSample[]> {\n        this.setFeatureProfile(enabled);\n        const phaseMap = Object.assign({}, ...phaseGroups) as Record<string, () => void>;\n        const phaseNames = Object.keys(phaseMap);\n        const sampleCount = profile === 'warmup' ? phaseNames.length : 3;\n        const samples: BenchmarkSample[] = [];\n        for (let index = 0; index < sampleCount; index += 1) {\n            const phaseName = phaseNames[index % phaseNames.length]!;\n            this.updateStatus(profile, phaseName, index + 1, sampleCount);\n            resetPerformanceTelemetry();\n            measurePerformanceMetric(metricNamesByPhase[phaseName as keyof typeof metricNamesByPhase], () => {\n                phaseMap[phaseName]!();\n            });\n            samples.push({\n                label: `${profile}-${index + 1}`,\n                profile,\n                telemetry: snapshotPerformanceTelemetry(),\n            });\n            await this.nextFrame();\n        }\n        return samples;\n    }\n\n    private static updateStatus(profile: string, phase: string, runIndex: number, sampleCount: number): void {\n        const state = {\n            status: 'running',\n            completed: false,\n            phase,\n            profile,\n            runIndex,\n            sampleCount,\n        };\n        TestToolSupport.setState('performance', state);\n        if (this.summaryBlock) {\n            this.summaryBlock.textContent = JSON.stringify(state, null, 2);\n        }\n    }\n\n    private static createEntityFixture(viewport: { x: number; y: number; width: number; height: number; }, seed: number) {\n        const sceneRoot = new THREE.Group();\n        const camera = new THREE.OrthographicCamera(-400, 400, 300, -300, 0.1, 1000);\n        camera.position.set(0, 0, 100);\n        camera.lookAt(0, 0, 0);\n        camera.updateProjectionMatrix();\n        const random = this.createRandom(seed);\n        const renderables = new Map<string, any>();\n        const screenPoints: Array<{ x: number; y: number; }> = [];\n        const screenBoxes: THREE.Box2[] = [];\n        const gameObjectsOnTile: any[] = [];\n        const baseGeometry = new THREE.BoxGeometry(14, 14, 10);\n        const baseMaterial = new THREE.MeshBasicMaterial();\n        for (let index = 0; index < 48; index += 1) {\n            const id = `entity-${index}`;\n            const holder = new THREE.Group();\n            holder.userData.id = id;\n            holder.position.set(Math.floor((random() - 0.5) * 640), Math.floor((random() - 0.5) * 420), 0);\n            const mesh = new THREE.Mesh(baseGeometry, baseMaterial);\n            mesh.position.set(0, 0, 0);\n            holder.add(mesh);\n            sceneRoot.add(holder);\n            const gameObject = {\n                id,\n                position: { worldPosition: { x: holder.position.x, y: holder.position.y, z: 0 } },\n                isUnit: () => index % 4 !== 0,\n                isBuilding: () => index % 4 === 0,\n                isDestroyed: false,\n                isCrashing: false,\n            };\n            const renderable = {\n                gameObject,\n                getIntersectTarget: () => index % 3 === 0 ? [mesh] : mesh,\n            };\n            renderables.set(id, renderable);\n            gameObjectsOnTile.push(gameObject);\n            const projected = new THREE.Vector3(holder.position.x, holder.position.y, 0).project(camera);\n            const screenPoint = {\n                x: viewport.x + ((projected.x + 1) / 2) * viewport.width,\n                y: viewport.y + ((1 - projected.y) / 2) * viewport.height,\n            };\n            screenPoints.push(screenPoint);\n            screenBoxes.push(new THREE.Box2(new THREE.Vector2(screenPoint.x - 16, screenPoint.y - 16), new THREE.Vector2(screenPoint.x + 16, screenPoint.y + 16)));\n        }\n        sceneRoot.updateMatrixWorld(true);\n        const raycastHelper = new RaycastHelper({ viewport, camera });\n        const helper = new EntityIntersectHelper({\n            getObjectsOnTile: () => gameObjectsOnTile,\n        } as any, {\n            getRenderableContainer: () => ({ get3DObject: () => sceneRoot }),\n            getRenderableById: (id: string) => renderables.get(id),\n            getRenderableByGameObject: (gameObject: any) => renderables.get(gameObject.id),\n        } as any, {\n            getTileAtScreenPoint: () => ({ rx: 0, ry: 0, z: 0 }),\n        } as any, raycastHelper as any, {\n            viewport,\n        } as any, {\n            intersectsScreenBox: () => true,\n        } as any);\n        return {\n            run: () => {\n                for (let index = 0; index < screenPoints.length; index += 1) {\n                    helper.getEntityAtScreenPoint(screenPoints[index]!);\n                    helper.getEntitiesAtScreenBox(screenBoxes[index]!);\n                }\n            },\n        };\n    }\n\n    private static createMapFixture(viewport: { x: number; y: number; width: number; height: number; }, pan: { x: number; y: number; }) {\n        IsoCoords.init({ x: 0, y: 0 });\n        const tiles = new Map<string, { rx: number; ry: number; z: number; }>();\n        for (let x = -32; x < 96; x += 1) {\n            for (let y = -32; y < 96; y += 1) {\n                tiles.set(`${x},${y}`, { rx: x, ry: y, z: (x + y) % 3 === 0 ? 1 : 0 });\n            }\n        }\n        const helper = new MapTileIntersectHelper({\n            tiles: {\n                getByMapCoords: (x: number, y: number) => tiles.get(`${x},${y}`),\n            },\n        } as any, {\n            viewport,\n            cameraPan: {\n                getPan: () => pan,\n            },\n        } as any);\n        const points = this.createScreenPointSamples(viewport, 40);\n        const pans = this.createPanSamples(24);\n        return {\n            helper,\n            run: () => {\n                for (let index = 0; index < points.length; index += 1) {\n                    const nextPan = pans[index % pans.length]!;\n                    pan.x = nextPan.x;\n                    pan.y = nextPan.y;\n                    helper.getTileAtScreenPoint(points[index]!);\n                    helper.intersectTilesByScreenPos(points[index]!);\n                }\n            },\n        };\n    }\n\n    private static createWorldViewportFixture(viewport: { x: number; y: number; width: number; height: number; }, pan: { x: number; y: number; }) {\n        const isoHelper = new WorldViewportHelper({\n            viewport,\n            cameraPan: {\n                getPan: () => pan,\n            },\n        } as any);\n        const camera = new THREE.OrthographicCamera(-400, 400, 300, -300, 0.1, 1000);\n        camera.position.set(0, 0, 100);\n        camera.lookAt(0, 0, 0);\n        camera.updateProjectionMatrix();\n        const cameraHelper = new WorldViewportHelper({\n            viewport,\n            cameraPan: {\n                getPan: () => pan,\n            },\n            camera,\n        } as any);\n        const worldPositions = Array.from({ length: 72 }, (_, index) => ({\n            x: (index - 36) * 48,\n            y: (index % 6) * 10,\n            z: ((index * 37) % 48 - 24) * 32,\n        }));\n        const pans = this.createPanSamples(36);\n        const screenBox = new THREE.Box2(new THREE.Vector2(120, 120), new THREE.Vector2(680, 480));\n        return {\n            isoHelper,\n            run: () => {\n                for (let index = 0; index < worldPositions.length; index += 1) {\n                    const nextPan = pans[index % pans.length]!;\n                    pan.x = nextPan.x;\n                    pan.y = nextPan.y;\n                    const worldPosition = worldPositions[index]!;\n                    isoHelper.distanceToViewport(worldPosition);\n                    isoHelper.distanceToViewportCenter(worldPosition);\n                    isoHelper.distanceToScreenBox(worldPosition, screenBox);\n                    cameraHelper.distanceToViewport(worldPosition);\n                    cameraHelper.distanceToViewportCenter(worldPosition);\n                    cameraHelper.distanceToScreenBox(worldPosition, screenBox);\n                }\n            },\n        };\n    }\n\n    private static createWorldSoundFixture(viewport: { x: number; y: number; width: number; height: number; }, pan: { x: number; y: number; }, mapTileHelper: MapTileIntersectHelper, worldViewportHelper: WorldViewportHelper) {\n        const localPlayer = { id: 'local' };\n        const enemyPlayer = { id: 'enemy' };\n        const frameDispatcher = new EventDispatcher<string, number>();\n        const fixtureSpecs = new Map<string | number, {\n            name: string;\n            volume: number;\n            minVolume: number;\n            type: SoundType[];\n            control: Set<SoundControl>;\n            limit: number;\n            loop: number;\n            range: number;\n        }>();\n        const worldSound = new WorldSound({\n            getSoundSpec: (key: string | number) => {\n                if (!fixtureSpecs.has(key)) {\n                    fixtureSpecs.set(key, {\n                        name: `fixture-${String(key)}`,\n                        volume: 100,\n                        minVolume: 20,\n                        type: [SoundType.Global],\n                        control: new Set(),\n                        limit: 4,\n                        loop: 0,\n                        range: 8,\n                    });\n                }\n                return fixtureSpecs.get(key);\n            },\n            playWithOptions: () => undefined,\n        } as any, localPlayer as any, {\n            getShroudTypeByTileCoords: () => 0,\n        } as any, worldViewportHelper as any, mapTileHelper as any, {\n            onObjectRemoved: frameDispatcher.asEvent(),\n        } as any, {\n            viewport,\n        } as any, {\n            onFrame: frameDispatcher.asEvent(),\n        } as any);\n        const handles = Array.from({ length: 96 }, () => ({\n            isPlaying: () => true,\n            stop: () => undefined,\n            setVolume: (_volume: number) => undefined,\n            setPan: (_pan: number) => undefined,\n        }));\n        (worldSound as any).soundInstances = handles.map((handle, index) => ({\n            spec: {\n                name: `test-${index % 6}`,\n                volume: 100,\n                minVolume: 20,\n                type: index % 3 === 0 ? [SoundType.Screen] : index % 3 === 1 ? [SoundType.Local] : [SoundType.Global],\n                control: new Set(index % 2 === 0 ? [SoundControl.Loop] : []),\n                limit: 4,\n                loop: index % 2 === 0 ? 1 : 0,\n                range: 8,\n            },\n            gameObject: {\n                position: {\n                    worldPosition: {\n                        x: (index - 48) * (Coords.LEPTONS_PER_TILE / 2),\n                        y: 0,\n                        z: ((index % 12) - 6) * (Coords.LEPTONS_PER_TILE / 2),\n                    },\n                },\n            },\n            worldPos: {\n                x: (index - 48) * (Coords.LEPTONS_PER_TILE / 2),\n                y: 0,\n                z: ((index % 12) - 6) * (Coords.LEPTONS_PER_TILE / 2),\n            },\n            player: index % 5 === 0 ? enemyPlayer : localPlayer,\n            handle,\n            gain: 1,\n            volume: 0,\n            loop: index % 2 === 0,\n        }));\n        const pans = this.createPanSamples(32);\n        return {\n            run: () => {\n                for (let index = 0; index < 72; index += 1) {\n                    const nextPan = pans[index % pans.length]!;\n                    pan.x = nextPan.x;\n                    pan.y = nextPan.y;\n                    (worldSound as any).update();\n                }\n            },\n        };\n    }\n\n    private static createScreenPointSamples(viewport: { x: number; y: number; width: number; height: number; }, count: number) {\n        return Array.from({ length: count }, (_, index) => ({\n            x: viewport.x + 24 + (index * 37) % (viewport.width - 48),\n            y: viewport.y + 24 + (index * 29) % (viewport.height - 48),\n        }));\n    }\n\n    private static createPanSamples(count: number) {\n        return Array.from({ length: count }, (_, index) => ({\n            x: Math.sin(index / 2) * 80,\n            y: Math.cos(index / 3) * 48,\n        }));\n    }\n\n    private static createRandom(seed: number): () => number {\n        let state = seed >>> 0;\n        return () => {\n            state = (state * 1664525 + 1013904223) >>> 0;\n            return state / 0x100000000;\n        };\n    }\n\n    private static computeMetricMedian(samples: BenchmarkSample[], metricName: string): MetricSummary {\n        const totals = samples.map((sample) => sample.telemetry.metrics[metricName]?.totalMs ?? 0);\n        const avgs = samples.map((sample) => sample.telemetry.metrics[metricName]?.avgMs ?? 0);\n        return {\n            totalMsMedian: this.median(totals),\n            avgMsMedian: this.median(avgs),\n        };\n    }\n\n    private static computeRegressionPct(baseline: number, candidate: number): number {\n        if (baseline <= 0) {\n            return 0;\n        }\n        return ((candidate - baseline) / baseline) * 100;\n    }\n\n    private static median(values: number[]): number {\n        const sorted = [...values].sort((left, right) => left - right);\n        const middle = Math.floor(sorted.length / 2);\n        if (sorted.length % 2 === 0) {\n            return (sorted[middle - 1]! + sorted[middle]!) / 2;\n        }\n        return sorted[middle] ?? 0;\n    }\n\n    private static nextFrame(): Promise<void> {\n        return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n    }\n\n    static destroy(): void {\n        TestToolSupport.clearState('performance');\n        this.host?.remove();\n        this.summaryBlock = undefined;\n        this.host = undefined;\n        this.homeButton?.remove();\n        this.homeButton = undefined;\n        this.currentOptions = undefined;\n    }\n}\n"
  },
  {
    "path": "src/tools/ShpTester.ts",
    "content": "import { Renderer } from \"@/engine/gfx/Renderer\";\nimport { UiScene } from \"@/gui/UiScene\";\nimport { Hud } from \"@/gui/screen/game/component/Hud\";\nimport { Engine } from \"@/engine/Engine\";\nimport { Rules } from \"@/game/rules/Rules\";\nimport { Art } from \"@/game/art/Art\";\nimport { Country } from \"@/game/Country\";\nimport { World } from \"@/game/World\";\nimport { ObjectFactory } from \"@/game/gameobject/ObjectFactory\";\nimport { ObjectArt } from \"@/game/art/ObjectArt\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { UiAnimationLoop } from \"@/engine/UiAnimationLoop\";\nimport { Game } from \"@/game/Game\";\nimport { JsxRenderer } from \"@/gui/jsx/JsxRenderer\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { Alliances } from \"@/game/Alliances\";\nimport { PlayerList } from \"@/game/PlayerList\";\nimport { Pointer } from \"@/gui/Pointer\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\nimport { TileCollection } from \"@/game/map/TileCollection\";\nimport { TileOccupation } from \"@/game/map/TileOccupation\";\nimport { Bridges } from \"@/game/map/Bridges\";\nimport { UnitSelection } from \"@/game/gameobject/selection/UnitSelection\";\nimport { GameModeType } from \"@/game/ini/GameModeType\";\nimport { getRandomInt, clamp } from \"@/util/math\";\nimport { TheaterType } from \"@/engine/TheaterType\";\nimport { GameMap } from \"@/game/GameMap\";\nimport { RadarTrait } from \"@/game/player/trait/RadarTrait\";\nimport { Minimap } from \"@/gui/screen/game/component/Minimap\";\nimport { Production } from \"@/game/player/production/Production\";\nimport { CombatantSidebarModel } from \"@/gui/screen/game/component/hud/viewmodel/CombatantSidebarModel\";\nimport { MessageList } from \"@/gui/screen/game/component/hud/viewmodel/MessageList\";\nimport { MapShroudTrait } from \"@/game/trait/MapShroudTrait\";\nimport { SellTrait } from \"@/game/trait/SellTrait\";\nimport { MapBounds } from \"@/game/map/MapBounds\";\nimport { mixDatabase } from \"@/engine/mixDatabase\";\nimport { CommandBarButtonType } from \"@/gui/screen/game/component/hud/commandBar/CommandBarButtonType\";\nimport { CanvasMetrics } from \"@/gui/CanvasMetrics\";\nimport { StalemateDetectTrait } from \"@/game/trait/StalemateDetectTrait\";\nimport { CountdownTimer } from \"@/game/CountdownTimer\";\nimport { IniSection } from \"@/data/IniSection\";\nimport { ChatHistory } from \"@/gui/chat/ChatHistory\";\nimport { SidebarItemTargetType, SidebarCategory, SidebarItemStatus } from \"@/gui/screen/game/component/hud/viewmodel/SidebarModel\";\nimport { PlayerFactory } from \"@/game/player/PlayerFactory\";\nimport { ResourceType } from \"@/engine/resourceConfigs\";\nimport { TestToolSupport, type TestToolRuntimeContext } from \"@/tools/TestToolSupport\";\ndeclare const THREE: any;\ninterface GameOptions {\n    superWeapons: boolean;\n    gameSpeed: number;\n}\ninterface SidebarItem {\n    target: {\n        type: SidebarItemTargetType;\n        rules: any;\n    };\n    cameo: string;\n    disabled: boolean;\n    progress: number;\n    quantity: number;\n    status: SidebarItemStatus;\n}\ninterface MenuButton {\n    label: string;\n    disabled?: boolean;\n    isBottom?: boolean;\n    onClick: () => void;\n}\nexport class ShpTester {\n    private static disposables = new CompositeDisposable();\n    static async main(mixFileLoader: any, gameMap: any, parentElement: HTMLElement, strings: any, context: TestToolRuntimeContext = {}): Promise<void> {\n        await TestToolSupport.ensureTheater(TheaterType.Temperate, context.cdnResourceLoader, [ResourceType.UiAlly, ResourceType.Cameo]);\n        this.buildHomeButton();\n        const hostElement = TestToolSupport.prepareHost(context, 800, 600);\n        const renderer = new Renderer(800, 600);\n        renderer.init(hostElement);\n        TestToolSupport.placeRendererCanvas(renderer, 0, 0);\n        renderer.initStats(document.body);\n        this.disposables.add(renderer);\n        const uiScene = UiScene.factory({\n            x: 0,\n            y: 0,\n            width: 800,\n            height: 600,\n        });\n        this.disposables.add(uiScene);\n        const cameoDatabase = mixDatabase.get(\"cameo.mix\");\n        if (!cameoDatabase) {\n            throw new Error(\"Missing file list database for cameos\");\n        }\n        const rules = new Rules(Engine.getRules());\n        const art = new Art(rules, Engine.getArt(), undefined, undefined);\n        const theater = await Engine.loadTheater(TheaterType.Temperate);\n        const gameMapInstance = new GameMap(gameMap, theater.tileSets, rules, (min: number, max: number) => getRandomInt(min, max));\n        const gameOptions: GameOptions = {\n            superWeapons: false,\n            gameSpeed: 5\n        };\n        const playerFactory = new PlayerFactory(rules, gameOptions, []);\n        const country = Country.factory(\"Americans\", rules as any);\n        const player = playerFactory.createCombatant(\"Player\", country, 0, \"Red\", false, undefined);\n        (player as any).radarTrait = new RadarTrait();\n        (player as any).production = new Production(player, 10, gameOptions, rules, [\n            ...(rules as any).buildingRules.values(),\n            ...(rules as any).infantryRules.values(),\n        ]);\n        this.disposables.add(player);\n        const world = new World();\n        const playerList = new PlayerList();\n        const alliances = new Alliances(playerList);\n        const unitSelection = new UnitSelection();\n        const tileCollection = new TileCollection([], null, rules.general, () => getRandomInt(0, 1000));\n        const tileOccupation = new TileOccupation(tileCollection);\n        const mapBounds = new MapBounds();\n        const bridges = new Bridges(theater.tileSets, tileCollection, tileOccupation, mapBounds, rules);\n        const gameSpeedVar = new BoxedVar(1);\n        const objectFactory = new ObjectFactory(tileCollection, tileOccupation, bridges, gameSpeedVar);\n        const game = new Game(world, gameMapInstance, rules, art, null, \"0\", 0, gameOptions, GameModeType.Battle, playerList, unitSelection, alliances, gameSpeedVar, objectFactory, null);\n        game.addPlayer(player);\n        game.mapShroudTrait = new MapShroudTrait(gameMapInstance, alliances);\n        game.traits.add(game.mapShroudTrait);\n        game.sellTrait = new SellTrait(game, game.rules.general);\n        game.traits.add(game.sellTrait);\n        const buildingTypes = [\n            \"GACNST\",\n            \"GAPOWR\",\n            \"GAREFN\",\n            \"GAPILE\",\n            \"GAAIRC\",\n            \"GAWEAP\",\n            \"GATECH\",\n            \"NACNST\",\n            \"NAPOWR\",\n        ];\n        buildingTypes.forEach((buildingType) => {\n            player.addOwnedObject(objectFactory.create(ObjectType.Building, buildingType, rules, art));\n        });\n        const combatantSidebarModel = new CombatantSidebarModel(player, game);\n        combatantSidebarModel.powerDrained = 150;\n        combatantSidebarModel.powerGenerated = 300;\n        (player as any).radarTrait.setDisabled(false);\n        const powerUpdateInterval = setInterval(() => {\n            combatantSidebarModel.powerDrained = getRandomInt(0, 300);\n            combatantSidebarModel.powerGenerated = getRandomInt(200, 1000);\n            console.log(`Set power = ${combatantSidebarModel.powerGenerated}, drain = ${combatantSidebarModel.powerDrained}`);\n        }, 5000);\n        this.disposables.add(() => clearInterval(powerUpdateInterval));\n        player.credits = 5000;\n        const creditsUpdateInterval = setInterval(() => {\n            player.credits = clamp(player.credits + getRandomInt(-1000, 1000), 0, 1000000);\n            console.log(\"Set credits\", player.credits);\n        }, 5000);\n        this.disposables.add(() => clearInterval(creditsUpdateInterval));\n        for (const availableObject of (player as any).production.getAvailableObjects()) {\n            const objectArt = ObjectArt.factory((availableObject as any).type, availableObject as any, Engine.getArt(), Engine.getArt().getSection((availableObject as any).imageName) ??\n                new IniSection((availableObject as any).imageName));\n            const tab = combatantSidebarModel.getTabForQueueType((player as any).production.getQueueTypeForObject(availableObject));\n            const sidebarItem: SidebarItem = {\n                target: {\n                    type: SidebarItemTargetType.Techno,\n                    rules: availableObject\n                },\n                cameo: objectArt.cameo,\n                disabled: tab.id === SidebarCategory.Structures,\n                progress: 0,\n                quantity: 0,\n                status: SidebarItemStatus.Idle,\n            };\n            tab.items.push(sidebarItem);\n        }\n        const firstActiveTabItem = combatantSidebarModel.activeTab.items[1];\n        if (firstActiveTabItem) {\n            firstActiveTabItem.disabled = false;\n            firstActiveTabItem.progress = 0.75;\n            firstActiveTabItem.quantity = 2;\n            firstActiveTabItem.status = SidebarItemStatus.OnHold;\n        }\n        const firstInfantryItem = combatantSidebarModel.tabs[SidebarCategory.Infantry].items[0];\n        if (firstInfantryItem) {\n            firstInfantryItem.quantity = 5;\n            firstInfantryItem.progress = 1;\n            firstInfantryItem.status = SidebarItemStatus.Ready;\n        }\n        const canvasMetrics = new CanvasMetrics(renderer.getCanvas(), window);\n        canvasMetrics.init();\n        this.disposables.add(canvasMetrics);\n        const pointer = Pointer.factory(Engine.getImages().get(\"mouse.shp\"), Engine.getPalettes().get(\"mousepal.pal\"), renderer, document, canvasMetrics, new BoxedVar(false));\n        pointer.init();\n        pointer.lock();\n        this.disposables.add(pointer);\n        uiScene.add(pointer.getSprite());\n        const jsxRenderer = new JsxRenderer(Engine.getImages(), Engine.getPalettes(), uiScene.getCamera(), pointer.pointerEvents);\n        const messageList = new MessageList(game.rules.audioVisual.messageDuration, 6, player);\n        const systemMessages = [\n            \"txt_low_power\",\n            \"txt_space_cant_save\",\n            \"txt_receiving_scenario\",\n            \"txt_bad_chankey\",\n        ];\n        let messageTimeout: number;\n        const addRandomMessage = (): void => {\n            const messageKey = strings.get(systemMessages[getRandomInt(0, systemMessages.length - 1)]);\n            console.log(\"Add system message:\", messageKey);\n            messageList.addSystemMessage(messageKey, \"#\" + new THREE.Color(Math.random(), Math.random(), Math.random()).getHexString());\n            messageTimeout = setTimeout(addRandomMessage, 5000 * Math.random());\n        };\n        messageTimeout = setTimeout(addRandomMessage, 5000 * Math.random());\n        this.disposables.add(() => clearTimeout(messageTimeout));\n        const hud = new Hud((player.country as any).side, uiScene.viewport, Engine.getImages() as any, Engine.getPalettes() as any, cameoDatabase, combatantSidebarModel, messageList, new ChatHistory(), new BoxedVar(\"\"), new BoxedVar(false), undefined, [], new StalemateDetectTrait(), new CountdownTimer(), jsxRenderer, strings, Object.values(CommandBarButtonType).filter((value) => typeof value === \"number\") as CommandBarButtonType[]);\n        const minimap = new Minimap(game, player, 0xFFFFFF, rules.general.radar as any);\n        minimap.setPointerEvents(pointer.pointerEvents);\n        hud.setMinimap(minimap);\n        this.disposables.add(minimap);\n        uiScene.add(hud);\n        hud.onSidebarSlotClick.subscribe((slotData: any) => {\n            console.log(\"clicked\", slotData);\n        });\n        hud.onOptButtonClick.subscribe(() => {\n            pointer.unlock();\n            const menuButtons: MenuButton[] = [\n                {\n                    label: \"Button 1\",\n                    onClick(): void {\n                        console.log(\"button 1 clicked\");\n                    },\n                },\n                {\n                    label: \"Button 2\",\n                    disabled: true,\n                    onClick(): void {\n                        console.error(\"button 2 should not trigger onClick\");\n                    },\n                },\n                {\n                    label: \"Exit\",\n                    isBottom: true,\n                    onClick(): void {\n                        pointer.lock();\n                        hud.hideSidebarMenu();\n                    },\n                },\n            ];\n            hud.showSidebarMenu(menuButtons);\n        });\n        hud.onRepairButtonClick.subscribe(() => {\n            (player as any).radarTrait.setDisabled(!(player as any).radarTrait.isDisabled());\n        });\n        hud.onCommandBarButtonClick.subscribe((buttonType: CommandBarButtonType) => {\n            console.log(\"Clicked command bar -> \" + CommandBarButtonType[buttonType]);\n        });\n        const startTime = new Date().getTime();\n        renderer.addScene(uiScene);\n        const uiAnimationLoop = new UiAnimationLoop(renderer);\n        this.disposables.add(uiAnimationLoop);\n        uiAnimationLoop.start();\n        const endTime = new Date().getTime();\n        console.log(\"Rendering took \" + (endTime - startTime) + \"ms\");\n        hostElement.appendChild(uiScene.getHtmlContainer().getElement());\n        this.disposables.add(() => {\n            uiScene.getHtmlContainer().getElement().remove();\n        });\n        TestToolSupport.setState('shp', {\n            activeTab: combatantSidebarModel.activeTab.id,\n            structureItems: combatantSidebarModel.tabs[SidebarCategory.Structures].items.length,\n            infantryItems: combatantSidebarModel.tabs[SidebarCategory.Infantry].items.length,\n            messageCount: messageList.getAll().length,\n        });\n    }\n    private static buildHomeButton(): void {\n        const homeButton = document.createElement('button');\n        homeButton.innerHTML = '点此返回主页';\n        homeButton.style.cssText = `\n      position: fixed;\n      left: 50%;\n      top: 10px;\n      transform: translateX(-50%);\n      padding: 10px 20px;\n      background-color: rgba(0, 0, 0, 0.8);\n      color: white;\n      border: 2px solid rgba(255, 255, 255, 0.3);\n      border-radius: 6px;\n      cursor: pointer;\n      font-size: 16px;\n      font-weight: bold;\n      z-index: 1000;\n      transition: all 0.3s ease;\n      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n    `;\n        homeButton.onmouseover = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';\n            homeButton.style.transform = 'translateX(-50%) translateY(-2px)';\n            homeButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';\n        };\n        homeButton.onmouseout = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';\n            homeButton.style.transform = 'translateX(-50%) translateY(0)';\n            homeButton.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';\n        };\n        homeButton.onclick = () => {\n            window.location.hash = '/';\n        };\n        document.body.appendChild(homeButton);\n        this.disposables.add(() => homeButton.remove());\n    }\n    static destroy(): void {\n        TestToolSupport.clearState('shp');\n        this.disposables.dispose();\n        try {\n            import(\"@/engine/renderable/entity/PipOverlay\").then(({ PipOverlay }) => {\n                (PipOverlay as any)?.clearCaches?.();\n            }).catch(() => { });\n            import(\"@/engine/gfx/TextureUtils\").then(({ TextureUtils }) => {\n                if ((TextureUtils as any)?.cache) {\n                    (TextureUtils as any).cache.forEach((tex: any) => tex.dispose?.());\n                    (TextureUtils as any).cache.clear();\n                }\n            }).catch(() => { });\n        }\n        catch (err) {\n            console.warn('[ShpTester] Failed to clear caches during destroy:', err);\n        }\n    }\n}\n"
  },
  {
    "path": "src/tools/SoundTester.ts",
    "content": "import { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { AudioBagFile } from \"@/data/AudioBagFile\";\nimport { IdxFile } from \"@/data/IdxFile\";\nimport { Engine } from \"@/engine/Engine\";\nimport { LazyResourceCollection } from \"@/engine/LazyResourceCollection\";\nimport { WavFile } from \"@/data/WavFile\";\nimport { TestToolSupport, type TestToolRuntimeContext } from \"@/tools/TestToolSupport\";\nexport class SoundTester {\n    private static disposables = new CompositeDisposable();\n    private static sounds: LazyResourceCollection<WavFile>;\n    private static audioBag: AudioBagFile;\n    private static listEl: HTMLDivElement;\n    private static homeButton?: HTMLButtonElement;\n    private static hostElement?: HTMLElement;\n    static async main(fileSystem: any, containerElement: HTMLElement, context: TestToolRuntimeContext = {}): Promise<void> {\n        await TestToolSupport.ensureAudio(context.cdnResourceLoader);\n        const hostElement = this.hostElement = TestToolSupport.prepareHost(context, 212, 600);\n        this.sounds = Engine.getSounds();\n        this.audioBag = new AudioBagFile();\n        const audioBagFile = fileSystem.openFile(\"audio.bag\");\n        const audioIdxFile = fileSystem.openFile(\"audio.idx\");\n        this.audioBag.fromVirtualFile(audioBagFile, new IdxFile(audioIdxFile.stream));\n        fileSystem.addArchive(this.audioBag, \"audio.bag\");\n        this.buildBrowser(hostElement);\n        this.buildHomeButton();\n        TestToolSupport.setState('sound', {\n            soundCount: this.audioBag.getFileList().length,\n            selectedSound: null,\n        });\n    }\n    private static selectSound(soundName: string): void {\n        const audioContext = new AudioContext();\n        const gainNode = audioContext.createGain();\n        gainNode.gain.value = 0.5;\n        const soundData = new Uint8Array(this.sounds.get(soundName).getData()).buffer;\n        audioContext.decodeAudioData(soundData, (audioBuffer: AudioBuffer) => {\n            const source = audioContext.createBufferSource();\n            source.buffer = audioBuffer;\n            source.connect(gainNode).connect(audioContext.destination);\n            source.start(0);\n        }, (error: DOMException) => console.log(error));\n        TestToolSupport.setState('sound', {\n            soundCount: this.audioBag.getFileList().length,\n            selectedSound: soundName,\n        });\n    }\n    private static buildBrowser(hostElement: HTMLElement): void {\n        const listElement = this.listEl = document.createElement(\"div\");\n        listElement.style.position = \"absolute\";\n        listElement.style.right = \"0\";\n        listElement.style.top = \"0\";\n        listElement.style.height = \"600px\";\n        listElement.style.width = \"200px\";\n        listElement.style.overflowY = \"auto\";\n        listElement.style.padding = \"5px\";\n        listElement.style.background = \"rgba(255, 255, 255, 0.5)\";\n        listElement.style.border = \"1px black solid\";\n        listElement.appendChild(document.createTextNode(\"Sound files:\"));\n        const fileList = this.audioBag.getFileList();\n        fileList.forEach((fileName: string) => {\n            const link = document.createElement(\"a\");\n            link.style.display = \"block\";\n            link.textContent = fileName;\n            link.setAttribute(\"href\", \"javascript:;\");\n            link.addEventListener(\"click\", () => {\n                this.selectSound(fileName);\n            });\n            listElement.appendChild(link);\n        });\n        hostElement.appendChild(listElement);\n        TestToolSupport.applyPanelTheme(listElement);\n    }\n    private static buildHomeButton(): void {\n        const homeButton = this.homeButton = document.createElement('button');\n        homeButton.innerHTML = '点此返回主页';\n        homeButton.style.cssText = `\n      position: fixed;\n      left: 50%;\n      top: 10px;\n      transform: translateX(-50%);\n      padding: 10px 20px;\n      background-color: rgba(0, 0, 0, 0.8);\n      color: white;\n      border: 2px solid rgba(255, 255, 255, 0.3);\n      border-radius: 6px;\n      cursor: pointer;\n      font-size: 16px;\n      font-weight: bold;\n      z-index: 1000;\n      transition: all 0.3s ease;\n      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n    `;\n        homeButton.onmouseover = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';\n            homeButton.style.transform = 'translateX(-50%) translateY(-2px)';\n            homeButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';\n        };\n        homeButton.onmouseout = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';\n            homeButton.style.transform = 'translateX(-50%) translateY(0)';\n            homeButton.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';\n        };\n        homeButton.onclick = () => {\n            window.location.hash = '/';\n        };\n        document.body.appendChild(homeButton);\n    }\n    static destroy(): void {\n        this.listEl?.remove();\n        if (this.homeButton) {\n            this.homeButton.remove();\n            this.homeButton = undefined;\n        }\n        this.disposables.dispose();\n        TestToolSupport.clearState('sound');\n    }\n}\n"
  },
  {
    "path": "src/tools/TestToolSupport.ts",
    "content": "import { DataStream } from '@/data/DataStream';\nimport { MapFile } from '@/data/MapFile';\nimport { MixFile } from '@/data/MixFile';\nimport { Engine } from '@/engine/Engine';\nimport { ResourceType, theaterSpecificResources } from '@/engine/resourceConfigs';\nimport { TheaterType } from '@/engine/TheaterType';\nimport { MapFileLoader } from '@/gui/screen/game/MapFileLoader';\nimport { Renderer } from '@/engine/gfx/Renderer';\n\ntype CdnLoader = {\n    loadResources: (resources: ResourceType[], cancellationToken?: any, onProgress?: (progress: number) => void) => Promise<any>;\n    getResourceFileName: (resourceType: ResourceType) => string;\n};\n\ntype MapResourceLoader = {\n    loadBinary: (fileName: string, cancellationToken?: any) => Promise<ArrayBuffer>;\n};\n\nexport interface TestToolRuntimeContext {\n    cdnResourceLoader?: CdnLoader;\n    mapResourceLoader?: MapResourceLoader;\n    rootElement?: HTMLElement;\n}\n\nexport class TestToolSupport {\n    private static panel?: HTMLDivElement;\n    private static panelBody?: HTMLPreElement;\n    private static activeTool?: string;\n    private static fallbackHost?: HTMLDivElement;\n    private static readonly panelBackground = 'linear-gradient(180deg, rgba(120, 8, 8, 0.94), rgba(50, 0, 0, 0.94))';\n    private static readonly panelBorder = '1px solid #ff6400';\n    private static readonly panelTextColor = '#ffd84a';\n\n    static async ensureResourceTypes(resourceTypes: ResourceType[], cdnResourceLoader?: CdnLoader): Promise<string[]> {\n        if (!Engine.vfs) {\n            throw new Error('VFS not initialized');\n        }\n        const missingTypes: ResourceType[] = [];\n        const loadedArchives: string[] = [];\n        for (const resourceType of resourceTypes) {\n            const fileName = cdnResourceLoader\n                ? cdnResourceLoader.getResourceFileName(resourceType)\n                : this.getResourceFileName(resourceType);\n            loadedArchives.push(fileName);\n            if (!Engine.vfs.hasArchive(fileName)) {\n                missingTypes.push(resourceType);\n            }\n        }\n        if (!missingTypes.length) {\n            return loadedArchives;\n        }\n        if (cdnResourceLoader) {\n            const resources = await cdnResourceLoader.loadResources(missingTypes);\n            for (const resourceType of missingTypes) {\n                const fileName = cdnResourceLoader.getResourceFileName(resourceType);\n                if (Engine.vfs.hasArchive(fileName)) {\n                    continue;\n                }\n                const bytes = resources.pop(resourceType);\n                Engine.vfs.addArchive(new MixFile(new DataStream(bytes)), fileName);\n            }\n            return loadedArchives;\n        }\n        for (const resourceType of missingTypes) {\n            const fileName = this.getResourceFileName(resourceType);\n            await Engine.vfs.addMixFile(fileName);\n        }\n        return loadedArchives;\n    }\n\n    static async ensureTheater(theaterType: TheaterType, cdnResourceLoader?: CdnLoader, extraResources: ResourceType[] = []): Promise<void> {\n        const theaterResources = theaterSpecificResources.get(theaterType) ?? [];\n        await this.ensureResourceTypes([...extraResources, ...theaterResources], cdnResourceLoader);\n    }\n\n    static async ensureAudio(cdnResourceLoader?: CdnLoader): Promise<void> {\n        await this.ensureResourceTypes([ResourceType.Sounds], cdnResourceLoader);\n        if (!Engine.vfs) {\n            throw new Error('VFS not initialized');\n        }\n        if (!Engine.vfs.hasArchive('audio.bag') && Engine.vfs.fileExists('audio.bag')) {\n            await Engine.vfs.addBagFile('audio.bag');\n        }\n    }\n\n    static async loadMap(mapResourceLoader: MapResourceLoader, filename: string): Promise<MapFile> {\n        const loader = new MapFileLoader(mapResourceLoader, Engine.vfs);\n        const mapFile = await loader.load(filename);\n        return new MapFile(mapFile);\n    }\n\n    static getExistingFiles(fileNames: string[]): string[] {\n        if (!Engine.vfs) {\n            return [];\n        }\n        return fileNames.filter((fileName) => Engine.vfs?.fileExists(fileName));\n    }\n\n    static prepareHost(context: TestToolRuntimeContext, width: number, height: number): HTMLElement {\n        const rootElement = context.rootElement;\n        if (rootElement) {\n            rootElement.replaceChildren();\n            rootElement.style.position = 'relative';\n            rootElement.style.width = `${width}px`;\n            rootElement.style.height = `${height}px`;\n            rootElement.style.display = 'block';\n            rootElement.style.overflow = 'visible';\n            rootElement.style.transform = '';\n            rootElement.style.transformOrigin = '';\n            rootElement.style.willChange = '';\n            rootElement.style.flex = '0 0 auto';\n            return rootElement;\n        }\n        if (!this.fallbackHost) {\n            const host = document.createElement('div');\n            host.style.position = 'fixed';\n            host.style.left = '50%';\n            host.style.top = '50%';\n            host.style.transform = 'translate(-50%, -50%)';\n            host.style.zIndex = '1';\n            document.body.appendChild(host);\n            this.fallbackHost = host;\n        }\n        this.fallbackHost.replaceChildren();\n        this.fallbackHost.style.width = `${width}px`;\n        this.fallbackHost.style.height = `${height}px`;\n        return this.fallbackHost;\n    }\n\n    static placeRendererCanvas(renderer: Renderer, left: number = 0, top: number = 0): HTMLCanvasElement {\n        const canvas = renderer.getCanvas();\n        canvas.style.position = 'absolute';\n        canvas.style.left = `${left}px`;\n        canvas.style.top = `${top}px`;\n        canvas.style.display = 'block';\n        return canvas;\n    }\n\n    static applyPanelTheme(panel: HTMLElement): void {\n        panel.style.background = this.panelBackground;\n        panel.style.border = this.panelBorder;\n        panel.style.color = this.panelTextColor;\n        panel.style.boxShadow = '0 10px 24px rgba(0, 0, 0, 0.35), inset 0 0 0 1px rgba(255, 120, 0, 0.18)';\n        panel.querySelectorAll('button').forEach((button) => this.applyButtonTheme(button as HTMLButtonElement));\n        panel.querySelectorAll('select').forEach((select) => this.applySelectTheme(select as HTMLSelectElement));\n        panel.querySelectorAll('a').forEach((link) => this.applyLinkTheme(link as HTMLAnchorElement));\n        panel.querySelectorAll('input').forEach((input) => this.applyInputTheme(input as HTMLInputElement));\n        panel.querySelectorAll('hr').forEach((line) => {\n            const hr = line as HTMLHRElement;\n            hr.style.border = '0';\n            hr.style.height = '1px';\n            hr.style.background = 'rgba(255, 184, 74, 0.28)';\n        });\n    }\n\n    static applyHomeButtonTheme(button: HTMLButtonElement): void {\n        button.style.backgroundColor = '#7f0909';\n        button.style.color = this.panelTextColor;\n        button.style.border = '2px solid #ff6400';\n        button.style.textShadow = '0 1px 0 rgba(0, 0, 0, 0.55)';\n        button.style.boxShadow = '0 3px 10px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 190, 90, 0.14)';\n    }\n\n    private static applyButtonTheme(button: HTMLButtonElement): void {\n        button.style.background = 'linear-gradient(180deg, #a10d0d, #6b0000)';\n        button.style.color = this.panelTextColor;\n        button.style.border = '1px solid #ff6400';\n        button.style.borderRadius = '2px';\n        button.style.textShadow = '0 1px 0 rgba(0, 0, 0, 0.55)';\n        button.style.boxShadow = 'inset 0 1px 0 rgba(255, 204, 96, 0.12)';\n        button.style.minHeight = '24px';\n        button.style.padding = button.style.padding || '2px 8px';\n        if (!button.disabled) {\n            button.style.cursor = 'pointer';\n        }\n    }\n\n    private static applySelectTheme(select: HTMLSelectElement): void {\n        select.style.background = '#4a0000';\n        select.style.color = this.panelTextColor;\n        select.style.border = '1px solid #ff6400';\n        select.style.borderRadius = '2px';\n        select.style.minHeight = '24px';\n    }\n\n    private static applyLinkTheme(link: HTMLAnchorElement): void {\n        link.style.color = this.panelTextColor;\n        link.style.background = 'rgba(110, 6, 6, 0.72)';\n        link.style.border = '1px solid rgba(255, 100, 0, 0.55)';\n        link.style.borderRadius = '2px';\n        link.style.padding = '4px 6px';\n        link.style.marginBottom = '4px';\n        link.style.textDecoration = 'none';\n        link.style.cursor = 'pointer';\n    }\n\n    private static applyInputTheme(input: HTMLInputElement): void {\n        if (input.type === 'checkbox' || input.type === 'radio' || input.type === 'range') {\n            input.style.accentColor = '#ff6400';\n            return;\n        }\n        input.style.background = '#4a0000';\n        input.style.color = this.panelTextColor;\n        input.style.border = '1px solid #ff6400';\n        input.style.borderRadius = '2px';\n    }\n\n    static setState(tool: string, state: Record<string, unknown>): void {\n        const snapshot = {\n            tool,\n            state,\n            archives: Engine.vfs?.listArchives?.() ?? [],\n            updatedAt: Date.now(),\n        };\n        (window as any).__ra2test = snapshot;\n        if (this.panel) {\n            this.panel.remove();\n            this.panel = undefined;\n            this.panelBody = undefined;\n            this.activeTool = undefined;\n        }\n    }\n\n    static enumOptions(enumType: Record<string, string | number>, values: number[]): Array<{ value: number; label: string; }> {\n        return values.map((value) => ({\n            value,\n            label: this.enumLabel(enumType, value) ?? String(value),\n        }));\n    }\n\n    static enumLabel(enumType: Record<string, string | number>, value: number | undefined | null): string | null {\n        if (value === undefined || value === null) {\n            return null;\n        }\n        const label = enumType[value];\n        return typeof label === 'string' ? label : String(value);\n    }\n\n    static clearState(tool: string): void {\n        const currentState = (window as any).__ra2test;\n        if (currentState?.tool === tool) {\n            delete (window as any).__ra2test;\n        }\n        if (this.activeTool === tool) {\n            this.panel?.remove();\n            this.panel = undefined;\n            this.panelBody = undefined;\n            this.activeTool = undefined;\n        }\n    }\n\n    private static getResourceFileName(resourceType: ResourceType): string {\n        switch (resourceType) {\n            case ResourceType.IsoSnow:\n                return 'isosnow.mix';\n            case ResourceType.IsoTemp:\n                return 'isotemp.mix';\n            case ResourceType.IsoUrb:\n                return 'isourb.mix';\n            case ResourceType.BuildGen:\n                return 'build-gen.mix';\n            case ResourceType.TheaterSnow:\n                return 'snow.mix';\n            case ResourceType.TheaterTemp:\n                return 'temperat.mix';\n            case ResourceType.TheaterUrb:\n                return 'urban.mix';\n            case ResourceType.TheaterSnow2:\n                return 'sno.mix';\n            case ResourceType.TheaterTemp2:\n                return 'tem.mix';\n            case ResourceType.TheaterUrb2:\n                return 'urb.mix';\n            case ResourceType.Ui:\n                return 'ui.mix';\n            case ResourceType.UiAlly:\n                return 'sidec01.mix';\n            case ResourceType.UiSov:\n                return 'sidec02.mix';\n            case ResourceType.Anims:\n                return 'anims.mix';\n            case ResourceType.Vxl:\n                return 'vxl.mix';\n            case ResourceType.Cameo:\n                return 'cameo.mix';\n            case ResourceType.Ini:\n                return 'ini.mix';\n            case ResourceType.Strings:\n                return 'strings.mix';\n            case ResourceType.EvaAlly:\n                return 'eva-ally.mix';\n            case ResourceType.EvaSov:\n                return 'eva-sov.mix';\n            case ResourceType.Sounds:\n                return 'sounds.mix';\n            case ResourceType.HalloweenMix:\n                return 'expandspawn09.mix';\n            case ResourceType.XmasMix:\n                return 'expandspawn10.mix';\n            default:\n                throw new Error(`Unsupported resource type ${resourceType}`);\n        }\n    }\n}\n"
  },
  {
    "path": "src/tools/UnitMovementTester.ts",
    "content": "import { Renderer } from \"@/engine/gfx/Renderer\";\nimport { UiAnimationLoop } from \"@/engine/UiAnimationLoop\";\nimport { WorldScene } from \"@/engine/renderable/WorldScene\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\nimport { ShadowQuality } from \"@/engine/renderable/entity/unit/ShadowQuality\";\nimport { Engine } from \"@/engine/Engine\";\nimport { Rules } from \"@/game/rules/Rules\";\nimport { Art } from \"@/game/art/Art\";\nimport { TheaterType } from \"@/engine/TheaterType\";\nimport { GameMap } from \"@/game/GameMap\";\nimport { getRandomInt } from \"@/util/math\";\nimport { ImageFinder } from \"@/engine/ImageFinder\";\nimport { MapRenderable } from \"@/engine/renderable/entity/map/MapRenderable\";\nimport { Lighting } from \"@/engine/Lighting\";\nimport { LightingDirector } from \"@/engine/gfx/lighting/LightingDirector\";\nimport { IsoCoords } from \"@/engine/IsoCoords\";\nimport { CanvasMetrics } from \"@/gui/CanvasMetrics\";\nimport { CameraZoomControls } from \"@/tools/CameraZoomControls\";\nimport { PointerEvents } from \"@/gui/PointerEvents\";\nimport { Pointer } from \"@/gui/Pointer\";\nimport { PointerType } from \"@/engine/type/PointerType\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { MapTileIntersectHelper } from \"@/engine/util/MapTileIntersectHelper\";\nimport { RaycastHelper } from \"@/engine/util/RaycastHelper\";\nimport { WorldViewportHelper } from \"@/engine/util/WorldViewportHelper\";\nimport { EntityIntersectHelper } from \"@/engine/util/EntityIntersectHelper\";\nimport { World } from \"@/game/World\";\nimport { Player } from \"@/game/Player\";\nimport { PlayerList } from \"@/game/PlayerList\";\nimport { Alliances } from \"@/game/Alliances\";\nimport { UnitSelection } from \"@/game/gameobject/selection/UnitSelection\";\nimport { RenderableFactory } from \"@/engine/renderable/entity/RenderableFactory\";\nimport { RenderableManager } from \"@/engine/RenderableManager\";\nimport { Strings } from \"@/data/Strings\";\nimport { VxlBuilderFactory } from \"@/engine/renderable/builder/VxlBuilderFactory\";\nimport { VxlGeometryPool } from \"@/engine/renderable/builder/vxlGeometry/VxlGeometryPool\";\nimport { VxlGeometryCache } from \"@/engine/gfx/geometry/VxlGeometryCache\";\nimport { FlyerHelperMode } from \"@/engine/renderable/entity/unit/FlyerHelperMode\";\nimport { ObjectFactory } from \"@/game/gameobject/ObjectFactory\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { SelectionLevel } from \"@/game/gameobject/selection/SelectionLevel\";\nimport { Game } from \"@/game/Game\";\nimport { OrderType } from \"@/game/order/OrderType\";\nimport { MoveOrder } from \"@/game/order/MoveOrder\";\nimport { MapPanningHelper } from \"@/engine/util/MapPanningHelper\";\nimport { ResourceType } from \"@/engine/resourceConfigs\";\nimport { SellTrait } from \"@/game/trait/SellTrait\";\nimport { TestToolSupport, type TestToolRuntimeContext } from \"@/tools/TestToolSupport\";\ndeclare const THREE: any;\nexport class UnitMovementTester {\n    private static disposables = new CompositeDisposable();\n    private static renderer?: Renderer;\n    private static uiAnimationLoop?: UiAnimationLoop;\n    private static worldScene?: WorldScene;\n    private static renderableManager?: RenderableManager;\n    private static game?: Game;\n    private static gameTickTimer?: number;\n    private static currentUnit?: any;\n    private static currentVehicle?: any;\n    private static currentAircraft?: any;\n    private static unitSwitchEl?: HTMLDivElement;\n    private static selectBoxEl?: HTMLDivElement;\n    private static dragStart?: {\n        x: number;\n        y: number;\n    };\n    private static isDragging: boolean = false;\n    private static dragThreshold = 7;\n    private static entityIntersectHelper?: EntityIntersectHelper;\n    private static pointerEvents?: PointerEvents;\n    private static canvasMetrics?: CanvasMetrics;\n    private static pointer?: Pointer;\n    private static bodyUserSelectPrev?: string;\n    private static bodyWebkitUserSelectPrev?: string;\n    private static bodyMozUserSelectPrev?: string;\n    private static bodyMsUserSelectPrev?: string;\n    private static canvas?: HTMLCanvasElement;\n    static async main(mixFileLoader: any, gameMapFile: any, parentElement: HTMLElement, _strings: any, context: TestToolRuntimeContext = {}): Promise<void> {\n        await TestToolSupport.ensureTheater(TheaterType.Temperate, context.cdnResourceLoader, [ResourceType.UiAlly, ResourceType.Vxl, ResourceType.Anims]);\n        this.buildHomeButton();\n        const hostElement = TestToolSupport.prepareHost(context, 800, 600);\n        const renderer = (this.renderer = new Renderer(800, 600));\n        renderer.init(hostElement);\n        TestToolSupport.placeRendererCanvas(renderer, 0, 0);\n        renderer.initStats(document.body);\n        this.disposables.add(renderer);\n        const worldScene = (this.worldScene = WorldScene.factory({ x: 0, y: 0, width: 800, height: 600 }, new BoxedVar(true), new BoxedVar(ShadowQuality.High)));\n        this.disposables.add(worldScene);\n        IsoCoords.init({ x: 0, y: 0 });\n        worldScene.create3DObject();\n        (worldScene.scene as any).background = new (THREE as any).Color(0xE0E0E0);\n        const rules = new Rules(Engine.getRules());\n        const art = new Art(rules, Engine.getArt(), undefined, undefined);\n        const theater = await Engine.loadTheater(TheaterType.Temperate);\n        const gameMap = new GameMap(gameMapFile, theater.tileSets, rules, (min: number, max: number) => getRandomInt(min, max));\n        const lighting = new Lighting();\n        this.disposables.add(lighting);\n        worldScene.applyLighting(lighting);\n        const lightingDirector = new LightingDirector(lighting.mapLighting as any, renderer, new BoxedVar(1));\n        lightingDirector.init();\n        this.disposables.add(lightingDirector);\n        const imageFinder = new ImageFinder(Engine.getImages() as any, theater);\n        const mapRenderable = new MapRenderable(gameMap, undefined, { onChange: { subscribe() { }, unsubscribe() { } }, getRadLevel() { return 0; } }, lighting, theater, rules, art, imageFinder, worldScene.camera, new BoxedVar(false), 1, undefined as any, true);\n        (worldScene as any).add(mapRenderable as any);\n        const localSize = (gameMap as any).mapBounds.getLocalSize();\n        const computeMapScreenBounds = (ls: {\n            x: number;\n            y: number;\n            width: number;\n            height: number;\n        }) => {\n            const topLeft = IsoCoords.screenTileToScreen(ls.x, ls.y);\n            const bottomRight = IsoCoords.screenTileToScreen(ls.x + ls.width, ls.y + ls.height - 1);\n            return { x: topLeft.x, y: topLeft.y, width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y };\n        };\n        const mapScreenBounds = computeMapScreenBounds(localSize);\n        const panningHelper = new MapPanningHelper(gameMap as any);\n        worldScene.cameraPan.setPanLimits((panningHelper as any).computeCameraPanLimits(worldScene.viewport, mapScreenBounds));\n        const start = (gameMap as any).startingLocations?.[0] ?? { x: Math.floor(localSize.x + localSize.width / 2), y: Math.floor(localSize.y + localSize.height / 2) };\n        const initialPan = (panningHelper as any).computeCameraPanFromTile(start.x, start.y);\n        worldScene.cameraPan.setPan(initialPan);\n        const centerWorld = IsoCoords.screenTileToWorld(localSize.x + localSize.width / 2, localSize.y + localSize.height / 2);\n        worldScene.setLightFocusPoint(centerWorld.x, centerWorld.y);\n        const world = new World();\n        const player = new Player(\"Tester\");\n        const playerList = new PlayerList();\n        playerList.addPlayer(player);\n        const alliances = new Alliances(playerList);\n        const unitSelection = new UnitSelection();\n        const nextObjectId = new BoxedVar(1);\n        const objectFactory = new ObjectFactory((gameMap as any).tiles, (gameMap as any).tileOccupation, (gameMap as any).bridges, nextObjectId);\n        try {\n            const desiredColor = (rules as any).getMultiplayerColors?.().get?.(\"DarkRed\");\n            if (desiredColor)\n                (player as any).color = desiredColor;\n        }\n        catch { }\n        const botManager = { init() { }, update() { }, dispose() { } } as any;\n        const gameOpts: any = {\n            gameMode: 0,\n            gameSpeed: 5,\n            credits: 10000,\n            unitCount: 0,\n            shortGame: true,\n            superWeapons: false,\n            buildOffAlly: false,\n            mcvRepacks: false,\n            cratesAppear: false,\n            destroyableBridges: true,\n            multiEngineer: false,\n            noDogEngiKills: false,\n            mapName: \"mp03t4.map\",\n            mapTitle: \"Test\",\n            mapDigest: \"\",\n            mapSizeBytes: 0,\n            maxSlots: 2,\n            mapOfficial: true,\n            humanPlayers: [],\n            aiPlayers: []\n        };\n        const game = (this.game = new Game(world, gameMap, rules, art, {}, 1, Date.now(), gameOpts, \"Standard\", playerList, unitSelection, alliances, nextObjectId, objectFactory, botManager));\n        (game as any).mapShroudTrait = { getPlayerShroud() { return undefined; } };\n        (game as any).crateGeneratorTrait = { peekInsideCrate() { return undefined; }, pickupCrate() { } };\n        game.sellTrait = new SellTrait(game, game.rules.general);\n        game.traits.add(game.sellTrait);\n        const infantryTypes = [...(rules as any).infantryRules.keys()].filter((name: string) => art.hasObject(name, ObjectType.Infantry));\n        const infantryName = infantryTypes[0] ?? \"E1\";\n        const unit = objectFactory.create(ObjectType.Infantry, infantryName, rules as any, art as any);\n        game.changeObjectOwner(unit, player);\n        const startTile = (gameMap as any).tiles.getByMapCoords(start.x, start.y);\n        game.spawnObject(unit, startTile);\n        unitSelection.addToSelection(unit);\n        this.currentUnit = unit;\n        let vehicle: any | undefined;\n        const vehicleTypes = [...(rules as any).vehicleRules.keys()].filter((name: string) => art.hasObject(name, ObjectType.Vehicle));\n        if (vehicleTypes.length) {\n            vehicle = objectFactory.create(ObjectType.Vehicle, vehicleTypes[0], rules as any, art as any);\n            game.changeObjectOwner(vehicle, player);\n            const vTile = (gameMap as any).tiles.getByMapCoords(Math.min(start.x + 1, localSize.x + localSize.width - 1), start.y);\n            game.spawnObject(vehicle, vTile ?? startTile);\n            this.currentVehicle = vehicle;\n        }\n        let aircraft: any | undefined;\n        const aircraftTypes = [...(rules as any).aircraftRules.keys()].filter((name: string) => art.hasObject(name, ObjectType.Aircraft));\n        if (aircraftTypes.length) {\n            aircraft = objectFactory.create(ObjectType.Aircraft, aircraftTypes[0], rules as any, art as any);\n            game.changeObjectOwner(aircraft, player);\n            const aTile = (gameMap as any).tiles.getByMapCoords(start.x, Math.min(start.y + 1, localSize.y + localSize.height - 1));\n            game.spawnObject(aircraft, aTile ?? startTile);\n            this.currentAircraft = aircraft;\n        }\n        try {\n            const buildingTypes = [...(rules as any).buildingRules.keys()].filter((name: string) => art.hasObject(name, ObjectType.Building));\n            const pickCount = Math.min(3, buildingTypes.length);\n            const offsets = [\n                { dx: 16, dy: 5 },\n                { dx: 8, dy: 0 },\n                { dx: 3, dy: 8 }\n            ];\n            for (let i = 0; i < pickCount; i++) {\n                const name = buildingTypes[i];\n                const b = objectFactory.create(ObjectType.Building, name, rules as any, art as any);\n                game.changeObjectOwner(b, player);\n                const ox = offsets[i % offsets.length].dx;\n                const oy = offsets[i % offsets.length].dy;\n                const bx = Math.min(Math.max(localSize.x, start.x + ox), localSize.x + localSize.width - 1);\n                const by = Math.min(Math.max(localSize.y, start.y + oy), localSize.y + localSize.height - 1);\n                const bTile = (gameMap as any).tiles.getByMapCoords(bx, by) ?? startTile;\n                game.spawnObject(b, bTile);\n            }\n        }\n        catch (e) {\n            console.warn('[UnitMovementTester] Failed to spawn buildings:', e);\n        }\n        const vxlFactory = new VxlBuilderFactory(new VxlGeometryPool(new VxlGeometryCache(null, null)), false, worldScene.camera);\n        const renderableFactory = new RenderableFactory(new BoxedVar(player) as any, unitSelection as any, alliances as any, rules as any, art as any, mapRenderable as any, imageFinder as any, Engine.getPalettes() as any, Engine.getVoxels() as any, Engine.getVoxelAnims() as any, theater as any, worldScene.camera as any, lighting, lightingDirector as any, new BoxedVar(false) as any, new BoxedVar(false) as any, new BoxedVar(1) as any, null as any, new Strings({ TXT_PRIMARY: \"Primary\" }) as any, new BoxedVar(FlyerHelperMode.Selected) as any, new BoxedVar(false) as any, vxlFactory as any, new Map() as any, false, false);\n        const renderableManager = (this.renderableManager = new RenderableManager(world, worldScene, worldScene.camera as any, renderableFactory));\n        renderableManager.init();\n        this.disposables.add(renderableManager, () => (this.renderableManager = undefined));\n        const renderable = renderableManager.getRenderableByGameObject(unit);\n        renderable.selectionModel.setSelectionLevel(SelectionLevel.Selected);\n        renderable.selectionModel.setControlGroupNumber(1);\n        const vRenderable = vehicle ? renderableManager.getRenderableByGameObject(vehicle) : undefined;\n        const aRenderable = aircraft ? renderableManager.getRenderableByGameObject(aircraft) : undefined;\n        vRenderable?.selectionModel.setSelectionLevel(SelectionLevel.None);\n        vRenderable?.selectionModel.setControlGroupNumber(2);\n        aRenderable?.selectionModel.setSelectionLevel(SelectionLevel.None);\n        aRenderable?.selectionModel.setControlGroupNumber(3);\n        this.buildUnitSwitchUI({\n            onSelectInfantry: () => {\n                this.currentUnit = unit;\n                renderable.selectionModel.setSelectionLevel(SelectionLevel.Selected);\n                vRenderable?.selectionModel.setSelectionLevel(SelectionLevel.None);\n                aRenderable?.selectionModel.setSelectionLevel(SelectionLevel.None);\n            },\n            onSelectVehicle: () => {\n                if (!vehicle)\n                    return;\n                this.currentUnit = vehicle;\n                renderable.selectionModel.setSelectionLevel(SelectionLevel.None);\n                vRenderable?.selectionModel.setSelectionLevel(SelectionLevel.Selected);\n                aRenderable?.selectionModel.setSelectionLevel(SelectionLevel.None);\n            },\n            onSelectAircraft: () => {\n                if (!aircraft)\n                    return;\n                this.currentUnit = aircraft;\n                renderable.selectionModel.setSelectionLevel(SelectionLevel.None);\n                vRenderable?.selectionModel.setSelectionLevel(SelectionLevel.None);\n                aRenderable?.selectionModel.setSelectionLevel(SelectionLevel.Selected);\n            }\n        });\n        const canvasMetrics = (this.canvasMetrics = new CanvasMetrics(renderer.getCanvas(), window));\n        canvasMetrics.init();\n        this.disposables.add(() => canvasMetrics.dispose());\n        let pointerEvents: PointerEvents | undefined;\n        try {\n            const mouseShp = (Engine as any).images?.get?.('mouse.shp');\n            const mousePal = (Engine as any).palettes?.get?.('mousepal.pal');\n            if (mouseShp && mousePal) {\n                const pointer = (this.pointer = Pointer.factory(mouseShp, mousePal, renderer as any, document, canvasMetrics as any, new BoxedVar(false)));\n                pointer.init();\n                try {\n                    await pointer.getPointerLock().exit();\n                }\n                catch { }\n                worldScene.add(pointer.getSprite() as any);\n                pointerEvents = new PointerEvents(renderer as any, { x: 0, y: 0 }, document, canvasMetrics as any);\n            }\n        }\n        catch { }\n        if (!pointerEvents) {\n            pointerEvents = new PointerEvents(renderer as any, { x: 0, y: 0 }, document, canvasMetrics as any);\n        }\n        this.pointerEvents = pointerEvents;\n        const cameraZoomControls = new CameraZoomControls(pointerEvents, worldScene.cameraZoom);\n        cameraZoomControls.init();\n        this.disposables.add(pointerEvents, cameraZoomControls);\n        const canvas = renderer.getCanvas();\n        this.canvas = canvas;\n        const tileHelper = new MapTileIntersectHelper(gameMap as any, worldScene as any);\n        const raycastHelper = new RaycastHelper({ viewport: worldScene.viewport as any, camera: (worldScene as any).camera });\n        const worldViewportHelper = new WorldViewportHelper(worldScene as any);\n        this.entityIntersectHelper = new EntityIntersectHelper(gameMap as any, renderableManager as any, tileHelper as any, raycastHelper as any, worldScene as any, worldViewportHelper as any);\n        const ensureSelectBox = () => {\n            if (!this.selectBoxEl) {\n                const el = document.createElement('div');\n                el.style.position = 'fixed';\n                el.style.border = '1px solid #4aa3ff';\n                el.style.background = 'rgba(74,163,255,0.15)';\n                el.style.pointerEvents = 'none';\n                el.style.display = 'none';\n                el.style.zIndex = '1002';\n                document.body.appendChild(el);\n                this.selectBoxEl = el;\n                this.disposables.add(() => el.remove());\n            }\n        };\n        ensureSelectBox();\n        canvas.addEventListener('contextmenu', (e) => e.preventDefault());\n        const domDown = (e: MouseEvent) => {\n            try {\n                console.info('[UnitMovementTester] DOM mousedown', { button: e.button, x: (e as any).offsetX, y: (e as any).offsetY });\n            }\n            catch { }\n        };\n        const domUp = (e: MouseEvent) => {\n            try {\n                console.info('[UnitMovementTester] DOM mouseup', { button: e.button, x: (e as any).offsetX, y: (e as any).offsetY });\n            }\n            catch { }\n        };\n        canvas.addEventListener('mousedown', domDown, true);\n        canvas.addEventListener('mouseup', domUp, true);\n        this.disposables.add(() => {\n            canvas.removeEventListener('mousedown', domDown, true);\n            canvas.removeEventListener('mouseup', domUp, true);\n        });\n        const onMouseDown = (ev: any) => {\n            try {\n                console.info('[UnitMovementTester] onMouseDown', { btn: ev.button, x: ev.pointer?.x, y: ev.pointer?.y });\n            }\n            catch { }\n            if (ev.button !== 0)\n                return;\n            const x = ev.pointer.x;\n            const y = ev.pointer.y;\n            this.dragStart = { x, y };\n            this.isDragging = false;\n            this.setPointer(PointerType.Select);\n            this.bodyUserSelectPrev = document.body.style.userSelect;\n            this.bodyWebkitUserSelectPrev = (document.body.style as any).webkitUserSelect;\n            this.bodyMozUserSelectPrev = (document.body.style as any).MozUserSelect;\n            this.bodyMsUserSelectPrev = (document.body.style as any).msUserSelect;\n            document.body.style.userSelect = 'none';\n            (document.body.style as any).webkitUserSelect = 'none';\n            (document.body.style as any).MozUserSelect = 'none';\n            (document.body.style as any).msUserSelect = 'none';\n            if (this.pointerEvents)\n                this.pointerEvents.intersectionsEnabled = false;\n        };\n        const onMouseMove = (ev: any) => {\n            if (!this.dragStart)\n                return;\n            const x = ev.pointer.x;\n            const y = ev.pointer.y;\n            const dx = x - this.dragStart.x;\n            const dy = y - this.dragStart.y;\n            const withinClick = Math.abs(dx) <= this.dragThreshold && Math.abs(dy) <= this.dragThreshold;\n            if (!this.isDragging && !withinClick) {\n                this.isDragging = true;\n                if (this.selectBoxEl)\n                    this.selectBoxEl.style.display = 'block';\n                this.setPointer(PointerType.Pan);\n            }\n            if (this.isDragging && this.selectBoxEl) {\n                const left = this.canvasMetrics!.x + Math.min(this.dragStart.x, x);\n                const top = this.canvasMetrics!.y + Math.min(this.dragStart.y, y);\n                const width = Math.abs(dx);\n                const height = Math.abs(dy);\n                this.selectBoxEl.style.left = `${left}px`;\n                this.selectBoxEl.style.top = `${top}px`;\n                this.selectBoxEl.style.width = `${width}px`;\n                this.selectBoxEl.style.height = `${height}px`;\n            }\n            try {\n                console.info('[UnitMovementTester] onMouseMove', { x, y, dx, dy, withinClick, isDragging: this.isDragging });\n            }\n            catch { }\n        };\n        const onHoverMove = (ev: any) => {\n            if (this.isDragging || this.dragStart)\n                return;\n            const x = ev.pointer.x;\n            const y = ev.pointer.y;\n            const hit = this.entityIntersectHelper!.getEntityAtScreenPoint({ x, y }) as any;\n            if (hit?.renderable?.gameObject?.isUnit && hit.renderable.gameObject.isUnit()) {\n                this.setPointer(PointerType.Select);\n            }\n            else {\n                const hasSelection = unitSelection.getSelectedUnits().length > 0;\n                this.setPointer(hasSelection ? PointerType.Move : PointerType.Default);\n            }\n        };\n        const applySelection = (screenBox: any, additive: boolean) => {\n            const THREE_NS: any = (window as any).THREE || (THREE as any);\n            const box = new THREE_NS.Box2(new THREE_NS.Vector2(screenBox.min.x, screenBox.min.y), new THREE_NS.Vector2(screenBox.max.x, screenBox.max.y));\n            const results = this.entityIntersectHelper!.getEntitiesAtScreenBox(box) as any[];\n            try {\n                console.info('[UnitMovementTester] Box select:', {\n                    box: screenBox,\n                    results: results?.length ?? 0\n                });\n            }\n            catch { }\n            const units = results.map(r => r.gameObject).filter(obj => obj.isUnit && obj.isUnit());\n            if (!additive)\n                unitSelection.deselectAll();\n            units.forEach(u => unitSelection.addToSelection(u));\n            const sel = unitSelection.getSelectedUnits();\n            if (sel.length)\n                this.currentUnit = sel[0];\n        };\n        const onMouseUp = (ev: any) => {\n            try {\n                console.info('[UnitMovementTester] onMouseUp', { btn: ev.button, x: ev.pointer?.x, y: ev.pointer?.y });\n            }\n            catch { }\n            const x = ev.pointer.x;\n            const y = ev.pointer.y;\n            const shift = ev.shiftKey || ev.metaKey || ev.ctrlKey;\n            if (ev.button === 0) {\n                const dx = x - (this.dragStart?.x ?? x);\n                const dy = y - (this.dragStart?.y ?? y);\n                const withinClick = Math.abs(dx) <= this.dragThreshold && Math.abs(dy) <= this.dragThreshold;\n                if (this.dragStart && !withinClick) {\n                    if (this.selectBoxEl) {\n                        this.selectBoxEl.style.display = 'none';\n                    }\n                    const minX = Math.min(this.dragStart.x, x);\n                    const minY = Math.min(this.dragStart.y, y);\n                    const maxX = Math.max(this.dragStart.x, x);\n                    const maxY = Math.max(this.dragStart.y, y);\n                    applySelection({ min: { x: minX, y: minY }, max: { x: maxX, y: maxY } }, shift);\n                    this.setPointer(PointerType.Default);\n                }\n                else {\n                    const hit = this.entityIntersectHelper!.getEntityAtScreenPoint({ x, y }) as any;\n                    try {\n                        console.info('[UnitMovementTester] Click at', { x, y, hit: !!hit, unit: !!hit?.renderable?.gameObject?.isUnit && hit.renderable.gameObject.isUnit() });\n                    }\n                    catch { }\n                    if (hit?.renderable?.gameObject?.isUnit && hit.renderable.gameObject.isUnit()) {\n                        unitSelection.deselectAll();\n                        unitSelection.addToSelection(hit.renderable.gameObject);\n                        this.currentUnit = hit.renderable.gameObject;\n                        this.setPointer(PointerType.Default);\n                    }\n                    else {\n                        const tile = tileHelper.getTileAtScreenPoint({ x, y });\n                        try {\n                            console.info('[UnitMovementTester] Ground click tile:', tile ? { rx: tile.rx, ry: tile.ry, z: tile.z } : null);\n                        }\n                        catch { }\n                        if (tile) {\n                            const target = game.createTarget(undefined, tile);\n                            const selected = unitSelection.getSelectedUnits();\n                            try {\n                                console.info('[UnitMovementTester] Move order selected count:', selected?.length ?? 0);\n                            }\n                            catch { }\n                            selected.forEach((u: any) => {\n                                const order = new MoveOrder(game as any, gameMap as any, unitSelection as any, false).set(u, target);\n                                (u as any).unitOrderTrait.addOrder(order as any, false);\n                            });\n                            this.setPointer(PointerType.Move);\n                        }\n                        else {\n                            try {\n                                const alt = (tileHelper as any).intersectTilesByScreenPos({ x, y });\n                                console.warn('[UnitMovementTester] No tile at click. Nearby candidates:', alt?.slice?.(0, 3));\n                            }\n                            catch { }\n                        }\n                    }\n                }\n            }\n            else if (ev.button === 2) {\n                unitSelection.deselectAll();\n                this.setPointer(PointerType.Default);\n            }\n            if (this.bodyUserSelectPrev !== undefined) {\n                document.body.style.userSelect = this.bodyUserSelectPrev;\n                (document.body.style as any).webkitUserSelect = this.bodyWebkitUserSelectPrev ?? '';\n                (document.body.style as any).MozUserSelect = this.bodyMozUserSelectPrev ?? '';\n                (document.body.style as any).msUserSelect = this.bodyMsUserSelectPrev ?? '';\n                this.bodyUserSelectPrev = undefined;\n                this.bodyWebkitUserSelectPrev = undefined;\n                this.bodyMozUserSelectPrev = undefined;\n                this.bodyMsUserSelectPrev = undefined;\n            }\n            this.dragStart = undefined;\n            this.isDragging = false;\n            if (this.pointerEvents)\n                this.pointerEvents.intersectionsEnabled = true;\n        };\n        pointerEvents.addEventListener('canvas', 'mousedown', onMouseDown);\n        pointerEvents.addEventListener('canvas', 'mousemove', onMouseMove);\n        pointerEvents.addEventListener('canvas', 'mousemove', onHoverMove);\n        pointerEvents.addEventListener('canvas', 'mouseup', onMouseUp);\n        this.disposables.add(() => {\n            pointerEvents!.removeEventListener('canvas', 'mousedown', onMouseDown);\n            pointerEvents!.removeEventListener('canvas', 'mousemove', onMouseMove);\n            pointerEvents!.removeEventListener('canvas', 'mousemove', onHoverMove);\n            pointerEvents!.removeEventListener('canvas', 'mouseup', onMouseUp);\n        });\n        const onDocMouseUp = () => {\n            if (this.bodyUserSelectPrev !== undefined) {\n                document.body.style.userSelect = this.bodyUserSelectPrev;\n                (document.body.style as any).webkitUserSelect = this.bodyWebkitUserSelectPrev ?? '';\n                (document.body.style as any).MozUserSelect = this.bodyMozUserSelectPrev ?? '';\n                (document.body.style as any).msUserSelect = this.bodyMsUserSelectPrev ?? '';\n                this.bodyUserSelectPrev = undefined;\n                this.bodyWebkitUserSelectPrev = undefined;\n                this.bodyMozUserSelectPrev = undefined;\n                this.bodyMsUserSelectPrev = undefined;\n            }\n            if (this.isDragging && this.selectBoxEl)\n                this.selectBoxEl.style.display = 'none';\n            this.dragStart = undefined;\n            this.isDragging = false;\n            if (this.pointerEvents)\n                this.pointerEvents.intersectionsEnabled = true;\n        };\n        document.addEventListener('mouseup', onDocMouseUp, false);\n        this.disposables.add(() => document.removeEventListener('mouseup', onDocMouseUp, false));\n        const panKeys = { left: false, right: false, up: false, down: false, shift: false } as any;\n        const setKey = (e: KeyboardEvent, isDown: boolean) => {\n            const key = e.key;\n            const kc = (e as any).keyCode;\n            const before = { ...panKeys };\n            switch (key) {\n                case 'ArrowLeft':\n                    panKeys.left = isDown;\n                    e.preventDefault();\n                    break;\n                case 'ArrowRight':\n                    panKeys.right = isDown;\n                    e.preventDefault();\n                    break;\n                case 'ArrowUp':\n                    panKeys.up = isDown;\n                    e.preventDefault();\n                    break;\n                case 'ArrowDown':\n                    panKeys.down = isDown;\n                    e.preventDefault();\n                    break;\n                case 'Left':\n                    panKeys.left = isDown;\n                    e.preventDefault();\n                    break;\n                case 'Right':\n                    panKeys.right = isDown;\n                    e.preventDefault();\n                    break;\n                case 'Up':\n                    panKeys.up = isDown;\n                    e.preventDefault();\n                    break;\n                case 'Down':\n                    panKeys.down = isDown;\n                    e.preventDefault();\n                    break;\n                case 'Shift':\n                    panKeys.shift = isDown;\n                    break;\n                default:\n                    if (kc === 37) {\n                        panKeys.left = isDown;\n                        e.preventDefault();\n                    }\n                    else if (kc === 39) {\n                        panKeys.right = isDown;\n                        e.preventDefault();\n                    }\n                    else if (kc === 38) {\n                        panKeys.up = isDown;\n                        e.preventDefault();\n                    }\n                    else if (kc === 40) {\n                        panKeys.down = isDown;\n                        e.preventDefault();\n                    }\n                    else if (kc === 16) {\n                        panKeys.shift = isDown;\n                    }\n            }\n            try {\n                console.info('[UnitMovementTester] key', isDown ? 'down' : 'up', { key, keyCode: kc, target: (e.target as any)?.tagName, panKeys, before });\n            }\n            catch { }\n        };\n        const onKeyDown = (e: KeyboardEvent) => setKey(e, true);\n        const onKeyUp = (e: KeyboardEvent) => setKey(e, false);\n        try {\n            this.canvas?.setAttribute('tabindex', '0');\n            this.canvas?.focus({ preventScroll: true } as any);\n            this.canvas?.addEventListener('click', () => {\n                try {\n                    (this.canvas as any)?.focus?.({ preventScroll: true });\n                }\n                catch { }\n            });\n            console.info('[UnitMovementTester] keyboard listeners: focusing canvas', { active: (document.activeElement as any)?.tagName });\n        }\n        catch { }\n        window.addEventListener('keydown', onKeyDown, { passive: false, capture: true });\n        window.addEventListener('keyup', onKeyUp, { passive: false, capture: true });\n        this.canvas?.addEventListener('keydown', onKeyDown as any);\n        this.canvas?.addEventListener('keyup', onKeyUp as any);\n        const prevDocOnKeyDown = (document as any).onkeydown;\n        const prevDocOnKeyUp = (document as any).onkeyup;\n        const prevWinOnKeyDown = (window as any).onkeydown;\n        const prevWinOnKeyUp = (window as any).onkeyup;\n        (document as any).onkeydown = (ev: KeyboardEvent) => { try {\n            onKeyDown(ev);\n        }\n        finally {\n            return false;\n        } };\n        (document as any).onkeyup = (ev: KeyboardEvent) => { try {\n            onKeyUp(ev);\n        }\n        finally {\n            return false;\n        } };\n        ;\n        (window as any).onkeydown = (ev: KeyboardEvent) => { try {\n            onKeyDown(ev);\n        }\n        finally {\n            return false;\n        } };\n        ;\n        (window as any).onkeyup = (ev: KeyboardEvent) => { try {\n            onKeyUp(ev);\n        }\n        finally {\n            return false;\n        } };\n        this.disposables.add(() => {\n            window.removeEventListener('keydown', onKeyDown as any, true as any);\n            window.removeEventListener('keyup', onKeyUp as any, true as any);\n            this.canvas?.removeEventListener('keydown', onKeyDown as any);\n            this.canvas?.removeEventListener('keyup', onKeyUp as any);\n            (document as any).onkeydown = prevDocOnKeyDown;\n            (document as any).onkeyup = prevDocOnKeyUp;\n            (window as any).onkeydown = prevWinOnKeyDown;\n            (window as any).onkeyup = prevWinOnKeyUp;\n        });\n        const panInterval = window.setInterval(() => {\n            const dx = (panKeys.right ? 1 : 0) - (panKeys.left ? 1 : 0);\n            const dy = (panKeys.down ? 1 : 0) - (panKeys.up ? 1 : 0);\n            if (!dx && !dy)\n                return;\n            const step = (panKeys.shift ? 24 : 12);\n            const cur = worldScene.cameraPan.getPan();\n            const next = { x: cur.x + dx * step, y: cur.y + dy * step };\n            try {\n                console.info('[UnitMovementTester] panTick', { dx, dy, step, cur, next });\n            }\n            catch { }\n            worldScene.cameraPan.setPan(next);\n        }, 32);\n        this.disposables.add(() => clearInterval(panInterval));\n        renderer.addScene(worldScene);\n        const uiLoop = (this.uiAnimationLoop = new UiAnimationLoop(renderer));\n        uiLoop.start();\n        this.disposables.add(() => uiLoop.destroy());\n        this.gameTickTimer = window.setInterval(() => {\n            try {\n                game.update();\n            }\n            catch (e) {\n                console.error(\"[UnitMovementTester] game.update failed\", e);\n            }\n        }, 33);\n        this.disposables.add(() => {\n            if (this.gameTickTimer) {\n                clearInterval(this.gameTickTimer);\n                this.gameTickTimer = undefined;\n            }\n        });\n        TestToolSupport.setState('unitmovement', {\n            mapWidth: gameMapFile.fullSize.width,\n            mapHeight: gameMapFile.fullSize.height,\n            hasInfantry: Boolean(unit),\n            hasVehicle: Boolean(vehicle),\n            hasAircraft: Boolean(aircraft),\n        });\n    }\n    private static buildHomeButton(): void {\n        const homeButton = document.createElement('button');\n        homeButton.innerHTML = '点此返回主页';\n        homeButton.style.cssText = `\n      position: fixed;\n      left: 50%;\n      top: 10px;\n      transform: translateX(-50%);\n      padding: 10px 20px;\n      background-color: rgba(0, 0, 0, 0.8);\n      color: white;\n      border: 2px solid rgba(255, 255, 255, 0.3);\n      border-radius: 6px;\n      cursor: pointer;\n      font-size: 16px;\n      font-weight: bold;\n      z-index: 1000;\n      transition: all 0.3s ease;\n      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n    `;\n        homeButton.onmouseover = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';\n            homeButton.style.transform = 'translateX(-50%) translateY(-2px)';\n            homeButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';\n        };\n        homeButton.onmouseout = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';\n            homeButton.style.transform = 'translateX(-50%) translateY(0)';\n            homeButton.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';\n        };\n        homeButton.onclick = () => {\n            window.location.hash = '/';\n        };\n        document.body.appendChild(homeButton);\n        this.disposables.add(() => homeButton.remove());\n    }\n    private static buildUnitSwitchUI(handlers: {\n        onSelectInfantry: () => void;\n        onSelectVehicle: () => void;\n        onSelectAircraft: () => void;\n    }): void {\n        if (this.unitSwitchEl) {\n            this.unitSwitchEl.remove();\n        }\n        const box = (this.unitSwitchEl = document.createElement('div'));\n        box.style.cssText = `\n      position: fixed;\n      left: 10px;\n      top: 10px;\n      z-index: 1001;\n      background: rgba(255,255,255,0.8);\n      border: 1px solid #999;\n      padding: 8px;\n      border-radius: 4px;\n      display: flex;\n      gap: 6px;\n    `;\n        const btnInf = document.createElement('button');\n        btnInf.innerText = '步兵';\n        btnInf.onclick = handlers.onSelectInfantry;\n        const btnVeh = document.createElement('button');\n        btnVeh.innerText = '坦克';\n        btnVeh.onclick = handlers.onSelectVehicle;\n        const btnAir = document.createElement('button');\n        btnAir.innerText = '飞机';\n        btnAir.onclick = handlers.onSelectAircraft;\n        box.appendChild(btnInf);\n        box.appendChild(btnVeh);\n        box.appendChild(btnAir);\n        document.body.appendChild(box);\n        this.disposables.add(() => box.remove());\n    }\n    static destroy(): void {\n        TestToolSupport.clearState('unitmovement');\n        if (this.uiAnimationLoop) {\n            this.uiAnimationLoop.destroy();\n            this.uiAnimationLoop = undefined;\n        }\n        if (this.renderer) {\n            this.renderer.dispose();\n            this.renderer = undefined;\n        }\n        this.disposables.dispose();\n    }\n    private static setPointer(type: PointerType): void {\n        if (this.pointer) {\n            this.pointer.setPointerType(type);\n            return;\n        }\n        const canvas = this.canvas as HTMLCanvasElement | undefined;\n        if (!canvas)\n            return;\n        switch (type) {\n            case PointerType.Select:\n                canvas.style.cursor = 'pointer';\n                break;\n            case PointerType.Move:\n                canvas.style.cursor = 'crosshair';\n                break;\n            case PointerType.Pan:\n                canvas.style.cursor = 'grabbing';\n                break;\n            default:\n                canvas.style.cursor = 'default';\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "src/tools/VehicleTester.ts",
    "content": "import { Renderer } from \"@/engine/gfx/Renderer\";\nimport { Engine } from \"@/engine/Engine\";\nimport { IsoCoords } from \"@/engine/IsoCoords\";\nimport { Player } from \"@/game/Player\";\nimport { WorldScene } from \"@/engine/renderable/WorldScene\";\nimport { Rules } from \"@/game/rules/Rules\";\nimport { MapGrid } from \"@/engine/renderable/entity/map/MapGrid\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\nimport { UiAnimationLoop } from \"@/engine/UiAnimationLoop\";\nimport { ImageFinder } from \"@/engine/ImageFinder\";\nimport { Art } from \"@/game/art/Art\";\nimport { RenderableFactory } from \"@/engine/renderable/entity/RenderableFactory\";\nimport { TheaterType } from \"@/engine/TheaterType\";\nimport { Alliances } from \"@/game/Alliances\";\nimport { PlayerList } from \"@/game/PlayerList\";\nimport { SelectionLevel } from \"@/game/gameobject/selection/SelectionLevel\";\nimport { VeteranLevel } from \"@/game/gameobject/unit/VeteranLevel\";\nimport { PointerEvents } from \"@/gui/PointerEvents\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { UnitSelection } from \"@/game/gameobject/selection/UnitSelection\";\nimport { CameraZoomControls } from \"@/tools/CameraZoomControls\";\nimport { Lighting } from \"@/engine/Lighting\";\nimport { ObjectFactory } from \"@/game/gameobject/ObjectFactory\";\nimport { TileCollection } from \"@/game/map/TileCollection\";\nimport { ObjectType } from \"@/engine/type/ObjectType\";\nimport { MoveState } from \"@/game/gameobject/trait/MoveTrait\";\nimport { TileOccupation } from \"@/game/map/TileOccupation\";\nimport { Bridges } from \"@/game/map/Bridges\";\nimport { RenderableManager } from \"@/engine/RenderableManager\";\nimport { World } from \"@/game/World\";\nimport { Strings } from \"@/data/Strings\";\nimport { MapBounds } from \"@/game/map/MapBounds\";\nimport { FlyerHelperMode } from \"@/engine/renderable/entity/unit/FlyerHelperMode\";\nimport { VxlBuilderFactory } from \"@/engine/renderable/builder/VxlBuilderFactory\";\nimport { VxlGeometryPool } from \"@/engine/renderable/builder/vxlGeometry/VxlGeometryPool\";\nimport { VxlGeometryCache } from \"@/engine/gfx/geometry/VxlGeometryCache\";\nimport { ShadowQuality } from \"@/engine/renderable/entity/unit/ShadowQuality\";\nimport { CanvasMetrics } from \"@/gui/CanvasMetrics\";\nimport { PipOverlay } from \"@/engine/renderable/entity/PipOverlay\";\nimport { TextureUtils } from \"@/engine/gfx/TextureUtils\";\nimport { ZoneType } from \"@/game/gameobject/unit/ZoneType\";\nimport { LightingDirector } from \"@/engine/gfx/lighting/LightingDirector\";\nimport { rampHeights } from \"@/game/theater/rampHeights\";\nimport { ResourceType } from \"@/engine/resourceConfigs\";\nimport { TestToolSupport, type TestToolRuntimeContext } from \"@/tools/TestToolSupport\";\ndeclare const THREE: any;\nexport class VehicleTester {\n    private static disposables = new CompositeDisposable();\n    private static renderer: Renderer;\n    private static theater: any;\n    private static rules: Rules;\n    private static art: Art;\n    private static images: any;\n    private static voxels: any;\n    private static voxelAnims: any;\n    private static uiAnimationLoop: UiAnimationLoop;\n    private static worldScene: WorldScene;\n    private static world: World;\n    private static currentRenderable: any;\n    private static currentVehicle: any;\n    private static listEl: HTMLDivElement;\n    private static controlsEl: HTMLDivElement | undefined;\n    private static hostElement?: HTMLElement;\n    private static vxlGeometryPool: VxlGeometryPool;\n    private static fixedDirection: number | undefined;\n    private static animateTimer: number | undefined;\n    private static currentVehicleType?: string;\n    static async main(_args: any, context: TestToolRuntimeContext = {}): Promise<void> {\n        await TestToolSupport.ensureTheater(TheaterType.Temperate, context.cdnResourceLoader, [ResourceType.Vxl, ResourceType.Anims]);\n        const hostElement = this.hostElement = TestToolSupport.prepareHost(context, 1224, 600);\n        const renderer = (this.renderer = new Renderer(800, 600));\n        renderer.init(hostElement);\n        TestToolSupport.placeRendererCanvas(renderer, 212, 0);\n        renderer.initStats(document.body);\n        this.buildHomeButton();\n        const worldScene = WorldScene.factory({ x: 0, y: 0, width: 800, height: 600 }, new BoxedVar(true), new BoxedVar(ShadowQuality.High));\n        this.disposables.add(worldScene);\n        (worldScene.scene.background as any) = new THREE.Color(12632256);\n        IsoCoords.init({ x: 0, y: 0 });\n        this.theater = await Engine.loadTheater(TheaterType.Temperate);\n        const rules = new Rules(Engine.getRules());\n        this.rules = rules;\n        this.art = new Art(rules as any, Engine.getArt(), undefined as any, console);\n        this.images = Engine.getImages();\n        this.voxels = Engine.getVoxels();\n        this.voxelAnims = Engine.getVoxelAnims();\n        this.buildBrowser(rules[\"vehicleRules\"] as Map<string, any>);\n        const canvasMetrics = new CanvasMetrics(renderer.getCanvas(), window);\n        canvasMetrics.init();\n        this.disposables.add(() => canvasMetrics.dispose());\n        const pointerEvents = new PointerEvents(renderer as any, { x: 0, y: 0 }, document, canvasMetrics as any);\n        const cameraZoomControls = new CameraZoomControls(pointerEvents, worldScene.cameraZoom);\n        cameraZoomControls.init();\n        this.disposables.add(pointerEvents, cameraZoomControls);\n        renderer.addScene(worldScene);\n        const uiAnimationLoop = (this.uiAnimationLoop = new UiAnimationLoop(renderer));\n        uiAnimationLoop.start();\n        this.worldScene = worldScene;\n        this.vxlGeometryPool = new VxlGeometryPool(new VxlGeometryCache(null, null));\n        this.addGrid();\n        this.createFloor();\n        this.syncState();\n    }\n    static addGrid(): void {\n        const mapGrid = new MapGrid({ width: 10, height: 10 });\n        const gridObject = mapGrid.get3DObject();\n        const container = new THREE.Object3D();\n        container.add(gridObject);\n        this.worldScene.scene.add(container);\n    }\n    static createFloor(): void {\n        const geometry = new THREE.PlaneGeometry(10000, 10000);\n        const material = new THREE.ShadowMaterial();\n        material.opacity = 0.5;\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.rotation.x = -Math.PI / 2;\n        mesh.receiveShadow = true;\n        mesh.renderOrder = 200000;\n        mesh.position.y = 1;\n        this.worldScene.scene.add(mesh);\n    }\n    static selectVehicle(vehicleType: string): void {\n        if (this.currentVehicle && !this.currentVehicle.isDisposed) {\n            this.world.removeObject(this.currentVehicle);\n            this.currentVehicle.dispose();\n        }\n        const player = new Player(\"Player\");\n        this.disposables.add(player);\n        const desiredColor = this.rules.getMultiplayerColors().get(\"DarkRed\")!;\n        (player as any).color = desiredColor;\n        const playerList = new PlayerList();\n        playerList.addPlayer(player);\n        const alliances = new Alliances(playerList);\n        const unitSelection = new UnitSelection();\n        const lighting = new Lighting();\n        this.disposables.add(lighting);\n        const renderableFactory = new RenderableFactory(new BoxedVar(player) as any, unitSelection as any, alliances as any, this.rules as any, this.art as any, undefined as any, new ImageFinder(this.images, this.theater) as any, Engine.getPalettes() as any, this.voxels as any, this.voxelAnims as any, this.theater as any, this.worldScene.camera as any, new Lighting(), new LightingDirector(new Lighting().mapLighting, this.renderer as any, new BoxedVar(1) as any) as any, new BoxedVar(false) as any, new BoxedVar(false) as any, new BoxedVar(2) as any, undefined as any, new Strings() as any, new BoxedVar(FlyerHelperMode.Selected) as any, new BoxedVar(false) as any, new VxlBuilderFactory(this.vxlGeometryPool, false, this.worldScene.camera) as any, new Map() as any);\n        const tileCollection = new TileCollection([\n            { rx: 0, ry: 0, dx: 0, dy: 0, z: 0, tileNum: 0, subTile: 0 },\n            { rx: 1, ry: 0, dx: 1, dy: 0, z: 0, tileNum: 0, subTile: 0 },\n            { rx: 0, ry: 1, dx: 0, dy: 1, z: 0, tileNum: 0, subTile: 0 },\n            { rx: 1, ry: 1, dx: 1, dy: 1, z: 0, tileNum: 0, subTile: 0 },\n        ] as any, this.theater.tileSets as any, this.rules.general as any, () => 0);\n        const tileOccupation = new TileOccupation(tileCollection);\n        const mapBounds = new MapBounds();\n        const bridges = new Bridges(this.theater.tileSets, tileCollection, tileOccupation, mapBounds, this.rules);\n        const vehicle = (this.currentVehicle = new ObjectFactory(tileCollection, tileOccupation, bridges, new BoxedVar(1)).create(ObjectType.Vehicle, vehicleType, this.rules as any, this.art as any));\n        this.currentVehicleType = vehicleType;\n        vehicle.owner = player;\n        vehicle.position.tile = this.tile;\n        const world = (this.world = new World());\n        const renderableManager = new RenderableManager(world, this.worldScene, this.worldScene.camera, renderableFactory);\n        renderableManager.init();\n        this.disposables.add(renderableManager);\n        world.spawnObject(vehicle);\n        const renderable = (this.currentRenderable = renderableManager.getRenderableByGameObject(vehicle));\n        renderable.selectionModel.setSelectionLevel(SelectionLevel.Selected);\n        renderable.selectionModel.setControlGroupNumber(3);\n        this.buildControls();\n        this.startAutoAnimate();\n        this.syncState();\n    }\n    private static startAutoAnimate(): void {\n        if (this.animateTimer) {\n            clearTimeout(this.animateTimer);\n        }\n        const step = () => {\n            if (!this.currentVehicle)\n                return;\n            this.currentVehicle.direction = this.fixedDirection ?? ((this.currentVehicle.direction + 1) % 360);\n            if (this.currentVehicle.turretTrait) {\n                this.currentVehicle.turretTrait.facing = this.fixedDirection ?? ((this.currentVehicle.turretTrait.facing + 2) % 360);\n            }\n            this.animateTimer = window.setTimeout(step, 50);\n        };\n        this.animateTimer = window.setTimeout(step, 50);\n    }\n    static buildControls(): void {\n        if (this.controlsEl) {\n            this.controlsEl.remove();\n        }\n        const controls = (this.controlsEl = document.createElement(\"div\"));\n        controls.dataset.testid = \"vehicle-controls\";\n        controls.style.position = \"absolute\";\n        controls.style.left = \"0\";\n        controls.style.top = \"0\";\n        controls.style.width = \"200px\";\n        controls.style.padding = \"5px\";\n        controls.style.background = \"rgba(255, 255, 255, 0.5)\";\n        controls.style.border = \"1px black solid\";\n        controls.appendChild(document.createTextNode(\"Remap color:\"));\n        const colorMap = new Map(this.rules.getMultiplayerColors());\n        const colorSelect = document.createElement(\"select\");\n        colorSelect.dataset.testid = \"vehicle-color\";\n        colorSelect.style.display = \"block\";\n        colorSelect.addEventListener(\"change\", () => {\n            this.currentVehicle.owner.color = colorMap.get(colorSelect.value);\n            this.syncState();\n        });\n        controls.appendChild(colorSelect);\n        colorMap.forEach((color, name) => {\n            const option = document.createElement(\"option\");\n            option.innerHTML = name;\n            option.value = name;\n            option.selected = color.asHex() === this.currentVehicle.owner.color.asHex();\n            colorSelect.appendChild(option);\n        });\n        controls.appendChild(document.createTextNode(\"Selection level:\"));\n        const selDiv = document.createElement(\"div\");\n        controls.appendChild(selDiv);\n        [SelectionLevel.None, SelectionLevel.Hover, SelectionLevel.Selected].forEach((level) => {\n            const btn = document.createElement(\"button\");\n            btn.innerHTML = SelectionLevel[level];\n            btn.dataset.testid = `vehicle-selection-${SelectionLevel[level].toLowerCase()}`;\n            btn.addEventListener(\"click\", () => {\n                this.currentRenderable.selectionModel.setSelectionLevel(level);\n                this.syncState();\n            });\n            selDiv.appendChild(btn);\n        });\n        controls.appendChild(document.createTextNode(\"Veteran level:\"));\n        const vetDiv = document.createElement(\"div\");\n        controls.appendChild(vetDiv);\n        if (this.currentVehicle.veteranTrait) {\n            [VeteranLevel.None, VeteranLevel.Veteran, VeteranLevel.Elite].forEach((lvl) => {\n                const btn = document.createElement(\"button\");\n                btn.innerHTML = VeteranLevel[lvl];\n                btn.dataset.testid = `vehicle-veteran-${VeteranLevel[lvl].toLowerCase()}`;\n                btn.addEventListener(\"click\", () => {\n                    this.currentVehicle.veteranTrait.veteranLevel = lvl;\n                    this.syncState();\n                });\n                vetDiv.appendChild(btn);\n            });\n        }\n        controls.appendChild(document.createTextNode(\"Ramp type:\"));\n        const rampSelect = document.createElement(\"select\");\n        rampSelect.dataset.testid = \"vehicle-ramp\";\n        rampSelect.style.display = \"block\";\n        rampSelect.addEventListener(\"change\", () => {\n            this.tile.rampType = Number(rampSelect.value);\n            this.currentVehicle.tilterTrait?.onTileChange?.(this.currentVehicle);\n            this.syncState();\n        });\n        for (let i = 0; i < rampHeights.length; i++) {\n            const opt = document.createElement(\"option\");\n            opt.innerHTML = String(i);\n            opt.value = String(i);\n            rampSelect.appendChild(opt);\n        }\n        controls.appendChild(rampSelect);\n        controls.appendChild(document.createTextNode(\"Turret #:\"));\n        const turretSelect = document.createElement(\"select\");\n        turretSelect.dataset.testid = \"vehicle-turret\";\n        turretSelect.style.display = \"block\";\n        turretSelect.disabled = !this.currentVehicle.rules.turret;\n        turretSelect.addEventListener(\"change\", () => {\n            this.currentVehicle.turretNo = Number(turretSelect.value);\n            this.syncState();\n        });\n        for (let t = 0; t < (this.currentVehicle.rules.turretCount || 0); t++) {\n            const opt = document.createElement(\"option\");\n            opt.innerHTML = String(t);\n            opt.value = String(t);\n            turretSelect.appendChild(opt);\n        }\n        controls.appendChild(turretSelect);\n        controls.appendChild(document.createTextNode(\"isMoving:\"));\n        const moving = document.createElement(\"input\");\n        moving.dataset.testid = \"vehicle-moving\";\n        moving.type = \"checkbox\";\n        moving.style.display = \"block\";\n        moving.addEventListener(\"change\", (e) => {\n            const checked = (e.target as HTMLInputElement).checked;\n            this.currentVehicle.moveTrait.moveState = checked ? MoveState.Moving : MoveState.Idle;\n            if (this.currentVehicle.rules.consideredAircraft && checked) {\n                this.currentVehicle.zone = ZoneType.Air;\n            }\n            else {\n                this.currentVehicle.zone = this.currentVehicle.rules.naval ? ZoneType.Water : ZoneType.Ground;\n            }\n            this.syncState();\n        });\n        controls.appendChild(moving);\n        controls.appendChild(document.createTextNode(\"isFiring:\"));\n        const firing = document.createElement(\"input\");\n        firing.dataset.testid = \"vehicle-firing\";\n        firing.type = \"checkbox\";\n        firing.style.display = \"block\";\n        firing.addEventListener(\"change\", (e) => {\n            this.currentVehicle.isFiring = (e.target as HTMLInputElement).checked;\n            this.syncState();\n        });\n        controls.appendChild(firing);\n        controls.appendChild(document.createTextNode(\"isRocking:\"));\n        const rocking = document.createElement(\"input\");\n        rocking.dataset.testid = \"vehicle-rocking\";\n        rocking.type = \"checkbox\";\n        rocking.style.display = \"block\";\n        rocking.addEventListener(\"change\", (e) => {\n            const checked = (e.target as HTMLInputElement).checked;\n            if (checked)\n                this.currentVehicle.applyRocking(360 * Math.random(), 1);\n            else\n                this.currentVehicle.rocking = undefined;\n            this.syncState();\n        });\n        controls.appendChild(rocking);\n        if (this.currentVehicle.airSpawnTrait) {\n            controls.appendChild(document.createTextNode(\"hasSpawns:\"));\n            const hasSpawns = document.createElement(\"input\");\n            hasSpawns.dataset.testid = \"vehicle-has-spawns\";\n            hasSpawns.type = \"checkbox\";\n            hasSpawns.style.display = \"block\";\n            hasSpawns.checked = !!this.currentVehicle.airSpawnTrait.availableSpawns;\n            hasSpawns.addEventListener(\"change\", (e) => {\n                const v = (e.target as HTMLInputElement).checked ? 1 : 0;\n                this.currentVehicle.airSpawnTrait.debugSetStorage(null, v);\n                this.syncState();\n            });\n            controls.appendChild(hasSpawns);\n        }\n        controls.appendChild(document.createTextNode(\"Warped out:\"));\n        const warped = document.createElement(\"input\");\n        warped.dataset.testid = \"vehicle-warped\";\n        warped.type = \"checkbox\";\n        warped.style.display = \"block\";\n        warped.addEventListener(\"change\", (e) => {\n            this.currentVehicle.warpedOutTrait?.debugSetActive((e.target as HTMLInputElement).checked);\n            this.syncState();\n        });\n        controls.appendChild(warped);\n        controls.appendChild(document.createTextNode(\"Direction:\"));\n        const dirWrap = document.createElement(\"div\");\n        controls.appendChild(dirWrap);\n        const dir = document.createElement(\"input\");\n        dir.dataset.testid = \"vehicle-direction\";\n        dir.type = \"range\";\n        dir.min = \"-180\";\n        dir.max = \"180\";\n        dir.value = \"0\";\n        dir.disabled = this.fixedDirection === undefined ? true : false;\n        dir.style.verticalAlign = \"middle\";\n        dir.addEventListener(\"input\", () => {\n            this.fixedDirection = Number(dir.value);\n            this.syncState();\n        });\n        dirWrap.appendChild(dir);\n        const reset = document.createElement(\"button\");\n        reset.dataset.testid = \"vehicle-direction-reset\";\n        reset.innerHTML = \"Reset\";\n        reset.disabled = this.fixedDirection === undefined;\n        reset.style.verticalAlign = \"middle\";\n        reset.addEventListener(\"click\", () => {\n            if (this.fixedDirection !== undefined) {\n                this.fixedDirection = 0;\n                dir.value = \"0\";\n                this.syncState();\n            }\n        });\n        dirWrap.appendChild(reset);\n        const autoRotate = document.createElement(\"input\");\n        autoRotate.dataset.testid = \"vehicle-auto-rotate\";\n        autoRotate.type = \"checkbox\";\n        autoRotate.checked = this.fixedDirection === undefined;\n        autoRotate.addEventListener(\"change\", (e) => {\n            this.fixedDirection = (e.target as HTMLInputElement).checked ? undefined : 0;\n            const disabled = this.fixedDirection === undefined;\n            dir.disabled = disabled;\n            reset.disabled = disabled;\n            dir.value = \"0\";\n            this.syncState();\n        });\n        controls.appendChild(autoRotate);\n        const autoLabel = document.createElement(\"label\");\n        autoLabel.innerHTML = \"Auto rotate\";\n        controls.appendChild(autoLabel);\n        const destroy = document.createElement(\"button\");\n        destroy.dataset.testid = \"vehicle-destroy\";\n        destroy.style.display = \"block\";\n        destroy.style.color = \"red\";\n        destroy.innerHTML = \"DESTROY\";\n        destroy.addEventListener(\"click\", async () => {\n            this.currentVehicle.isDestroyed = true;\n            this.world.removeObject(this.currentVehicle);\n            this.currentVehicle.dispose();\n            this.currentVehicle = undefined;\n            this.currentVehicleType = undefined;\n            this.controlsEl?.remove();\n            this.controlsEl = undefined;\n            this.syncState();\n        });\n        controls.appendChild(destroy);\n        this.hostElement?.appendChild(controls);\n        TestToolSupport.applyPanelTheme(controls);\n        this.syncState();\n    }\n    private static syncState(): void {\n        const vehicle = this.currentVehicle;\n        const renderable = this.currentRenderable;\n        const selectionLevel = renderable?.selectionModel?.getSelectionLevel?.();\n        const veteranLevel = vehicle?.veteranTrait?.veteranLevel ?? vehicle?.veteranLevel ?? VeteranLevel.None;\n        const controlGroupTextureUuid = renderable?.pipOverlay?.controlGroupSprite?.material?.map?.uuid\n            ?? renderable?.pipOverlay?.controlGroupSprite?.material?.uniforms?.map?.value?.uuid\n            ?? null;\n        const controlGroupColorHex = renderable?.pipOverlay?.lastOwnerColorHex !== undefined\n            ? `#${Number(renderable.pipOverlay.lastOwnerColorHex).toString(16).padStart(6, \"0\")}`\n            : null;\n        const vxlExtraLightScalar = renderable?.vxlExtraLight?.x ?? null;\n        TestToolSupport.setState('vehicle', {\n            availableVehicles: this.listEl?.querySelectorAll('a').length ?? 0,\n            selectedVehicle: this.currentVehicleType ?? null,\n            rendered: Boolean(renderable?.get3DObject?.() ?? renderable),\n            selectionLevelValue: selectionLevel ?? null,\n            selectionLevel: TestToolSupport.enumLabel(SelectionLevel, selectionLevel),\n            selectionLevelOptions: TestToolSupport.enumOptions(SelectionLevel, [SelectionLevel.None, SelectionLevel.Hover, SelectionLevel.Selected]),\n            veteranLevelValue: vehicle ? veteranLevel : null,\n            veteranLevel: vehicle ? TestToolSupport.enumLabel(VeteranLevel, veteranLevel) : null,\n            veteranLevelOptions: TestToolSupport.enumOptions(VeteranLevel, [VeteranLevel.None, VeteranLevel.Veteran, VeteranLevel.Elite]),\n            rampType: vehicle ? this.tile.rampType : null,\n            rampOptions: rampHeights.map((_, index) => String(index)),\n            turretNo: vehicle ? this.currentVehicle.turretNo ?? 0 : null,\n            turretOptions: vehicle ? Array.from({ length: this.currentVehicle.rules.turretCount || 0 }, (_, index) => String(index)) : [],\n            hasTurret: Boolean(vehicle?.turretTrait),\n            moveState: vehicle?.moveTrait?.moveState ?? null,\n            isMoving: vehicle?.moveTrait?.moveState === MoveState.Moving,\n            zoneValue: vehicle?.zone ?? null,\n            zone: TestToolSupport.enumLabel(ZoneType, vehicle?.zone),\n            isFiring: Boolean(vehicle?.isFiring),\n            isRocking: Boolean(vehicle?.rocking),\n            hasSpawns: Boolean(vehicle?.airSpawnTrait),\n            availableSpawns: vehicle?.airSpawnTrait?.availableSpawns ?? null,\n            warpedOut: Boolean(vehicle?.warpedOutTrait?.isActive?.()),\n            direction: vehicle?.direction ?? null,\n            fixedDirection: this.fixedDirection ?? null,\n            autoRotate: this.fixedDirection === undefined,\n            ownerColor: vehicle?.owner?.color?.asHexString?.() ?? null,\n            controlGroupTextureUuid,\n            controlGroupColorHex,\n            vxlExtraLightScalar,\n        });\n    }\n    static buildBrowser(vehicleRules: Map<string, any>): void {\n        const list = (this.listEl = document.createElement(\"div\"));\n        list.style.position = \"absolute\";\n        list.style.right = \"0\";\n        list.style.top = \"0\";\n        list.style.height = \"600px\";\n        list.style.width = \"200px\";\n        list.style.overflowY = \"auto\";\n        list.style.padding = \"5px\";\n        list.style.background = \"rgba(255, 255, 255, 0.5)\";\n        list.style.border = \"1px black solid\";\n        list.appendChild(document.createTextNode(\"Vehicle types:\"));\n        const types = [...vehicleRules.keys()]\n            .filter((name) => this.art.hasObject(name, ObjectType.Vehicle))\n            .sort();\n        types.forEach((name) => {\n            const link = document.createElement(\"a\");\n            link.dataset.vehicleType = name;\n            link.style.display = \"block\";\n            link.textContent = name;\n            link.setAttribute(\"href\", \"javascript:;\");\n            link.addEventListener(\"click\", () => {\n                console.log(\"Selected vehicle\", name);\n                this.selectVehicle(name);\n            });\n            list.appendChild(link);\n        });\n        this.hostElement?.appendChild(list);\n        TestToolSupport.applyPanelTheme(list);\n        this.syncState();\n        setTimeout(() => {\n            if (types.length) {\n                this.selectVehicle(types[0]);\n            }\n        }, 50);\n    }\n    private static buildHomeButton(): void {\n        const homeButton = document.createElement('button');\n        homeButton.innerHTML = '点此返回主页';\n        homeButton.style.cssText = `\n      position: fixed;\n      left: 50%;\n      top: 10px;\n      transform: translateX(-50%);\n      padding: 10px 20px;\n      background-color: rgba(0, 0, 0, 0.8);\n      color: white;\n      border: 2px solid rgba(255, 255, 255, 0.3);\n      border-radius: 6px;\n      cursor: pointer;\n      font-size: 16px;\n      font-weight: bold;\n      z-index: 1000;\n      transition: all 0.3s ease;\n      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n    `;\n        homeButton.onmouseover = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';\n            homeButton.style.transform = 'translateX(-50%) translateY(-2px)';\n            homeButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';\n        };\n        homeButton.onmouseout = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';\n            homeButton.style.transform = 'translateX(-50%) translateY(0)';\n            homeButton.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';\n        };\n        homeButton.onclick = () => {\n            window.location.hash = '/';\n        };\n        document.body.appendChild(homeButton);\n        this.disposables.add(() => homeButton.remove());\n    }\n    static destroy(): void {\n        this.renderer?.dispose?.();\n        this.uiAnimationLoop?.destroy?.();\n        this.listEl?.remove?.();\n        if (this.controlsEl) {\n            this.controlsEl.remove();\n            this.controlsEl = undefined;\n        }\n        if (this.animateTimer) {\n            clearTimeout(this.animateTimer);\n            this.animateTimer = undefined;\n        }\n        this.currentVehicleType = undefined;\n        this.disposables.dispose();\n        TestToolSupport.clearState('vehicle');\n        try {\n            if ((PipOverlay as any)?.clearCaches) {\n                PipOverlay.clearCaches();\n            }\n            if ((TextureUtils as any)?.cache) {\n                TextureUtils.cache.forEach((tex: any) => tex.dispose?.());\n                TextureUtils.cache.clear();\n            }\n        }\n        catch (err) {\n            console.warn('[VehicleTester] Failed to clear caches during destroy:', err);\n        }\n    }\n    private static tile: {\n        rx: number;\n        ry: number;\n        z: number;\n        rampType: number;\n    } = { rx: 1, ry: 1, z: 0, rampType: 0 };\n}\n"
  },
  {
    "path": "src/tools/VxlTester.ts",
    "content": "import { Palette } from '../data/Palette';\nimport { VxlFile } from '../data/VxlFile';\nimport { Renderer } from '../engine/gfx/Renderer';\nimport { WorldScene } from '../engine/renderable/WorldScene';\nimport { VxlNonBatchedBuilder } from '../engine/renderable/builder/VxlNonBatchedBuilder';\nimport { UiAnimationLoop } from '../engine/UiAnimationLoop';\nimport { PointerEvents } from '../gui/PointerEvents';\nimport { CompositeDisposable } from '../util/disposable/CompositeDisposable';\nimport { CameraZoomControls } from './CameraZoomControls';\nimport { BoxedVar } from '../util/BoxedVar';\nimport { VxlGeometryPool } from '../engine/renderable/builder/vxlGeometry/VxlGeometryPool';\nimport { VxlGeometryCache } from '../engine/gfx/geometry/VxlGeometryCache';\nimport { ShadowQuality } from '../engine/renderable/entity/unit/ShadowQuality';\nimport { CanvasMetrics } from '../gui/CanvasMetrics';\nimport { VirtualFileSystem } from '../data/vfs/VirtualFileSystem';\nimport { Renderable } from '../engine/gfx/RenderableContainer';\nimport { ResourceType } from '../engine/resourceConfigs';\nimport { PipOverlay } from '../engine/renderable/entity/PipOverlay';\nimport { TextureUtils } from '../engine/gfx/TextureUtils';\nimport { TestToolSupport, type TestToolRuntimeContext } from './TestToolSupport';\nimport * as THREE from 'three';\nconst VXL_FILES = [\n    \"1tnk.vxl\", \"1tnkbarl.vxl\", \"1tnktur.vxl\", \"2tnk.vxl\", \"2tnkbarl.vxl\",\n    \"2tnktur.vxl\", \"3tnk.vxl\", \"3tnkbarl.vxl\", \"3tnktur.vxl\", \"4tnk.vxl\",\n    \"4tnkbarl.vxl\", \"4tnktur.vxl\", \"aegis.vxl\", \"apache.vxl\", \"apc.vxl\",\n    \"apcw.vxl\", \"art2.vxl\", \"art2barl.vxl\", \"art2tur.vxl\", \"arty.vxl\",\n    \"artybarl.vxl\", \"asw.vxl\", \"axle.vxl\", \"bana.vxl\", \"beag.vxl\",\n    \"bggy.vxl\", \"bike.vxl\", \"bus.vxl\", \"car.vxl\", \"cargocar.vxl\",\n    \"carrier.vxl\", \"cdest.vxl\", \"cdestwo.vxl\", \"cmin.vxl\", \"cmon.vxl\",\n    \"cona.vxl\", \"cop.vxl\", \"cplane.vxl\", \"cruise.vxl\", \"dest.vxl\",\n    \"destwo.vxl\", \"dmisl.vxl\", \"dpod.vxl\", \"dred.vxl\", \"dredwo.vxl\",\n    \"dshp.vxl\", \"euroc.vxl\", \"falc.vxl\", \"flak.vxl\", \"flaktur.vxl\",\n    \"flata.vxl\", \"fortress.vxl\", \"ftnk.vxl\", \"fv.vxl\", \"fvtur.vxl\",\n    \"fvtur1.vxl\", \"fvtur10.vxl\", \"fvtur11.vxl\", \"fvtur12.vxl\", \"fvtur13.vxl\",\n    \"fvtur14.vxl\", \"fvtur2.vxl\", \"fvtur3.vxl\", \"fvtur4.vxl\", \"fvtur5.vxl\",\n    \"fvtur6.vxl\", \"fvtur7.vxl\", \"fvtur8.vxl\", \"fvtur9.vxl\", \"gastank.vxl\",\n    \"gtgcanbarl.vxl\", \"gtgcantur.vxl\", \"gtnk.vxl\", \"gtnkbarl.vxl\", \"gtnktur.vxl\",\n    \"harv.vxl\", \"harvtur.vxl\", \"heli.vxl\", \"hind.vxl\", \"hmec.vxl\",\n    \"hornet.vxl\", \"horv.vxl\", \"htk.vxl\", \"htkbarl.vxl\", \"htktur.vxl\",\n    \"htnk.vxl\", \"htnkbarl.vxl\", \"htnktur.vxl\", \"hvr.vxl\", \"hvrtur.vxl\",\n    \"hwtz.vxl\", \"hyd.vxl\", \"icbm.vxl\", \"jeep.vxl\", \"jeeptur.vxl\",\n    \"laser.vxl\", \"lcrf.vxl\", \"limo.vxl\", \"lpst.vxl\", \"ltnk.vxl\",\n    \"ltnkbarl.vxl\", \"ltnktur.vxl\", \"m113.vxl\", \"m113tur.vxl\", \"mcv.vxl\",\n    \"misl.vxl\", \"mislchem.vxl\", \"mislmlti.vxl\", \"mislorca.vxl\", \"mislsam.vxl\",\n    \"mlrs.vxl\", \"mlrstur.vxl\", \"mmchbarl.vxl\", \"mnly.vxl\", \"monocar.vxl\",\n    \"monoeng.vxl\", \"mrj.vxl\", \"mrjtur.vxl\", \"mtnk.vxl\", \"mtnkbarl.vxl\",\n    \"mtnktur.vxl\", \"mtrb.vxl\", \"mtrs.vxl\", \"mtrt.vxl\", \"orca.vxl\",\n    \"orcab.vxl\", \"orcatran.vxl\", \"outp.vxl\", \"pdplane.vxl\", \"phal.vxl\",\n    \"pick.vxl\", \"piece.vxl\", \"probe.vxl\", \"propa.vxl\", \"ptruck.vxl\",\n    \"pulscan.vxl\", \"repair.vxl\", \"rtnk.vxl\", \"rtnkbarl.vxl\", \"rtnktur.vxl\",\n    \"sam.vxl\", \"sapc.vxl\", \"scrin.vxl\", \"shad.vxl\", \"smcv.vxl\",\n    \"sonic.vxl\", \"sonictur.vxl\", \"sref.vxl\", \"sreftur.vxl\", \"sreftur1.vxl\",\n    \"sreftur2.vxl\", \"sreftur3.vxl\", \"stang.vxl\", \"stnk.vxl\", \"sub.vxl\",\n    \"subt.vxl\", \"subtank.vxl\", \"suvb.vxl\", \"suvw.vxl\", \"taxi.vxl\",\n    \"tire.vxl\", \"tnkd.vxl\", \"tractor.vxl\", \"tran.vxl\", \"trnsport.vxl\",\n    \"trs.vxl\", \"truck2.vxl\", \"trucka.vxl\", \"truckb.vxl\", \"truk.vxl\",\n    \"ttnk.vxl\", \"ttnktur.vxl\", \"tug.vxl\", \"utnk.vxl\", \"v3.vxl\",\n    \"v3rocket.vxl\", \"v3wo.vxl\", \"vlad.vxl\", \"vladwo.vxl\", \"weed.vxl\",\n    \"wini.vxl\", \"wrmn.vxl\", \"zbomb.vxl\", \"zep.vxl\"\n];\nclass VxlWrapper implements Renderable {\n    private builder: VxlNonBatchedBuilder;\n    private wrapper: THREE.Object3D;\n    constructor(vxlFile: VxlFile, hvaFile: any | undefined, palette: Palette, vxlGeometryPool: VxlGeometryPool, camera: THREE.Camera) {\n        this.builder = new VxlNonBatchedBuilder(vxlFile, palette, hvaFile ?? null, vxlGeometryPool, camera);\n        this.wrapper = new THREE.Object3D();\n    }\n    get3DObject(): THREE.Object3D {\n        return this.wrapper;\n    }\n    create3DObject(): void {\n        console.log('[VxlWrapper] create3DObject called');\n        this.builder.setExtraLight(new THREE.Vector3(1.7, 1.7, 1.7));\n        const object = this.builder.build();\n        console.log('[VxlWrapper] Builder returned object:', object);\n        if (object) {\n            console.log('[VxlWrapper] Object details:', {\n                type: object.type,\n                children: object.children.length,\n                visible: object.visible,\n                position: object.position,\n                scale: object.scale,\n                rotation: object.rotation\n            });\n            object.children.forEach((child, index) => {\n                console.log(`[VxlWrapper] Child ${index}:`, {\n                    type: child.type,\n                    visible: child.visible,\n                    hasChildren: child.children.length\n                });\n                if (child.children.length > 0) {\n                    child.children.forEach((subChild, subIndex) => {\n                        if (subChild instanceof THREE.Mesh) {\n                            const mesh = subChild as THREE.Mesh;\n                            console.log(`[VxlWrapper] SubChild ${subIndex} mesh:`, {\n                                geometry: mesh.geometry,\n                                vertexCount: mesh.geometry.attributes.position?.count || 0,\n                                material: mesh.material,\n                                materialType: Array.isArray(mesh.material) ? 'array' : mesh.material.type,\n                                visible: mesh.visible\n                            });\n                        }\n                    });\n                }\n            });\n        }\n        this.wrapper.add(object);\n        console.log('[VxlWrapper] Object added to wrapper');\n    }\n    update(deltaTime: number): void {\n        this.wrapper.rotation.y -= 0.01;\n    }\n    destroy(): void {\n        this.builder.dispose();\n    }\n}\nexport class VxlTester {\n    private static renderer?: Renderer;\n    private static worldScene?: WorldScene;\n    private static vfs?: VirtualFileSystem;\n    private static vxlGeometryPool?: VxlGeometryPool;\n    private static palette?: Palette;\n    private static uiAnimationLoop?: UiAnimationLoop;\n    private static currentVxl?: VxlWrapper;\n    private static listEl?: HTMLDivElement;\n    private static homeButton?: HTMLButtonElement;\n    private static hostElement?: HTMLElement;\n    private static disposables = new CompositeDisposable();\n    private static availableFiles: string[] = [];\n    static async main(vfs: VirtualFileSystem, runtimeVars: any, context: TestToolRuntimeContext = {}): Promise<void> {\n        await TestToolSupport.ensureResourceTypes([ResourceType.TheaterTemp, ResourceType.TheaterTemp2, ResourceType.Vxl], context.cdnResourceLoader);\n        const hostElement = this.hostElement = TestToolSupport.prepareHost(context, 1020, 600);\n        const renderer = this.renderer = new Renderer(800, 600);\n        renderer.init(hostElement);\n        TestToolSupport.placeRendererCanvas(renderer, 0, 0);\n        renderer.initStats(document.body);\n        this.vfs = vfs;\n        this.vxlGeometryPool = new VxlGeometryPool(new VxlGeometryCache(null, null));\n        this.palette = new Palette(vfs.openFile(\"unittem.pal\"));\n        this.availableFiles = TestToolSupport.getExistingFiles(VXL_FILES);\n        TestToolSupport.setState('vxl', {\n            availableFileCount: this.availableFiles.length,\n            selectedFile: null,\n            meshCount: 0,\n            visibleMeshCount: 0,\n        });\n        const worldScene = this.worldScene = WorldScene.factory({ x: 0, y: 0, width: 800, height: 600 }, new BoxedVar(true), new BoxedVar(ShadowQuality.High));\n        this.disposables.add(worldScene);\n        worldScene.scene.background = new THREE.Color(0xE0E0E0);\n        worldScene.camera.far += 1000;\n        worldScene.camera.updateProjectionMatrix();\n        worldScene.create3DObject();\n        worldScene.scene.traverse((obj) => {\n            if (obj instanceof THREE.Light) {\n                if (obj instanceof THREE.AmbientLight) {\n                    (obj as any).intensity = 1.0;\n                }\n                else if (obj instanceof THREE.DirectionalLight) {\n                    (obj as any).intensity = 1.2;\n                }\n            }\n        });\n        const canvasMetrics = new CanvasMetrics(renderer.getCanvas(), window);\n        canvasMetrics.init();\n        this.disposables.add(canvasMetrics);\n        const pointerEvents = new PointerEvents(renderer, { x: 0, y: 0 }, document, canvasMetrics);\n        const cameraZoomControls = new CameraZoomControls(pointerEvents, worldScene.cameraZoom);\n        this.disposables.add(cameraZoomControls, pointerEvents);\n        cameraZoomControls.init();\n        this.createFloor();\n        this.buildBrowser(hostElement);\n        renderer.addScene(worldScene);\n        const animationLoop = this.uiAnimationLoop = new UiAnimationLoop(renderer);\n        animationLoop.start();\n    }\n    private static createFloor(): void {\n        const geometry = new THREE.PlaneGeometry(10000, 10000);\n        const material = new THREE.ShadowMaterial();\n        material.opacity = 0.5;\n        const mesh = new THREE.Mesh(geometry, material);\n        mesh.rotation.x = -Math.PI / 2;\n        mesh.position.y = -100;\n        mesh.receiveShadow = true;\n        this.worldScene?.scene.add(mesh);\n    }\n    private static async selectVxl(filename: string): Promise<void> {\n        if (this.currentVxl) {\n            this.worldScene?.remove(this.currentVxl);\n            this.currentVxl.destroy();\n        }\n        const file = this.vfs!.openFile(filename);\n        const vxlFile = new VxlFile(file);\n        const vxl = this.currentVxl = new VxlWrapper(vxlFile, undefined, this.palette!, this.vxlGeometryPool!, this.worldScene!.camera);\n        this.worldScene?.add(vxl);\n        if (this.worldScene) {\n            this.worldScene.processRenderQueue();\n        }\n        const state = this.collectState(filename, vxlFile);\n        TestToolSupport.setState('vxl', state);\n    }\n    private static buildBrowser(hostElement: HTMLElement): void {\n        const homeButton = document.createElement('button');\n        homeButton.innerHTML = '点此返回主页';\n        homeButton.style.cssText = `\n      position: fixed;\n      left: 50%;\n      top: 10px;\n      transform: translateX(-50%);\n      padding: 10px 20px;\n      background-color: rgba(0, 0, 0, 0.8);\n      color: white;\n      border: 2px solid rgba(255, 255, 255, 0.3);\n      border-radius: 6px;\n      cursor: pointer;\n      font-size: 16px;\n      font-weight: bold;\n      z-index: 1000;\n      transition: all 0.3s ease;\n      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n    `;\n        homeButton.onmouseover = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';\n            homeButton.style.transform = 'translateX(-50%) translateY(-2px)';\n            homeButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';\n        };\n        homeButton.onmouseout = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';\n            homeButton.style.transform = 'translateX(-50%) translateY(0)';\n            homeButton.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';\n        };\n        homeButton.onclick = () => {\n            window.location.hash = '/';\n        };\n        document.body.appendChild(homeButton);\n        const listEl = this.listEl = document.createElement('div');\n        listEl.style.position = 'absolute';\n        listEl.style.right = '0';\n        listEl.style.top = '0';\n        listEl.style.height = '600px';\n        listEl.style.width = '200px';\n        listEl.style.overflowY = 'auto';\n        listEl.style.backgroundColor = 'rgba(255, 255, 255, 0.9)';\n        listEl.style.padding = '10px';\n        const title = document.createElement('h3');\n        title.textContent = 'VXL Files';\n        title.style.marginTop = '0';\n        title.style.marginBottom = '10px';\n        listEl.appendChild(title);\n        this.availableFiles.forEach(filename => {\n            const link = document.createElement('a');\n            link.style.display = 'block';\n            link.style.padding = '4px 0';\n            link.textContent = filename;\n            link.setAttribute('href', 'javascript:;');\n            link.onmouseover = () => {\n                link.style.backgroundColor = 'rgba(170, 16, 16, 0.95)';\n            };\n            link.onmouseout = () => {\n                link.style.backgroundColor = 'rgba(110, 6, 6, 0.72)';\n            };\n            link.addEventListener('click', () => {\n                console.log('Selected vxl', filename);\n                VxlTester.selectVxl(filename);\n            });\n            listEl.appendChild(link);\n        });\n        hostElement.appendChild(listEl);\n        TestToolSupport.applyPanelTheme(listEl);\n        this.homeButton = homeButton;\n        setTimeout(() => {\n            const initialFile = this.availableFiles[0];\n            if (initialFile) {\n                VxlTester.selectVxl(initialFile);\n            }\n        }, 50);\n    }\n    private static collectState(filename: string, vxlFile: VxlFile): Record<string, unknown> {\n        const object = this.currentVxl?.get3DObject();\n        const meshStats = {\n            meshCount: 0,\n            visibleMeshCount: 0,\n            vertexCount: 0,\n        };\n        object?.traverse((child) => {\n            if ((child as THREE.Mesh).isMesh) {\n                meshStats.meshCount += 1;\n                if (child.visible) {\n                    meshStats.visibleMeshCount += 1;\n                }\n                meshStats.vertexCount += (child as THREE.Mesh).geometry?.getAttribute?.('position')?.count ?? 0;\n            }\n        });\n        return {\n            availableFileCount: this.availableFiles.length,\n            selectedFile: filename,\n            sectionCount: vxlFile.sections.length,\n            voxelCount: vxlFile.voxelCount,\n            ...meshStats,\n        };\n    }\n    static destroy(): void {\n        if (this.currentVxl) {\n            this.worldScene?.remove(this.currentVxl);\n            this.currentVxl.destroy();\n            this.currentVxl = undefined;\n        }\n        if (this.uiAnimationLoop) {\n            this.uiAnimationLoop.destroy();\n            this.uiAnimationLoop = undefined;\n        }\n        if (this.listEl) {\n            this.listEl.remove();\n            this.listEl = undefined;\n        }\n        if (this.homeButton) {\n            this.homeButton.remove();\n            this.homeButton = undefined;\n        }\n        this.hostElement = undefined;\n        if (this.renderer) {\n            this.renderer.dispose();\n            this.renderer = undefined;\n        }\n        this.disposables.dispose();\n        this.worldScene = undefined;\n        this.vfs = undefined;\n        this.vxlGeometryPool = undefined;\n        this.palette = undefined;\n        this.availableFiles = [];\n        TestToolSupport.clearState('vxl');\n        if ((PipOverlay as any)?.clearCaches) {\n            PipOverlay.clearCaches();\n        }\n        if ((TextureUtils as any)?.cache) {\n            TextureUtils.cache.forEach((tex: any) => tex.dispose?.());\n            TextureUtils.cache.clear();\n        }\n    }\n}\n"
  },
  {
    "path": "src/tools/WorldSceneTester.ts",
    "content": "import { Renderer } from \"@/engine/gfx/Renderer\";\nimport { UiAnimationLoop } from \"@/engine/UiAnimationLoop\";\nimport { WorldScene } from \"@/engine/renderable/WorldScene\";\nimport { BoxedVar } from \"@/util/BoxedVar\";\nimport { ShadowQuality } from \"@/engine/renderable/entity/unit/ShadowQuality\";\nimport { CanvasMetrics } from \"@/gui/CanvasMetrics\";\nimport { PointerEvents } from \"@/gui/PointerEvents\";\nimport { CameraZoomControls } from \"@/tools/CameraZoomControls\";\nimport { Engine } from \"@/engine/Engine\";\nimport { Rules } from \"@/game/rules/Rules\";\nimport { Art } from \"@/game/art/Art\";\nimport { TheaterType } from \"@/engine/TheaterType\";\nimport { GameMap } from \"@/game/GameMap\";\nimport { getRandomInt } from \"@/util/math\";\nimport { ImageFinder } from \"@/engine/ImageFinder\";\nimport { MapRenderable } from \"@/engine/renderable/entity/map/MapRenderable\";\nimport { Lighting } from \"@/engine/Lighting\";\nimport { LightingDirector } from \"@/engine/gfx/lighting/LightingDirector\";\nimport { IsoCoords } from \"@/engine/IsoCoords\";\nimport { CompositeDisposable } from \"@/util/disposable/CompositeDisposable\";\nimport { TestToolSupport, type TestToolRuntimeContext } from \"@/tools/TestToolSupport\";\ndeclare const THREE: any;\nexport class WorldSceneTester {\n    private static disposables = new CompositeDisposable();\n    private static renderer?: Renderer;\n    private static worldScene?: WorldScene;\n    private static uiAnimationLoop?: UiAnimationLoop;\n    static async main(mixFileLoader: any, gameMapFile: any, parentElement: HTMLElement, _strings: any, context: TestToolRuntimeContext = {}): Promise<void> {\n        await TestToolSupport.ensureTheater(TheaterType.Temperate, context.cdnResourceLoader);\n        this.buildHomeButton();\n        const hostElement = TestToolSupport.prepareHost(context, 800, 600);\n        const renderer = (this.renderer = new Renderer(800, 600));\n        renderer.init(hostElement);\n        TestToolSupport.placeRendererCanvas(renderer, 0, 0);\n        renderer.initStats(document.body);\n        this.disposables.add(renderer);\n        const worldScene = (this.worldScene = WorldScene.factory({ x: 0, y: 0, width: 800, height: 600 }, new BoxedVar(true), new BoxedVar(ShadowQuality.High)));\n        this.disposables.add(worldScene);\n        IsoCoords.init({ x: 0, y: 0 });\n        worldScene.create3DObject();\n        const rules = new Rules(Engine.getRules());\n        const art = new Art(rules, Engine.getArt(), undefined, undefined);\n        const theater = await Engine.loadTheater(TheaterType.Temperate);\n        const gameMap = new GameMap(gameMapFile, theater.tileSets, rules, (min: number, max: number) => getRandomInt(min, max));\n        const lighting = new Lighting();\n        this.disposables.add(lighting);\n        worldScene.applyLighting(lighting);\n        const lightingDirector = new LightingDirector(lighting.mapLighting as any, renderer, new BoxedVar(1));\n        lightingDirector.init();\n        this.disposables.add(lightingDirector);\n        const imageFinder = new ImageFinder(Engine.getImages() as any, theater);\n        const mapRenderable = new MapRenderable(gameMap, undefined, { onChange: { subscribe() { }, unsubscribe() { } }, getRadLevel() { return 0; } }, lighting, theater, rules, art, imageFinder, worldScene.camera, new BoxedVar(false), 1, undefined as any, true);\n        (worldScene as any).add(mapRenderable as any);\n        worldScene.processRenderQueue();\n        try {\n            const localSize = (gameMap as any).mapBounds.getLocalSize();\n            const computeMapScreenBounds = (ls: {\n                x: number;\n                y: number;\n                width: number;\n                height: number;\n            }) => {\n                const topLeft = IsoCoords.screenTileToScreen(ls.x, ls.y);\n                const bottomRight = IsoCoords.screenTileToScreen(ls.x + ls.width, ls.y + ls.height - 1);\n                return { x: topLeft.x, y: topLeft.y, width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y };\n            };\n            const mapScreenBounds = computeMapScreenBounds(localSize);\n            const { MapPanningHelper } = await import(\"@/engine/util/MapPanningHelper\");\n            const panningHelper = new (MapPanningHelper as any)(gameMap);\n            worldScene.cameraPan.setPanLimits((panningHelper as any).computeCameraPanLimits(worldScene.viewport, mapScreenBounds));\n            const start = (gameMap as any).startingLocations?.[0] ?? { x: Math.floor(localSize.x + localSize.width / 2), y: Math.floor(localSize.y + localSize.height / 2) };\n            const initialPan = (panningHelper as any).computeCameraPanFromTile(start.x, start.y);\n            worldScene.cameraPan.setPan(initialPan);\n            const centerWorld = IsoCoords.screenTileToWorld(localSize.x + localSize.width / 2, localSize.y + localSize.height / 2);\n            worldScene.setLightFocusPoint(centerWorld.x, centerWorld.y);\n        }\n        catch (e) {\n            console.warn('[WorldSceneTester] Failed to set initial camera/light focus:', e);\n        }\n        (worldScene.scene as any).background = new (THREE as any).Color(0xE0E0E0);\n        const canvasMetrics = new CanvasMetrics(renderer.getCanvas(), window);\n        canvasMetrics.init();\n        this.disposables.add(canvasMetrics);\n        const pointerEvents = new PointerEvents(renderer, { x: 0, y: 0 }, document, canvasMetrics);\n        const cameraZoomControls = new CameraZoomControls(pointerEvents, worldScene.cameraZoom);\n        this.disposables.add(cameraZoomControls, pointerEvents);\n        cameraZoomControls.init();\n        renderer.addScene(worldScene);\n        const loop = (this.uiAnimationLoop = new UiAnimationLoop(renderer));\n        loop.start();\n        TestToolSupport.setState('worldscene', {\n            mapWidth: gameMapFile.fullSize.width,\n            mapHeight: gameMapFile.fullSize.height,\n            startingLocations: gameMap.startingLocations?.length ?? 0,\n            viewport: worldScene.viewport,\n        });\n    }\n    private static buildHomeButton(): void {\n        const homeButton = document.createElement('button');\n        homeButton.innerHTML = '点此返回主页';\n        homeButton.style.cssText = `\n      position: fixed;\n      left: 50%;\n      top: 10px;\n      transform: translateX(-50%);\n      padding: 10px 20px;\n      background-color: rgba(0, 0, 0, 0.8);\n      color: white;\n      border: 2px solid rgba(255, 255, 255, 0.3);\n      border-radius: 6px;\n      cursor: pointer;\n      font-size: 16px;\n      font-weight: bold;\n      z-index: 1000;\n      transition: all 0.3s ease;\n      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n    `;\n        homeButton.onmouseover = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';\n            homeButton.style.transform = 'translateX(-50%) translateY(-2px)';\n            homeButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';\n        };\n        homeButton.onmouseout = () => {\n            homeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';\n            homeButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';\n            homeButton.style.transform = 'translateX(-50%) translateY(0)';\n            homeButton.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';\n        };\n        homeButton.onclick = () => {\n            window.location.hash = '/';\n        };\n        document.body.appendChild(homeButton);\n        this.disposables.add(() => homeButton.remove());\n    }\n    static destroy(): void {\n        TestToolSupport.clearState('worldscene');\n        if (this.uiAnimationLoop) {\n            this.uiAnimationLoop.destroy();\n            this.uiAnimationLoop = undefined;\n        }\n        if (this.renderer) {\n            this.renderer.dispose();\n            this.renderer = undefined;\n        }\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/types/game.d.ts",
    "content": "import type { Building as BuildingType } from \"@/game/gameobject/Building\";\nimport type { Player as PlayerType } from \"@/game/Player\";\nimport type { GameObject as GameObjectType } from \"@/game/gameobject/GameObject\";\nimport type { Tile as TileType } from \"@/game/map/Tile\";\ndeclare global {\n    type GameContext = import(\"@/game/Game\").Game;\n    type Game = import(\"@/game/Game\").Game;\n    type Building = BuildingType;\n    type Player = PlayerType;\n    type Unit = any;\n    type GameObject = GameObjectType;\n    type Tile = TileType;\n}\nexport {};\n"
  },
  {
    "path": "src/types/global.d.ts",
    "content": "declare global {\n    interface Window {\n        showDirectoryPicker?: () => Promise<FileSystemDirectoryHandle>;\n    }\n    interface FileSystemHandle {\n        readonly kind: 'file' | 'directory';\n        readonly name: string;\n    }\n    interface FileSystemFileHandle extends FileSystemHandle {\n        readonly kind: 'file';\n        getFile(): Promise<File>;\n    }\n    interface FileSystemDirectoryHandle extends FileSystemHandle {\n        readonly kind: 'directory';\n        entries(): AsyncIterableIterator<[\n            string,\n            FileSystemHandle\n        ]>;\n        keys(): AsyncIterableIterator<string>;\n        values(): AsyncIterableIterator<FileSystemHandle>;\n        getDirectoryHandle(name: string, options?: {\n            create?: boolean;\n        }): Promise<FileSystemDirectoryHandle>;\n        getFileHandle(name: string, options?: {\n            create?: boolean;\n        }): Promise<FileSystemFileHandle>;\n        removeEntry(name: string, options?: {\n            recursive?: boolean;\n        }): Promise<void>;\n    }\n}\nexport {};\n"
  },
  {
    "path": "src/util/Base64.ts",
    "content": "declare var Buffer: any;\nexport class Base64 {\n    static encode(str: string): string {\n        if (typeof globalThis.btoa === 'function') {\n            try {\n                return globalThis.btoa(str);\n            }\n            catch (e) {\n                if (typeof Buffer !== 'undefined') {\n                    return Buffer.from(str, 'utf-8').toString('base64');\n                }\n                else {\n                    console.warn('Base64.encode: Buffer is not defined, encoding may be incorrect for non-ASCII.');\n                    return unescape(encodeURIComponent(str));\n                }\n            }\n        }\n        else if (typeof Buffer !== 'undefined') {\n            return Buffer.from(str, 'utf-8').toString('base64');\n        }\n        else {\n            throw new Error('Base64 encoding unsupported in this environment.');\n        }\n    }\n    static decode(encodedStr: string): string {\n        if (typeof globalThis.atob === 'function') {\n            try {\n                return globalThis.atob(encodedStr);\n            }\n            catch (e) {\n                if (typeof Buffer !== 'undefined') {\n                    return Buffer.from(encodedStr, 'base64').toString('utf-8');\n                }\n                else {\n                    console.warn('Base64.decode: Buffer is not defined, decoding may be incorrect for non-ASCII.');\n                    return decodeURIComponent(escape(encodedStr));\n                }\n            }\n        }\n        else if (typeof Buffer !== 'undefined') {\n            return Buffer.from(encodedStr, 'base64').toString('utf-8');\n        }\n        else {\n            throw new Error('Base64 decoding unsupported in this environment.');\n        }\n    }\n    static isBase64(str: string): boolean {\n        if (!str || typeof str !== 'string') {\n            return false;\n        }\n        const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;\n        if (!base64Regex.test(str)) {\n            return false;\n        }\n        const strictBase64Regex = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\n        return strictBase64Regex.test(str);\n    }\n}\n"
  },
  {
    "path": "src/util/BoxedVar.ts",
    "content": "import { EventDispatcher, EventListener } from './event';\nexport class BoxedVar<T> {\n    private _value: T;\n    private readonly _onChange: EventDispatcher<BoxedVar<T>, T>;\n    constructor(initialValue: T) {\n        this._onChange = new EventDispatcher<BoxedVar<T>, T>();\n        this._value = initialValue;\n    }\n    get value(): T {\n        return this._value;\n    }\n    set value(newValue: T) {\n        const hasChanged = newValue !== this._value;\n        this._value = newValue;\n        if (hasChanged) {\n            this._onChange.dispatch(this, newValue);\n        }\n    }\n    get onChange(): {\n        subscribe: (listener: EventListener<BoxedVar<T>, T>) => void;\n        subscribeOnce: (listener: EventListener<BoxedVar<T>, T>) => void;\n        unsubscribe: (listener: EventListener<BoxedVar<T>, T>) => void;\n    } {\n        return this._onChange.asEvent();\n    }\n}\n"
  },
  {
    "path": "src/util/Color.ts",
    "content": "import { pad } from \"./string\";\nexport class Color {\n    public r: number;\n    public g: number;\n    public b: number;\n    static fromRgb(r: number, g: number, b: number): Color {\n        return new Color(r, g, b);\n    }\n    static fromHsv(h: number, s: number, v: number): Color {\n        let r_out = 0, g_out = 0, b_out = 0;\n        const h_norm = ((h / 255) * 360) % 360;\n        const s_norm = s / 255;\n        const v_norm = v / 255;\n        if (s_norm === 0) {\n            r_out = v_norm;\n            g_out = v_norm;\n            b_out = v_norm;\n        }\n        else {\n            const i = Math.floor(h_norm / 60);\n            const f = h_norm / 60 - i;\n            const p = v_norm * (1 - s_norm);\n            const q = v_norm * (1 - s_norm * f);\n            const t = v_norm * (1 - s_norm * (1 - f));\n            switch (i) {\n                case 0:\n                    r_out = v_norm;\n                    g_out = t;\n                    b_out = p;\n                    break;\n                case 1:\n                    r_out = q;\n                    g_out = v_norm;\n                    b_out = p;\n                    break;\n                case 2:\n                    r_out = p;\n                    g_out = v_norm;\n                    b_out = t;\n                    break;\n                case 3:\n                    r_out = p;\n                    g_out = q;\n                    b_out = v_norm;\n                    break;\n                case 4:\n                    r_out = t;\n                    g_out = p;\n                    b_out = v_norm;\n                    break;\n                case 5:\n                default:\n                    r_out = v_norm;\n                    g_out = p;\n                    b_out = q;\n                    break;\n            }\n        }\n        return Color.fromRgb(Math.floor(r_out * 255), Math.floor(g_out * 255), Math.floor(b_out * 255));\n    }\n    constructor(r: number, g: number, b: number) {\n        this.r = r;\n        this.g = g;\n        this.b = b;\n    }\n    asHex(): number {\n        return (this.r << 16) | (this.g << 8) | this.b;\n    }\n    asHexString(): string {\n        return \"#\" + this.asHex().toString(16).padStart(6, '0');\n    }\n    clone(): Color {\n        return new Color(this.r, this.g, this.b);\n    }\n}\n"
  },
  {
    "path": "src/util/CssLoader.ts",
    "content": "export class CssLoader {\n    private document: Document;\n    constructor(document: Document) {\n        this.document = document;\n    }\n    async load(url: string): Promise<void> {\n        return new Promise((resolve, reject) => {\n            const link = document.createElement(\"link\");\n            link.rel = \"stylesheet\";\n            link.type = \"text/css\";\n            link.href = url;\n            if (\"onload\" in link) {\n                link.onload = () => resolve();\n            }\n            if (\"onerror\" in link) {\n                link.onerror = () => reject(new Error(`Couldn't load CSS at \"${url}\"`));\n            }\n            this.document.head.appendChild(link);\n            if (!(\"onload\" in link)) {\n                resolve();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/util/Graph.ts",
    "content": "export class GraphNode<T> {\n    id: string;\n    data: T;\n    neighbors: Set<GraphNode<T>>;\n    constructor(id: string, data: T) {\n        this.id = id;\n        this.data = data;\n        this.neighbors = new Set();\n    }\n    addLink(node: GraphNode<T>): void {\n        this.neighbors.add(node);\n        if (node !== this) {\n            node.neighbors.add(this);\n        }\n    }\n    removeLink(node: GraphNode<T>): void {\n        this.neighbors.delete(node);\n        node.neighbors.delete(this);\n    }\n    deleteLinks(): void {\n        for (const node of this.neighbors) {\n            node.neighbors.delete(this);\n        }\n        this.neighbors.clear();\n    }\n}\nexport class Graph<T> {\n    private nodes: Map<string, GraphNode<T>>;\n    constructor() {\n        this.nodes = new Map();\n    }\n    addNode(id: string, data: T): GraphNode<T> {\n        let node = this.getNode(id);\n        if (node) {\n            node.data = data;\n        }\n        else {\n            node = new GraphNode(id, data);\n        }\n        this.nodes.set(id, node);\n        return node;\n    }\n    removeNode(id: string): boolean {\n        const node = this.getNode(id);\n        if (!node) {\n            return false;\n        }\n        node.deleteLinks();\n        this.nodes.delete(id);\n        return true;\n    }\n    hasNode(id: string): boolean {\n        return this.nodes.has(id);\n    }\n    getNode(id: string): GraphNode<T> | undefined {\n        return this.nodes.get(id);\n    }\n    getNodeCount(): number {\n        return this.nodes.size;\n    }\n    forEachNode(callback: (node: GraphNode<T>) => void): void {\n        for (const node of this.nodes.values()) {\n            callback(node);\n        }\n    }\n    clear(): void {\n        for (const node of this.nodes.values()) {\n            node.deleteLinks();\n        }\n        this.nodes.clear();\n    }\n}\n"
  },
  {
    "path": "src/util/PointerLock.ts",
    "content": "import { EventDispatcher } from \"./event\";\nimport { CompositeDisposable } from \"./disposable/CompositeDisposable\";\nexport class PointerLock {\n    private element: HTMLElement;\n    private document: Document;\n    private _onChange: EventDispatcher<PointerLock, boolean>;\n    private disposables: CompositeDisposable;\n    private listening: boolean;\n    get onChange() {\n        return this._onChange.asEvent();\n    }\n    constructor(element: HTMLElement, document: Document) {\n        this.element = element;\n        this.document = document;\n        this._onChange = new EventDispatcher();\n        this.disposables = new CompositeDisposable();\n        this.listening = false;\n    }\n    async request(options?: {\n        unadjustedMovement?: boolean;\n    }): Promise<void> {\n        if (options?.unadjustedMovement) {\n            try {\n                await this.requestInternal({ unadjustedMovement: true });\n            }\n            catch (e) {\n                if ((e as Error).name !== \"NotSupportedError\")\n                    throw e;\n                await this.requestInternal();\n            }\n        }\n        else {\n            await this.requestInternal();\n        }\n    }\n    private async requestInternal(options?: PointerLockOptions): Promise<void> {\n        if (!this.isActive()) {\n            if (!this.listening) {\n                this.listening = true;\n                const changeHandler = () => {\n                    this._onChange.dispatch(this, this.isActive());\n                };\n                this.document.addEventListener(\"pointerlockchange\", changeHandler, false);\n                this.disposables.add(() => this.document.removeEventListener(\"pointerlockchange\", changeHandler, false));\n                const touchHandler = () => {\n                    this.exit().catch((e) => console.error(e));\n                };\n                this.document.addEventListener(\"touchstart\", touchHandler, false);\n                this.disposables.add(() => this.document.removeEventListener(\"touchstart\", touchHandler, false));\n            }\n            return new Promise<void>((resolve, reject) => {\n                const changeHandler = () => {\n                    this.document.removeEventListener(\"pointerlockchange\", changeHandler, false);\n                    this.document.removeEventListener(\"pointerlockerror\", errorHandler, false);\n                    resolve();\n                };\n                const errorHandler = (e: Event) => {\n                    this.document.removeEventListener(\"pointerlockchange\", changeHandler, false);\n                    this.document.removeEventListener(\"pointerlockerror\", errorHandler, false);\n                    console.error(e);\n                    reject(new Error(\"Pointer lock error\"));\n                };\n                this.document.addEventListener(\"pointerlockerror\", errorHandler, false);\n                this.document.addEventListener(\"pointerlockchange\", changeHandler, false);\n                this.element.requestPointerLock(options)?.catch?.(reject);\n            });\n        }\n    }\n    async exit(): Promise<void> {\n        if (this.isActive()) {\n            return new Promise<void>((resolve, reject) => {\n                const changeHandler = () => {\n                    this.document.removeEventListener(\"pointerlockchange\", changeHandler, false);\n                    this.document.removeEventListener(\"pointerlockerror\", errorHandler, false);\n                    resolve();\n                };\n                const errorHandler = (e: Event) => {\n                    this.document.removeEventListener(\"pointerlockchange\", changeHandler, false);\n                    this.document.removeEventListener(\"pointerlockerror\", errorHandler, false);\n                    console.error(e);\n                    reject(new Error(\"Pointer lock error\"));\n                };\n                this.document.addEventListener(\"pointerlockerror\", errorHandler, false);\n                this.document.addEventListener(\"pointerlockchange\", changeHandler, false);\n                this.document.exitPointerLock();\n            });\n        }\n    }\n    isActive(): boolean {\n        return this.element === this.document.pointerLockElement;\n    }\n    dispose(): void {\n        this.disposables.dispose();\n    }\n}\n"
  },
  {
    "path": "src/util/QuadTree.ts",
    "content": "import { Box2 } from '@/game/math/Box2';\nimport { Vector2 } from '@/game/math/Vector2';\ninterface QuadTreeConfig<T> {\n    getKey: (item: T) => Vector2;\n    maxDepth: number;\n    splitThreshold: number;\n    joinThreshold: number;\n}\ninterface QuadTreeItem<T> {\n    key: Vector2;\n    value: T;\n}\nexport class QuadTree<T> {\n    private box: Box2;\n    private config: QuadTreeConfig<T>;\n    private parentMap: Map<T, QuadTree<T>>;\n    private objects: QuadTreeItem<T>[];\n    private regions?: QuadTree<T>[];\n    private parent?: QuadTree<T>;\n    constructor(box: Box2, config: QuadTreeConfig<T>) {\n        this.box = box;\n        this.config = config;\n        this.parentMap = new Map();\n        this.objects = [];\n    }\n    add(item: T, shouldUpdate: boolean = true): boolean {\n        const key = this.config.getKey(item);\n        if (this.box.containsPoint(key)) {\n            if (!this.regions) {\n                this.parentMap.get(item)?.remove(item);\n                this.parentMap.set(item, this);\n                this.objects.push({ key, value: item });\n                if (shouldUpdate) {\n                    this.update();\n                }\n                return true;\n            }\n            for (const region of this.regions) {\n                if (region.add(item, shouldUpdate)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n    remove(item: T, shouldUpdate: boolean = true): void {\n        const region = this.parentMap.get(item);\n        if (region) {\n            if (region === this) {\n                this.parentMap.delete(item);\n                const index = this.objects.findIndex(obj => obj.value === item);\n                this.objects.splice(index, 1);\n                if (shouldUpdate && this.parent) {\n                    this.parent.update();\n                }\n            }\n            else {\n                region.remove(item, shouldUpdate);\n            }\n        }\n    }\n    updateObject(item: T): void {\n        const region = this.parentMap.get(item);\n        if (region) {\n            const key = this.config.getKey(item);\n            if (region.box.containsPoint(key)) {\n                const obj = region.objects.find(obj => obj.value === item);\n                if (obj) {\n                    obj.key = key;\n                }\n            }\n            else {\n                region.remove(item, false);\n                let parent = region.parent;\n                while (parent && !parent.add(item, false)) {\n                    parent = parent.parent;\n                }\n            }\n        }\n    }\n    queryRange(range: Box2, result: T[] = []): T[] {\n        if (this.box.intersectsBox(range)) {\n            if (this.regions) {\n                for (const region of this.regions) {\n                    region.queryRange(range, result);\n                }\n            }\n            else {\n                for (const obj of this.objects) {\n                    if (range.containsPoint(obj.key)) {\n                        result.push(obj.value);\n                    }\n                }\n            }\n        }\n        return result;\n    }\n    update(): number {\n        let count = 0;\n        if (this.regions) {\n            for (const region of this.regions) {\n                count += region.update();\n            }\n            if (count <= this.config.joinThreshold) {\n                this.join();\n            }\n        }\n        else {\n            count = this.objects.length;\n            if (count >= this.config.splitThreshold && this.split()) {\n                this.update();\n            }\n        }\n        return count;\n    }\n    private split(): boolean {\n        if (this.regions || this.config.maxDepth <= 1) {\n            return false;\n        }\n        const config = {\n            getKey: this.config.getKey,\n            joinThreshold: this.config.joinThreshold,\n            splitThreshold: this.config.splitThreshold,\n            maxDepth: this.config.maxDepth - 1,\n        };\n        const newRegions = this.generateRegions();\n        const oldObjects = this.objects;\n        this.objects = [];\n        this.regions = [];\n        for (const box of newRegions) {\n            const region = new QuadTree<T>(box, config);\n            region.parentMap = this.parentMap;\n            this.regions.push(region);\n            region.parent = this;\n        }\n        for (const obj of oldObjects) {\n            this.parentMap.delete(obj.value);\n            this.add(obj.value, false);\n        }\n        return true;\n    }\n    private join(): boolean {\n        if (!this.regions) {\n            return false;\n        }\n        for (const region of this.regions) {\n            region.join();\n            region.parent = undefined;\n            for (const obj of region.objects) {\n                this.objects.push(obj);\n                this.parentMap.set(obj.value, this);\n            }\n        }\n        this.regions = undefined;\n        return true;\n    }\n    private generateRegions(): Box2[] {\n        const regions: Box2[] = [this.box.clone()];\n        const center = this.box.getCenter(new Vector2());\n        let current = regions[0];\n        let next = current.clone();\n        current.max.x = center.x;\n        next.min.x = center.x;\n        regions.push(next);\n        for (let i = 0, len = regions.length; i < len; i++) {\n            current = regions[i];\n            next = current.clone();\n            current.max.y = center.y;\n            next.min.y = center.y;\n            regions.push(next);\n        }\n        return regions;\n    }\n}\n"
  },
  {
    "path": "src/util/Routing.ts",
    "content": "type RouteController = (params: string[]) => void | Promise<void>;\ninterface RouteEntry {\n    controller: RouteController;\n}\nexport class Routing {\n    private routes: Record<string, RouteEntry> = {};\n    constructor() { }\n    public addRoute(path: string, controller: RouteController): void {\n        const normalizedPath = path === \"*\" ? \"*\" : (path.startsWith('/') ? path : `/${path}`);\n        this.routes[normalizedPath] = { controller };\n    }\n    public init(): void {\n        if (typeof window !== 'undefined' && typeof location !== 'undefined') {\n            window.addEventListener(\"hashchange\", this.handleHashChange);\n            this.router();\n        }\n        else {\n            console.warn(\"Routing.init: Cannot initialize routing outside of a browser environment.\");\n        }\n    }\n    private handleHashChange = (): void => {\n        this.router();\n    };\n    public async router(): Promise<void> {\n        if (typeof location === 'undefined') {\n            return;\n        }\n        const hashPath = location.hash.slice(1) || \"/\";\n        if (hashPath.startsWith('/')) {\n            const segments = hashPath.split('/');\n            const mainSegment = segments[1] || '';\n            const params = segments.slice(2);\n            const routeKey = `/${mainSegment}`;\n            const wildcardRoute = this.routes[\"*\"];\n            if (wildcardRoute) {\n                await wildcardRoute.controller(params);\n            }\n            const specificRoute = this.routes[routeKey];\n            if (specificRoute && specificRoute.controller) {\n                await specificRoute.controller(params);\n            }\n            else if (!wildcardRoute && routeKey !== \"/\") {\n                console.warn(`Routing: No controller found for route '${routeKey}'`);\n            }\n        }\n        else {\n            console.warn(`Routing: Path '${hashPath}' does not conform to expected format (e.g., #/path/to/resource)`);\n        }\n    }\n    public destroy(): void {\n        if (typeof window !== 'undefined') {\n            window.removeEventListener(\"hashchange\", this.handleHashChange);\n        }\n    }\n}\n"
  },
  {
    "path": "src/util/ScriptLoader.ts",
    "content": "export class ScriptLoader {\n    private document: Document;\n    constructor(document: Document) {\n        this.document = document;\n    }\n    async load(url: string, options: {\n        type?: string;\n        charset?: string;\n        async?: boolean;\n        attrs?: Record<string, string>;\n        text?: string;\n    } = {}): Promise<void> {\n        return new Promise((resolve, reject) => {\n            const head = this.document.head;\n            const script = this.document.createElement(\"script\");\n            script.type = options.type || \"text/javascript\";\n            script.charset = options.charset || \"utf8\";\n            script.async = options.async ?? true;\n            script.src = url;\n            if (options.attrs) {\n                Object.keys(options.attrs).forEach(key => script.setAttribute(key, options.attrs![key]));\n            }\n            if (options.text) {\n                script.text = options.text;\n            }\n            script.onload = () => resolve();\n            const error = new Error(`Failed to load script \"${url}\"`);\n            script.onerror = () => reject(error);\n            head.appendChild(script);\n        });\n    }\n}\n"
  },
  {
    "path": "src/util/Sentry.ts",
    "content": "import { ScriptLoader } from \"./ScriptLoader\";\ninterface SentryConfig {\n    dsn: string;\n    env: string;\n    defaultIntegrations?: boolean;\n    lazyLoad?: boolean;\n}\ninterface SentrySDK {\n    init: (config: any) => void;\n    onLoad: (callback: () => void) => void;\n    forceLoad: () => void;\n    captureException: (error: Error, context?: any) => void;\n    configureScope: (callback: (scope: any) => void) => void;\n    addBreadcrumb: (breadcrumb: any) => void;\n}\ndeclare global {\n    interface Window {\n        Sentry: SentrySDK;\n    }\n}\nexport class Sentry {\n    private sdk?: SentrySDK;\n    async init(config: SentryConfig, release: string): Promise<void> {\n        await new ScriptLoader(document).load(`https://js.sentry-cdn.com/${config.dsn}.min.js`);\n        let sdk = (this.sdk = window.Sentry);\n        const initTime = new Date();\n        sdk.init({\n            environment: config.env,\n            release: release,\n            denyUrls: [/^file:/],\n            ignoreErrors: [\n                /init message from worker/,\n                /The object can not be found here/,\n                /itemsclipboard/,\n                /A requested file or directory could not be found/,\n                /The requested file could not be read/,\n                /The play\\(\\) request/,\n                /^db$/,\n            ],\n            initialScope: (scope: any) => scope\n                .setTags({ locale: navigator.language })\n                .setExtra(\"initTime\", initTime),\n            ...(config.defaultIntegrations ? {} : { defaultIntegrations: false }),\n        });\n        sdk.onLoad(() => {\n            this.sdk = window.Sentry;\n        });\n        if (!config.lazyLoad) {\n            sdk.forceLoad();\n        }\n    }\n    captureException(error: Error, context?: any): void {\n        this.sdk?.captureException(error, context);\n    }\n    configureScope(callback: (scope: any) => void): void {\n        this.sdk?.configureScope(callback);\n    }\n    addBreadcrumb(breadcrumb: any): void {\n        this.sdk?.addBreadcrumb(breadcrumb);\n    }\n}\n"
  },
  {
    "path": "src/util/Serializable.ts",
    "content": "export class Serializable {\n    serialize(): any {\n        return {};\n    }\n    deserialize(data: any): void {\n    }\n}\n"
  },
  {
    "path": "src/util/array.ts",
    "content": "export function findReverse<T>(array: T[], predicate: (value: T, index: number, array: T[]) => boolean): T | undefined {\n    for (let i = array.length - 1; i >= 0; i--) {\n        if (predicate(array[i], i, array)) {\n            return array[i];\n        }\n    }\n    return undefined;\n}\nexport function findIndexReverse<T>(array: T[], predicate: (value: T, index: number, array: T[]) => boolean): number {\n    for (let i = array.length - 1; i >= 0; i--) {\n        if (predicate(array[i], i, array)) {\n            return i;\n        }\n    }\n    return -1;\n}\nexport function equals<T>(array1: T[], array2: T[]): boolean {\n    if (array1.length !== array2.length) {\n        return false;\n    }\n    for (let i = 0, length = array1.length; i < length; i++) {\n        if (array1[i] !== array2[i]) {\n            return false;\n        }\n    }\n    return true;\n}\n"
  },
  {
    "path": "src/util/bresenham.ts",
    "content": "export function bresenham(x0: number, y0: number, x1: number, y1: number, callback?: (x: number, y: number) => void): Array<{\n    x: number;\n    y: number;\n}> {\n    const points: Array<{\n        x: number;\n        y: number;\n    }> = [];\n    const dx = x1 - x0;\n    const dy = y1 - y0;\n    const absDx = Math.abs(dx);\n    const absDy = Math.abs(dy);\n    let error = 0;\n    const stepX = dx > 0 ? 1 : -1;\n    const stepY = dy > 0 ? 1 : -1;\n    callback = callback || ((x: number, y: number) => {\n        points.push({ x, y });\n    });\n    if (absDy < absDx) {\n        for (let x = x0, y = y0; stepX < 0 ? x >= x1 : x <= x1; x += stepX) {\n            callback(x, y);\n            error += absDy;\n            if ((error << 1) >= absDx) {\n                y += stepY;\n                error -= absDx;\n            }\n        }\n    }\n    else {\n        for (let x = x0, y = y0; stepY < 0 ? y >= y1 : y <= y1; y += stepY) {\n            callback(x, y);\n            error += absDx;\n            if ((error << 1) >= absDy) {\n                x += stepX;\n                error -= absDy;\n            }\n        }\n    }\n    return points;\n}\n"
  },
  {
    "path": "src/util/disposable/CompositeDisposable.ts",
    "content": "export interface Disposable {\n    dispose(): void;\n}\nexport interface Destroyable {\n    destroy(): void;\n}\nexport type DisposableFunction = () => void;\nexport type DisposableItem = Disposable | Destroyable | DisposableFunction;\nexport class CompositeDisposable implements Disposable {\n    private disposables = new Set<DisposableItem>();\n    add(...items: DisposableItem[]): void {\n        items.forEach(item => {\n            if (typeof item === 'function') {\n                this.disposables.add(item);\n            }\n            else {\n                this.disposables.add(item);\n            }\n        });\n    }\n    remove(...items: DisposableItem[]): void {\n        items.forEach(item => {\n            this.disposables.delete(item);\n        });\n    }\n    dispose(): void {\n        this.disposables.forEach(item => {\n            if (typeof item === 'function') {\n                item();\n            }\n            else if ('dispose' in item) {\n                item.dispose();\n            }\n            else if ('destroy' in item) {\n                item.destroy();\n            }\n        });\n        this.disposables.clear();\n    }\n}\n"
  },
  {
    "path": "src/util/disposable/Disposable.ts",
    "content": "export interface IDisposable {\n    dispose(): void;\n}\nexport class Disposable implements IDisposable {\n    private _isDisposed: boolean = false;\n    public get isDisposed(): boolean {\n        return this._isDisposed;\n    }\n    public dispose(): void {\n        if (this._isDisposed) {\n            return;\n        }\n        this._isDisposed = true;\n    }\n}\n"
  },
  {
    "path": "src/util/disposable/LegacyDisposable.ts",
    "content": "export class LegacyDisposable {\n    private disposed: boolean = false;\n    public dispose(): void {\n        if (this.disposed) {\n            return;\n        }\n        this.disposed = true;\n    }\n    public isDisposed(): boolean {\n        return this.disposed;\n    }\n}\n"
  },
  {
    "path": "src/util/dom.ts",
    "content": "export function getOffset(element: HTMLElement): {\n    top: number;\n    left: number;\n} {\n    let top = 0;\n    let left = 0;\n    let currentElement: HTMLElement | null = element;\n    while (currentElement) {\n        top += currentElement.offsetTop || 0;\n        left += currentElement.offsetLeft || 0;\n        currentElement = currentElement.offsetParent as HTMLElement | null;\n    }\n    return { top, left };\n}\nexport function contains(container: Element, element: Element | null): boolean {\n    let currentElement: Element | null = element;\n    do {\n        if (currentElement === container) {\n            return true;\n        }\n    } while ((currentElement = currentElement?.parentElement || null));\n    return false;\n}\n"
  },
  {
    "path": "src/util/event.ts",
    "content": "export type EventListener<TSource = any, TData = any> = (data: TData, source: TSource) => void;\nexport interface IEvent<TSource = any, TData = any> {\n    subscribe(listener: EventListener<TSource, TData>): void;\n    subscribeOnce(listener: EventListener<TSource, TData>): void;\n    unsubscribe(listener: EventListener<TSource, TData>): void;\n}\nexport class EventDispatcher<TSource = any, TData = any> implements IEvent<TSource, TData> {\n    private listeners: Set<EventListener<TSource, TData>>;\n    constructor() {\n        this.listeners = new Set<EventListener<TSource, TData>>();\n    }\n    subscribe(listener: EventListener<TSource, TData>): void {\n        this.listeners.add(listener);\n    }\n    subscribeOnce(listener: EventListener<TSource, TData>): void {\n        let onceListener: EventListener<TSource, TData> | undefined = (data: TData, source: TSource) => {\n            listener(data, source);\n            this.unsubscribe(onceListener!);\n            onceListener = undefined;\n        };\n        this.subscribe(onceListener);\n    }\n    unsubscribe(listener: EventListener<TSource, TData>): void {\n        this.listeners.delete(listener);\n    }\n    dispatch(source: TSource, data?: TData): void {\n        this.listeners.forEach((listener) => listener(data as TData, source));\n    }\n    asEvent(): IEvent<TSource, TData> {\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/util/format.ts",
    "content": "import { pad } from './string';\nexport function formatTimeDuration(seconds: number, showHours: boolean = false): string {\n    const hours = Math.floor(seconds / 3600);\n    seconds -= 3600 * hours;\n    const minutes = Math.floor(seconds / 60);\n    seconds = Math.floor(seconds - 60 * minutes);\n    return [...(hours || !showHours ? [hours] : []), pad(minutes, \"00\"), pad(seconds, \"00\")].join(\":\");\n}\n"
  },
  {
    "path": "src/util/fullScreen.ts",
    "content": "export type FullScreenChangeHandler = (isFullScreen: boolean) => void;\nexport function setupFullScreenChangeListener(document: Document, handler: FullScreenChangeHandler): (() => void) | undefined {\n    if (!document.fullscreenEnabled) {\n        console.warn(\"Browser fullscreen API not available.\");\n        return undefined;\n    }\n    let canF11Request = true;\n    const fullscreenChangeHandler = () => {\n        const isFullScreen = !!document.fullscreenElement;\n        if (isFullScreen) {\n            canF11Request = false;\n        }\n        else {\n            setTimeout(() => (canF11Request = true), 100);\n        }\n        handler(isFullScreen);\n    };\n    const keyUpHandler = async (event: KeyboardEvent) => {\n        if (event.keyCode === 122 && canF11Request && !document.fullscreenElement) {\n            try {\n                await document.documentElement.requestFullscreen();\n            }\n            catch (error) {\n                console.warn(\"Full screen permission denied by user.\");\n            }\n        }\n    };\n    document.addEventListener(\"fullscreenchange\", fullscreenChangeHandler);\n    document.addEventListener(\"keyup\", keyUpHandler);\n    return () => {\n        document.removeEventListener(\"fullscreenchange\", fullscreenChangeHandler);\n        document.removeEventListener(\"keyup\", keyUpHandler);\n    };\n}\n"
  },
  {
    "path": "src/util/geometry.ts",
    "content": "import { isBetween } from \"./math\";\nimport * as THREE from \"three\";\ninterface Point {\n    x: number;\n    y: number;\n}\ninterface Rect {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n}\ninterface Circle {\n    center: Point;\n    radius: number;\n}\nexport function pointEquals(a: Point | null | undefined, b: Point | null | undefined): boolean {\n    return (a && b && a.x === b.x && a.y === b.y) || (!a && !b);\n}\nexport function rectIntersect(a: Rect, b: Rect): boolean {\n    return (a.x <= b.x + b.width &&\n        b.x <= a.x + a.width &&\n        a.y <= b.y + b.height &&\n        b.y <= a.y + a.height);\n}\nexport function rectEquals(a: Rect, b: Rect): boolean {\n    return (a.x === b.x &&\n        a.y === b.y &&\n        a.width === b.width &&\n        a.height === b.height);\n}\nexport function circleIntersect(a: Circle, b: Circle): boolean {\n    const centerA = a.center;\n    const centerB = b.center;\n    return isBetween((centerA.x - centerB.x) * (centerA.x - centerB.x) + (centerA.y - centerB.y) * (centerA.y - centerB.y), (a.radius - b.radius) * (a.radius - b.radius), (a.radius + b.radius) * (a.radius + b.radius));\n}\nexport function circleContainsPoint(circle: Circle, point: Point): boolean {\n    const center = circle.center;\n    return ((center.x - point.x) * (center.x - point.x) + (center.y - point.y) * (center.y - point.y) <=\n        circle.radius * circle.radius);\n}\nexport function rectContainsPoint(rect: Rect, point: Point): boolean {\n    const box = new THREE.Box2(new THREE.Vector2(rect.x, rect.y), new THREE.Vector2(rect.x + rect.width, rect.y + rect.height));\n    return box.containsPoint(new THREE.Vector2(point.x, point.y));\n}\nexport function rectContainsRect(outer: Rect, inner: Rect): boolean {\n    const outerBox = new THREE.Box2(new THREE.Vector2(outer.x, outer.y), new THREE.Vector2(outer.x + outer.width, outer.y + outer.height));\n    const innerBox = new THREE.Box2(new THREE.Vector2(inner.x, inner.y), new THREE.Vector2(inner.x + inner.width, inner.y + inner.height));\n    return outerBox.containsBox(innerBox);\n}\nexport function rectClampPoint(rect: Rect, point: Point): THREE.Vector2 {\n    const box = new THREE.Box2(new THREE.Vector2(rect.x, rect.y), new THREE.Vector2(rect.x + rect.width, rect.y + rect.height));\n    return box.clampPoint(new THREE.Vector2(point.x, point.y), new THREE.Vector2());\n}\nexport function octileDistance(a: Point, b: Point): number {\n    const dx = Math.abs(a.x - b.x);\n    const dy = Math.abs(a.y - b.y);\n    return dx + dy + (Math.SQRT2 - 2) * Math.min(dx, dy);\n}\n"
  },
  {
    "path": "src/util/keyNames.ts",
    "content": "const keyMap = new Map<number, string>([\n    [8, \"Backspace\"],\n    [9, \"Tab\"],\n    [12, \"Clear\"],\n    [13, \"Enter\"],\n    [19, \"Pause/Break\"],\n    [20, \"CapsLock\"],\n    [27, \"Esc\"],\n    [32, \"Space\"],\n    [33, \"PageUp\"],\n    [34, \"PageDown\"],\n    [35, \"End\"],\n    [36, \"Home\"],\n    [37, \"ArrowLeft\"],\n    [38, \"ArrowUp\"],\n    [39, \"ArrowRight\"],\n    [40, \"ArrowDown\"],\n    [44, \"PrintScreen\"],\n    [45, \"Insert\"],\n    [46, \"Delete\"],\n    [91, \"LeftWin/⌘\"],\n    [92, \"RightWin/⌘\"],\n    ...Array.from({ length: 10 }, (_, i): [\n        number,\n        string\n    ] => [96 + i, `Num${i}`]),\n    [106, \"Num*\"],\n    [107, \"Num+\"],\n    [109, \"Num-\"],\n    [110, \"NumDel\"],\n    [111, \"Num/\"],\n    ...Array.from({ length: 32 }, (_, i): [\n        number,\n        string\n    ] => [111 + i + 1, `F${i + 1}`]),\n    [144, \"NumLock\"],\n    [145, \"ScrollLock\"],\n    [186, \";\"],\n    [187, \"=\"],\n    [188, \",\"],\n    [189, \"-\"],\n    [190, \".\"],\n    [191, \"/\"],\n    [192, \"`\"],\n    [219, \"[\"],\n    [220, \"\\\\\"],\n    [221, \"]\"],\n    [222, \"'\"],\n]);\nexport function getKeyName(keyCode: number): string {\n    const name = keyMap.get(keyCode);\n    return name !== undefined ? name : String.fromCharCode(keyCode);\n}\n"
  },
  {
    "path": "src/util/logger.ts",
    "content": "import Logger from 'js-logger';\nLogger.useDefaults();\nexport const AppLogger = Logger;\nexport default AppLogger;\n"
  },
  {
    "path": "src/util/math.ts",
    "content": "export function getRandomInt(min: number, max: number): number {\n    min = Math.ceil(min);\n    max = Math.floor(max);\n    return Math.floor(Math.random() * (max - min + 1)) + min;\n}\nexport function clamp(value: number, min: number, max: number): number {\n    return Math.min(max, Math.max(value, min));\n}\nexport function isBetween(value: number, min: number, max: number): boolean {\n    return min <= value && value <= max;\n}\nexport function lerp(start: number, end: number, t: number): number {\n    return (1 - t) * start + t * end;\n}\nexport function truncToDecimals(num: number, decimalPlaces: number): number {\n    if (!num)\n        return num;\n    const factor = 10 ** decimalPlaces;\n    return (num >= 0 ? Math.floor(num * factor) : Math.ceil(num * factor)) / factor;\n}\nexport function roundToDecimals(num: number, decimalPlaces: number): number {\n    if (!num)\n        return num;\n    const factor = 10 ** decimalPlaces;\n    return Math.round(num * factor) / factor;\n}\nexport function floorTo(value: number, significance: number): number {\n    if (significance === 0)\n        return value;\n    return Math.floor(value / significance) * significance;\n}\nexport function fnv32a(data: Uint8Array | number[]): number {\n    let hash = 0x811c9dc5;\n    for (let i = 0; i < data.length; ++i) {\n        hash ^= data[i];\n        hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);\n    }\n    return hash >>> 0;\n}\n"
  },
  {
    "path": "src/util/mouse.ts",
    "content": "export class Mouse {\n    private static instance: Mouse;\n    private constructor() { }\n    public static getInstance(): Mouse {\n        if (!Mouse.instance) {\n            Mouse.instance = new Mouse();\n        }\n        return Mouse.instance;\n    }\n}\n"
  },
  {
    "path": "src/util/number.ts",
    "content": "export function int32ToFloat32(value: number): number {\n    const buffer = new DataView(new ArrayBuffer(4));\n    buffer.setInt32(0, value);\n    return buffer.getFloat32(0);\n}\n"
  },
  {
    "path": "src/util/stream.ts",
    "content": "export async function* makeTextFileLineIterator(file: File): AsyncGenerator<string> {\n    const decoder = new TextDecoder(\"utf-8\");\n    const reader = file.stream().getReader();\n    let { value: chunk, done } = await reader.read();\n    let text = chunk ? decoder.decode(chunk, { stream: true }) : \"\";\n    const lineEndingRegex = /\\r\\n|\\n|\\r/gm;\n    let startIndex = 0;\n    while (true) {\n        const match = lineEndingRegex.exec(text);\n        if (match) {\n            yield text.substring(startIndex, match.index);\n            startIndex = lineEndingRegex.lastIndex;\n        }\n        else {\n            if (done)\n                break;\n            const remaining = text.substr(startIndex);\n            ({ value: chunk, done } = await reader.read());\n            text = remaining + (chunk ? decoder.decode(chunk, { stream: true }) : \"\");\n            startIndex = lineEndingRegex.lastIndex = 0;\n        }\n    }\n    if (startIndex < text.length) {\n        yield text.substr(startIndex);\n    }\n}\n"
  },
  {
    "path": "src/util/string.ts",
    "content": "import { Base64 } from './Base64';\nexport function pad(value: string | number, formatPattern: string = \"0000\"): string {\n    const strValue = String(value);\n    if (strValue.length >= formatPattern.length) {\n        return strValue;\n    }\n    return formatPattern.substring(0, formatPattern.length - strValue.length) + strValue;\n}\nexport function equalsIgnoreCase(strA: string, strB: string): boolean {\n    if (strA === null || strA === undefined || strB === null || strB === undefined) {\n        return strA === strB;\n    }\n    return strA.toLowerCase() === strB.toLowerCase();\n}\nexport function binaryStringToUint8Array(binaryStr: string): Uint8Array {\n    const length = binaryStr.length;\n    const bytes = new Uint8Array(length);\n    for (let i = 0; i < length; i++) {\n        const charCode = binaryStr.charCodeAt(i);\n        if (charCode > 255) {\n            console.warn(`Invalid character in binaryStringToUint8Array at index ${i}: ${binaryStr[i]} (charCode ${charCode})`);\n            bytes[i] = charCode & 0xFF;\n        }\n        else {\n            bytes[i] = charCode;\n        }\n    }\n    return bytes;\n}\nexport function base64StringToUint8Array(base64Str: string): Uint8Array {\n    const decodedString = Base64.decode(base64Str);\n    return binaryStringToUint8Array(decodedString);\n}\nexport function uint8ArrayToBinaryString(bytes: Uint8Array | ReadonlyArray<number>): string {\n    let result = \"\";\n    for (let i = 0; i < bytes.length; i++) {\n        result += String.fromCharCode(bytes[i]);\n    }\n    return result;\n}\nexport function uint8ArrayToBase64String(bytes: Uint8Array | ReadonlyArray<number>): string {\n    const binaryString = uint8ArrayToBinaryString(bytes);\n    return Base64.encode(binaryString);\n}\nexport function utf16ToBinaryString(str: string): string {\n    const length = str.length;\n    let binary = \"\";\n    for (let i = 0; i < length; i++) {\n        const charCode = str.charCodeAt(i);\n        binary += String.fromCharCode(charCode >> 8);\n        binary += String.fromCharCode(charCode & 0xFF);\n    }\n    return binary;\n}\nexport function binaryStringToUtf16(binaryStr: string): string {\n    const length = binaryStr.length;\n    let utf16 = \"\";\n    if (length % 2 !== 0) {\n        console.warn(\"binaryStringToUtf16: Input binary string length is odd. Last byte will be ignored.\");\n    }\n    for (let i = 0; i < Math.floor(length / 2) * 2; i += 2) {\n        const highByte = binaryStr.charCodeAt(i);\n        const lowByte = binaryStr.charCodeAt(i + 1);\n        if (highByte > 255 || lowByte > 255) {\n            console.warn(`Invalid byte sequence in binaryStringToUtf16 at index ${i}`);\n        }\n        utf16 += String.fromCharCode((highByte << 8) | lowByte);\n    }\n    return utf16;\n}\nexport function bufferToHexString(buffer: ArrayBuffer): string {\n    const hexChars: string[] = [];\n    const dataView = new DataView(buffer);\n    const bytePattern = \"00000000\";\n    const numUint32 = Math.floor(dataView.byteLength / 4);\n    for (let i = 0; i < numUint32; i++) {\n        const uint32Value = dataView.getUint32(i * 4, false);\n        const hexString = uint32Value.toString(16);\n        hexChars.push((bytePattern + hexString).slice(-8));\n    }\n    const remainingBytes = dataView.byteLength % 4;\n    if (remainingBytes > 0) {\n        let lastChunkHex = \"\";\n        for (let i = 0; i < remainingBytes; i++) {\n            const byte = dataView.getUint8(numUint32 * 4 + i);\n            lastChunkHex += (byte < 16 ? '0' : '') + byte.toString(16);\n        }\n        hexChars.push(lastChunkHex);\n        console.warn(`bufferToHexString: Buffer length ${dataView.byteLength} is not a multiple of 4. Remaining ${remainingBytes} bytes processed individually.`);\n    }\n    return hexChars.join(\"\").toUpperCase();\n}\n"
  },
  {
    "path": "src/util/time.ts",
    "content": "export async function sleep(milliseconds: number): Promise<void> {\n    return new Promise((resolve) => {\n        setTimeout(() => resolve(), milliseconds);\n    });\n}\nexport function throttle<T extends (...args: any[]) => Promise<any>>(func: T, delay: number): T {\n    let inProgress = false;\n    let lastCallTime = Number.NEGATIVE_INFINITY;\n    const throttledFunc = async function (this: ThisParameterType<T>, ...args: Parameters<T>): Promise<ReturnType<T>> {\n        if (inProgress) {\n            return Promise.resolve(undefined as any);\n        }\n        const currentTime = Date.now();\n        const timeSinceLastCall = currentTime - lastCallTime;\n        if (delay <= timeSinceLastCall) {\n            lastCallTime = currentTime;\n            return await func.apply(this, args);\n        }\n        else {\n            inProgress = true;\n            await sleep(delay - timeSinceLastCall);\n            lastCallTime = Date.now();\n            inProgress = false;\n            return await func.apply(this, args);\n        }\n    } as T;\n    return throttledFunc;\n}\nexport function createThrottledMethod<T extends (...args: any[]) => Promise<any>>(func: T, delay: number): T {\n    return throttle(func, delay);\n}\n"
  },
  {
    "path": "src/util/typeGuard.ts",
    "content": "export function isNotNullOrUndefined<T>(value: T | null | undefined): value is T {\n    return value != null;\n}\n"
  },
  {
    "path": "src/util/userAgent.ts",
    "content": "export function isIpad(): boolean {\n    return (/iPad/i.test(navigator.userAgent) ||\n        (/MacIntel/i.test(navigator.platform) && !!navigator.maxTouchPoints));\n}\nexport function isMac(): boolean {\n    return navigator.platform.includes(\"Mac\");\n}\nexport function isMacFirefox(): boolean {\n    return isMac() && navigator.userAgent.toLowerCase().includes(\"firefox\");\n}\n"
  },
  {
    "path": "src/version.ts",
    "content": "export const version: string = \"0.0.1\";\n"
  },
  {
    "path": "src/vite-env.d.ts",
    "content": ""
  },
  {
    "path": "tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noFallthroughCasesInSwitch\": false\n  },\n  \"include\": [\n    \"src/main.tsx\",\n    \"src/App.tsx\",\n    \"src/vite-env.d.ts\",\n    \"src/types/**/*.d.ts\"\n  ],\n  \"exclude\": [\n    \"src/test/**\",\n    \"src/tools/**\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": false,\n    \"noImplicitAny\": false,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \n    /* Path Aliases */\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    }\n  },\n  \"include\": [\"src\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n} "
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n} "
  },
  {
    "path": "vite.config.ts",
    "content": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport basicSsl from '@vitejs/plugin-basic-ssl';\nimport fs from 'fs';\nconst devPort = 4000;\nconst manualHttpsConfig = fs.existsSync('./certs/server.key') && fs.existsSync('./certs/server.crt')\n    ? { key: fs.readFileSync('./certs/server.key'), cert: fs.readFileSync('./certs/server.crt') }\n    : undefined;\nexport default defineConfig({\n    plugins: [react(), ...(manualHttpsConfig ? [] : [basicSsl()])],\n    server: {\n        host: '0.0.0.0',\n        port: devPort,\n        strictPort: true,\n        https: manualHttpsConfig ?? {},\n        headers: {\n            'Cross-Origin-Embedder-Policy': 'require-corp',\n            'Cross-Origin-Opener-Policy': 'same-origin',\n        },\n        fs: {\n            allow: ['..']\n        }\n    },\n    preview: {\n        host: '0.0.0.0',\n        port: devPort,\n        strictPort: true,\n    },\n    resolve: {\n        alias: {\n            '@': '/src'\n        }\n    },\n    optimizeDeps: {\n        exclude: ['7z-wasm', '@ffmpeg/ffmpeg'],\n        include: []\n    },\n    worker: {\n        format: 'es'\n    },\n    assetsInclude: ['**/*.wasm']\n});\n"
  }
]